إنشاء طرق عرض مخصّصة

هذا الدرس العملي حول الترميز هو جزء من دورة "تطبيقات متقدّمة متوافقة مع نظام Android باستخدام لغة Kotlin". ستستفيد إلى أقصى حدّ من هذه الدورة التدريبية إذا تابعت دروس الترميز بالتسلسل، ولكن هذا ليس إلزاميًا. يمكنك الاطّلاع على جميع دورات الترميز في الدورة التدريبية على الصفحة المقصودة لدورات الترميز في "تطبيقات متقدمة متوافقة مع نظام Android باستخدام لغة Kotlin".

مقدمة

يوفّر نظام التشغيل Android مجموعة كبيرة من الفئات الفرعية View، مثل Button أو TextView أو EditText أو ImageView أو CheckBox أو RadioButton. يمكنك استخدام هذه الفئات الفرعية لإنشاء واجهة مستخدم تتيح تفاعل المستخدم وعرض المعلومات في تطبيقك. وإذا لم تستوفِ أي من الفئات الفرعية View احتياجاتك، يمكنك إنشاء فئة فرعية View تُعرف باسم طريقة العرض المخصّصة.

لإنشاء طريقة عرض مخصّصة، يمكنك إما توسيع فئة فرعية حالية من View (مثل Button أو EditText)، أو إنشاء فئة فرعية خاصة بك من View. من خلال توسيع View مباشرةً، يمكنك إنشاء عنصر تفاعلي في واجهة المستخدم بأي حجم وشكل عن طريق إلغاء طريقة onDraw() الخاصة بـ View لرسمه.

بعد إنشاء طريقة عرض مخصّصة، يمكنك إضافتها إلى تخطيطات الأنشطة بالطريقة نفسها التي تضيف بها TextView أو Button.

يوضّح لك هذا الدرس كيفية إنشاء عرض مخصّص من البداية من خلال توسيع View.

ما يجب معرفته

  • كيفية إنشاء تطبيق يتضمّن نشاطًا وتشغيله باستخدام "استوديو Android"

أهداف الدورة التعليمية

  • كيفية توسيع نطاق View لإنشاء طريقة عرض مخصّصة
  • كيفية رسم طريقة عرض مخصّصة دائرية الشكل
  • كيفية استخدام أدوات معالجة الأحداث للتعامل مع تفاعل المستخدم مع العرض المخصّص
  • كيفية استخدام طريقة عرض مخصّصة في تصميم

الإجراءات التي ستنفذّها

  • وسِّع View لإنشاء طريقة عرض مخصّصة.
  • تهيئة العرض المخصّص باستخدام قيم الرسم والتلوين
  • استبدِل onDraw() لرسم طريقة العرض.
  • استخدِم أدوات معالجة الأحداث لتوفير سلوك العرض المخصّص.
  • أضِف العرض المخصّص إلى تخطيط.

يوضّح تطبيق CustomFanController كيفية إنشاء فئة فرعية مخصّصة للعرض من خلال توسيع الفئة View. يُطلق على الفئة الفرعية الجديدة اسم DialView.

يعرض التطبيق عنصر واجهة مستخدم دائريًا يشبه أداة التحكّم المادية في المروحة، مع إعدادات الإيقاف (0) والسرعة المنخفضة (1) والمتوسطة (2) والعالية (3). عندما ينقر المستخدم على طريقة العرض، ينتقل مؤشر التحديد إلى الموضع التالي: 0-1-2-3، ثم يعود إلى 0. بالإضافة إلى ذلك، إذا كان مستوى التحديد 1 أو أعلى، يتغيّر لون الخلفية للجزء الدائري من العرض من الرمادي إلى الأخضر (ما يشير إلى أنّ مروحة الشفط تعمل).

طرق العرض هي الوحدات الأساسية لواجهة مستخدم التطبيق. يوفّر الصف View العديد من الفئات الفرعية، التي يُشار إليها باسم عناصر واجهة المستخدم، والتي تغطي العديد من احتياجات واجهة المستخدم لأي تطبيق Android عادي.

وحدات إنشاء واجهة المستخدم، مثل Button وTextView، هي فئات فرعية توسّع الفئة View. لتوفير الوقت والجهد في التطوير، يمكنك توسيع إحدى فئات View الفرعية هذه. تتضمّن طريقة العرض المخصّصة مظهر العنصر الأصل وسلوكه، ويمكنك تجاهل السلوك أو جانب المظهر الذي تريد تغييره. على سبيل المثال، إذا وسّعت نطاق EditText لإنشاء طريقة عرض مخصّصة، ستعمل طريقة العرض تمامًا مثل طريقة عرض EditText، ولكن يمكن أيضًا تخصيصها لعرض زر X، على سبيل المثال، يمحو النص من حقل إدخال النص.

يمكنك توسيع أي فئة فرعية من View، مثل EditText، للحصول على طريقة عرض مخصّصة، واختَر الفئة الأقرب إلى ما تريد تحقيقه. يمكنك بعد ذلك استخدام طريقة العرض المخصّصة مثل أي فئة فرعية أخرى من View في تصميم واحد أو أكثر كعنصر XML يتضمّن سمات.

لإنشاء طريقة عرض مخصّصة من البداية، عليك توسيع فئة View نفسها. تتجاوز التعليمات البرمجية طرق View لتحديد مظهر العرض ووظائفه. إنّ المفتاح لإنشاء طريقة عرض مخصّصة هو أنّك المسؤول عن رسم عنصر واجهة المستخدم بالكامل بأي حجم وشكل على الشاشة. إذا أنشأت فئة فرعية من طريقة عرض حالية، مثل Button، ستتولّى هذه الفئة عملية الرسم نيابةً عنك. (ستتعرّف على المزيد حول الرسم لاحقًا في هذا الدرس التطبيقي حول الترميز).

لإنشاء طريقة عرض مخصّصة، اتّبِع الخطوات العامة التالية:

  • أنشئ فئة طريقة عرض مخصّصة توسّع View أو توسّع فئة فرعية من View (مثل Button أو EditText).
  • في حال توسيع فئة فرعية View حالية، عليك إلغاء السلوك أو الجوانب المتعلقة بالمظهر التي تريد تغييرها فقط.
  • في حال توسيع الفئة View، ارسم شكل العرض المخصّص وتحكَّم في مظهره من خلال إلغاء طرق View، مثل onDraw() وonMeasure()، في الفئة الجديدة.
  • أضِف رمزًا برمجيًا للردّ على تفاعل المستخدم، وأعِد رسم العرض المخصّص إذا لزم الأمر.
  • استخدِم فئة العرض المخصّص كعنصر واجهة مستخدم في تخطيط XML الخاص بنشاطك. يمكنك أيضًا تحديد سمات مخصّصة للعرض، وذلك لتوفير إمكانية تخصيص العرض في تنسيقات مختلفة.

في هذه المهمة، عليك إجراء ما يلي:

  • أنشئ تطبيقًا يتضمّن ImageView كعنصر نائب مؤقت لطريقة العرض المخصّصة.
  • وسِّع View لإنشاء طريقة العرض المخصّصة.
  • تهيئة العرض المخصّص باستخدام قيم الرسم والتلوين

الخطوة 1: إنشاء تطبيق يتضمّن عنصر نائب ImageView

  1. أنشئ تطبيق Kotlin بعنوان CustomFanController باستخدام نموذج "نشاط فارغ". تأكَّد من أنّ اسم الحزمة هو com.example.android.customfancontroller.
  2. افتح activity_main.xml في علامة التبويب نص لتعديل رمز XML.
  3. استبدِل TextView الحالي بهذا الرمز. يعمل هذا النص كتصنيف في النشاط للعرض المخصّص.
<TextView
       android:id="@+id/customViewLabel"
       android:textAppearance="@style/Base.TextAppearance.AppCompat.Display3"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:padding="16dp"
       android:textColor="@android:color/black"
       android:layout_marginStart="8dp"
       android:layout_marginEnd="8dp"
       android:layout_marginTop="24dp"
       android:text="Fan Control"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       app:layout_constraintTop_toTopOf="parent"/>
  1. أضِف عنصر ImageView هذا إلى التنسيق. هذا عنصر نائب لطريقة العرض المخصّصة التي ستنشئها في هذا الدرس العملي.
<ImageView
       android:id="@+id/dialView"
       android:layout_width="200dp"
       android:layout_height="200dp"
       android:background="@android:color/darker_gray"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="8dp"
       android:layout_marginRight="8dp"
       android:layout_marginTop="8dp"/>
  1. استخرِج موارد السلاسل والأبعاد في كلٍّ من عناصر واجهة المستخدم.
  2. انقر على علامة التبويب التصميم. يجب أن يبدو التنسيق على النحو التالي:

الخطوة 2: إنشاء فئة طريقة العرض المخصّصة

  1. أنشئ فئة Kotlin جديدة باسم DialView.
  2. عدِّل تعريف الفئة لتوسيع View. استورِد android.view.View عندما يُطلب منك ذلك.
  3. انقر على View، ثم انقر على المصباح الأحمر. اختَر إضافة منشئات Android View باستخدام ‎@JvmOverloads. يضيف Android Studio الدالة الإنشائية من الفئة View. تطلب التعليق التوضيحي @JvmOverloads من مترجم Kotlin إنشاء عمليات تحميل زائد لهذه الدالة تستبدل قيم المَعلمات التلقائية.
class DialView @JvmOverloads constructor(
   context: Context,
   attrs: AttributeSet? = null,
   defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
  1. أضِف enum في أعلى مستوى لتمثيل سرعات المروحة المتاحة، وذلك فوق تعريف الفئة DialView مباشرةً أسفل عمليات الاستيراد. يُرجى العِلم أنّ enum هذا من النوع Int لأنّ القيم هي موارد سلسلة وليست سلاسل فعلية. سيعرض "استوديو Android" أخطاءً بشأن موارد السلسلة غير المتوفّرة في كل قيمة من هذه القيم، وستعمل على إصلاح ذلك في خطوة لاحقة.
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);
}
  1. أسفل enum، أضِف الثوابت التالية. ستستخدم هذه القيم كجزء من رسم مؤشرات القرص وعلاماته.
private const val RADIUS_OFFSET_LABEL = 30      
private const val RADIUS_OFFSET_INDICATOR = -35
  1. داخل فئة DialView، حدِّد عدة متغيرات تحتاج إليها لرسم العرض المخصّص. استورِد android.graphics.PointF إذا طُلب منك ذلك.
private var radius = 0.0f                   // Radius of the circle.
private var fanSpeed = FanSpeed.OFF         // The active selection.
// position variable which will be used to draw label and indicator circle position
private val pointPosition: PointF = PointF(0.0f, 0.0f)
  • radius هو نصف قطر الدائرة الحالي. يتم ضبط هذه القيمة عند رسم طريقة العرض على الشاشة.
  • تمثّل fanSpeed سرعة المروحة الحالية، وهي إحدى القيم في التعداد FanSpeed. القيمة التلقائية هي OFF.
  • أخيرًا، postPosition هي نقطة X وY سيتم استخدامها لرسم العديد من عناصر العرض على الشاشة.

يتم إنشاء هذه القيم وتهيئتها هنا بدلاً من وقت رسم العرض فعليًا، وذلك لضمان تنفيذ خطوة الرسم الفعلية بأسرع ما يمكن.

  1. داخل تعريف الفئة DialView أيضًا، يمكنك تهيئة كائن Paint باستخدام بعض الأنماط الأساسية. استورِد android.graphics.Paint وandroid.graphics.Typeface عند الطلب. كما هو الحال مع المتغيرات السابقة، يتم تهيئة هذه الأنماط هنا للمساعدة في تسريع خطوة الرسم.
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
   style = Paint.Style.FILL
   textAlign = Paint.Align.CENTER
   textSize = 55.0f
   typeface = Typeface.create( "", Typeface.BOLD)
}
  1. افتح res/values/strings.xml وأضِف موارد السلاسل لسرعات المروحة:
<string name="fan_off">off</string>
<string name="fan_low">1</string>
<string name="fan_medium">2</string>
<string name="fan_high">3</string>

بعد إنشاء طريقة عرض مخصّصة، يجب أن تتمكّن من رسمها. عند توسيع فئة فرعية من View، مثل EditText، تحدّد هذه الفئة الفرعية مظهر العرض وسماته وترسم نفسها على الشاشة. وبالتالي، ليس عليك كتابة رمز لرسم العرض. يمكنك بدلاً من ذلك إلغاء طرق العرض الرئيسية لتخصيص طريقة العرض.

إذا كنت تنشئ طريقة عرض خاصة بك من البداية (عن طريق توسيع View)، ستكون مسؤولاً عن رسم طريقة العرض بأكملها في كل مرة يتم فيها إعادة تحميل الشاشة، وعن إلغاء طرق View التي تتعامل مع الرسم. من أجل رسم عرض مخصّص يمتد إلى View بشكل صحيح، عليك إجراء ما يلي:

  • احتساب حجم العرض عند ظهوره لأول مرة، وفي كل مرة يتغير فيها حجم العرض، وذلك من خلال إلغاء طريقة onSizeChanged().
  • يمكنك إلغاء طريقة onDraw() لرسم العرض المخصّص، وذلك باستخدام عنصر Canvas منمّق بواسطة عنصر Paint.
  • استدعِ طريقة invalidate() عند الاستجابة لنقرة المستخدم التي تغيّر طريقة رسم العرض لإبطال العرض بأكمله، ما يؤدي إلى فرض استدعاء onDraw() لإعادة رسم العرض.

يتم استدعاء الطريقة onDraw() في كل مرة يتم فيها إعادة تحميل الشاشة، ويمكن أن يحدث ذلك عدة مرات في الثانية. لأسباب تتعلّق بالأداء وتجنُّب حدوث أخطاء مرئية، عليك تنفيذ أقل قدر ممكن من العمل في onDraw(). على وجه الخصوص، لا تضع عمليات التخصيص في onDraw()، لأنّ عمليات التخصيص قد تؤدي إلى جمع البيانات غير الضرورية، ما قد يتسبب في حدوث تقطّع مرئي.

تقدّم الفئتان Canvas وPaint عددًا من اختصارات الرسم المفيدة:

  • ارسم نصًا باستخدام drawText(). حدِّد نوع الخط من خلال استدعاء setTypeface()، ولون النص من خلال استدعاء setColor().
  • ارسم أشكالاً أساسية باستخدام drawRect() وdrawOval() وdrawArc(). يمكنك تغيير ما إذا كانت الأشكال مملوءة أو محدّدة أو كليهما من خلال استدعاء setStyle().
  • ارسم صورًا نقطية باستخدام drawBitmap().

ستتعرّف على المزيد من المعلومات حول Canvas وPaint في درس تطبيقي حول الترميز لاحقًا. لمزيد من المعلومات حول كيفية عرض Android للعناصر، يمكنك الاطّلاع على كيفية عرض Android للعناصر.

في هذه المهمة، سترسم العرض المخصّص لعنصر التحكّم في المروحة على الشاشة، أي القرص نفسه ومؤشر الموضع الحالي وتسميات المؤشر، باستخدام الطريقتَين onSizeChanged() وonDraw(). ستنشئ أيضًا طريقة مساعدة، computeXYForSpeed(),، لاحتساب موضع X وY الحالي لتصنيف المؤشر على القرص.

الخطوة 1: احتساب المواضع ورسم العرض

  1. في الفئة DialView، أسفل عمليات التهيئة، يمكنك إلغاء طريقة onSizeChanged() من الفئة View لحساب حجم قرص العرض المخصّص. استيراد kotlinmath.min عند الطلب.

    يتم استدعاء طريقة onSizeChanged() في كل مرة يتغير فيها حجم العرض، بما في ذلك المرة الأولى التي يتم فيها رسمه عند تضخيم التنسيق. يمكنك إلغاء onSizeChanged() لاحتساب المواضع والأبعاد وأي قيم أخرى ذات صلة بحجم العرض المخصّص، بدلاً من إعادة احتسابها في كل مرة ترسم فيها. في هذه الحالة، يمكنك استخدام onSizeChanged() لحساب نصف القطر الحالي لعنصر الدائرة في القرص.
override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) {
   radius = (min(width, height) / 2.0 * 0.8).toFloat()
}
  1. أسفل onSizeChanged()، أضِف الرمز التالي لتحديد دالة إضافة computeXYForSpeed() لفئة PointF . استورِد kotlin.math.cos وkotlin.math.sin عند الطلب. تحسب دالة الإضافة هذه في الفئة PointF إحداثيات X وY على الشاشة لتسمية النص والمؤشر الحالي (0 أو 1 أو 2 أو 3)، وذلك بالنظر إلى موضع FanSpeed الحالي ونصف قطر القرص. ستستخدم هذا في onDraw().
private fun PointF.computeXYForSpeed(pos: FanSpeed, radius: Float) {
   // Angles are in radians.
   val startAngle = Math.PI * (9 / 8.0)   
   val angle = startAngle + pos.ordinal * (Math.PI / 4)
   x = (radius * cos(angle)).toFloat() + width / 2
   y = (radius * sin(angle)).toFloat() + height / 2
}
  1. يمكنك إلغاء طريقة onDraw() لعرض طريقة العرض على الشاشة باستخدام الفئتين Canvas وPaint. استورِد android.graphics.Canvas عند الطلب. في ما يلي عملية إلغاء الهيكل:
override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   
}
  1. داخل onDraw()، أضِف هذا السطر لضبط لون الطلاء على الرمادي (Color.GRAY) أو الأخضر (Color.GREEN) استنادًا إلى ما إذا كانت سرعة المروحة هي OFF أو أي قيمة أخرى. استورِد android.graphics.Color عند الطلب.
// Set dial background color to green if selection not off.
paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
  1. أضِف هذا الرمز لرسم دائرة للقرص باستخدام طريقة drawCircle(). تستخدم هذه الطريقة عرض الرمز الحالي وارتفاعه للعثور على مركز الدائرة ونصف قطرها ولون الطلاء الحالي. السمتان width وheight هما عضوان في الفئة الفائقة View وتشيران إلى الأبعاد الحالية للعرض.
// Draw the dial.
canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
  1. أضِف الرمز التالي لرسم دائرة أصغر لعلامة مؤشر سرعة المروحة، وذلك باستخدام طريقة drawCircle() أيضًا. يستخدم هذا الجزء PointF.طريقة computeXYforSpeed() الإضافية لاحتساب إحداثيات X وY لمركز المؤشر استنادًا إلى سرعة المروحة الحالية
// Draw the indicator circle.
val markerRadius = radius + RADIUS_OFFSET_INDICATOR
pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
paint.color = Color.BLACK
canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
  1. أخيرًا، ارسم تصنيفات سرعة المروحة (0 و1 و2 و3) في المواضع المناسبة حول القرص. يستدعي هذا الجزء من الطريقة PointF.computeXYForSpeed() مرة أخرى للحصول على موضع كل تصنيف، ويعيد استخدام الكائن pointPosition في كل مرة لتجنُّب عمليات التخصيص. استخدِم drawText() لرسم التصنيفات.
// Draw the text labels.
val labelRadius = radius + RADIUS_OFFSET_LABEL
for (i in FanSpeed.values()) {
   pointPosition.computeXYForSpeed(i, labelRadius)
   val label = resources.getString(i.label)
   canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
}

يبدو onDraw() المكتمل على النحو التالي:

override fun onDraw(canvas: Canvas) {
   super.onDraw(canvas)
   // Set dial background color to green if selection not off.
   paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN
   // Draw the dial.
   canvas.drawCircle((width / 2).toFloat(), (height / 2).toFloat(), radius, paint)
   // Draw the indicator circle.
   val markerRadius = radius + RADIUS_OFFSET_INDICATOR
   pointPosition.computeXYForSpeed(fanSpeed, markerRadius)
   paint.color = Color.BLACK
   canvas.drawCircle(pointPosition.x, pointPosition.y, radius/12, paint)
   // Draw the text labels.
   val labelRadius = radius + RADIUS_OFFSET_LABEL
   for (i in FanSpeed.values()) {
       pointPosition.computeXYForSpeed(i, labelRadius)
       val label = resources.getString(i.label)
       canvas.drawText(label, pointPosition.x, pointPosition.y, paint)
   }
}

الخطوة 2: إضافة العرض إلى التنسيق

لإضافة طريقة عرض مخصّصة إلى واجهة مستخدم التطبيق، عليك تحديدها كعنصر في تنسيق XML الخاص بالنشاط. يمكنك التحكّم في مظهره وسلوكه باستخدام سمات عناصر XML، كما تفعل مع أي عنصر آخر في واجهة المستخدم.

  1. في activity_main.xml، غيِّر العلامة ImageView الخاصة بـ dialView إلى com.example.android.customfancontroller.DialView، واحذف السمة android:background. تكتسب كلّ من DialView وImageView الأصلية السمات العادية من الفئة View، لذلك لا داعي لتغيير أي من السمات الأخرى. يبدو العنصر الجديد DialView على النحو التالي:
<com.example.android.customfancontroller.DialView
       android:id="@+id/dialView"
       android:layout_width="@dimen/fan_dimen"
       android:layout_height="@dimen/fan_dimen"
       app:layout_constraintTop_toBottomOf="@+id/customViewLabel"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintRight_toRightOf="parent"
       android:layout_marginLeft="@dimen/default_margin"
       android:layout_marginRight="@dimen/default_margin"
       android:layout_marginTop="@dimen/default_margin" />
  1. شغِّل التطبيق. سيظهر عرض التحكّم في المروحة في النشاط.

المهمة الأخيرة هي تفعيل العرض المخصّص لتنفيذ إجراء عندما ينقر المستخدم على العرض. يجب أن يؤدي كل نقرة إلى نقل مؤشر التحديد إلى الموضع التالي: إيقاف-1-2-3 والرجوع إلى إيقاف. بالإضافة إلى ذلك، إذا كان التحديد 1 أو أعلى، يجب تغيير الخلفية من الرمادي إلى الأخضر، ما يشير إلى أنّ مروحة الشفط تعمل.

لإتاحة النقر على طريقة العرض المخصّصة، عليك اتّخاذ الإجراءات التالية:

  • اضبط السمة isClickable للعرض على true. يتيح ذلك لعرضك المخصّص الاستجابة للنقرات.
  • نفِّذ performClick() للفئة View لتنفيذ عمليات عند النقر على طريقة العرض.
  • استدعِ طريقة invalidate(). يطلب هذا الرمز من نظام التشغيل Android استدعاء الطريقة onDraw() لإعادة رسم العرض.

في العادة، عند استخدام طريقة عرض عادية في Android، يمكنك تنفيذ OnClickListener() لتنفيذ إجراء عندما ينقر المستخدم على طريقة العرض هذه. بالنسبة إلى طريقة العرض المخصّصة، يمكنك تنفيذ طريقة performClick() للفئة View بدلاً من ذلك، واستدعاء super.performClick(). تستدعي الطريقة التلقائية performClick() أيضًا onClickListener()، لذا يمكنك إضافة إجراءاتك إلى performClick() وترك onClickListener() متاحًا لمزيد من التخصيص من جانبك أو من جانب مطوّرين آخرين قد يستخدمون طريقة العرض المخصّصة.

  1. في DialView.kt، داخل تعداد FanSpeed، أضِف دالة ملحقة next() تغيّر سرعة المروحة الحالية إلى السرعة التالية في القائمة (من OFF إلى LOW وMEDIUM وHIGH، ثم العودة إلى OFF). يبدو التعداد الكامل الآن على النحو التالي:
private enum class FanSpeed(val label: Int) {
   OFF(R.string.fan_off),
   LOW(R.string.fan_low),
   MEDIUM(R.string.fan_medium),
   HIGH(R.string.fan_high);

   fun next() = when (this) {
       OFF -> LOW
       LOW -> MEDIUM
       MEDIUM -> HIGH
       HIGH -> OFF
   }
}
  1. داخل الفئة DialView، قبل الطريقة onSizeChanged() مباشرةً، أضِف كتلة init(). يؤدي ضبط السمة isClickable للعرض على "صحيح" إلى السماح لهذا العرض بقبول إدخال المستخدم.
init {
   isClickable = true
}
  1. أسفل init(),، استبدِل طريقة performClick() بالرمز أدناه.
override fun performClick(): Boolean {
   if (super.performClick()) return true

   fanSpeed = fanSpeed.next()
   contentDescription = resources.getString(fanSpeed.label)
  
   invalidate()
   return true
}

المكالمة إلى superيجب تنفيذ performClick() أولاً، ما يتيح أحداث تسهيل الاستخدام بالإضافة إلى المكالمات onClickListener().

يزيد السطران التاليان سرعة المروحة باستخدام الطريقة next()، ويضبطان وصف محتوى العرض على مورد السلسلة الذي يمثّل السرعة الحالية (إيقاف أو 1 أو 2 أو 3).

أخيرًا، يبطل الأسلوب invalidate() العرض بأكمله، ما يؤدي إلى فرض طلب إلى onDraw() لإعادة رسم العرض. إذا تغيّر أي شيء في العرض المخصّص لأي سبب، بما في ذلك تفاعل المستخدم، وكان يجب عرض التغيير، استخدِم الدالة invalidate()..

  1. شغِّل التطبيق. انقر على العنصر DialView لنقل المؤشر من "إيقاف" إلى 1. يجب أن يتحول لون القرص إلى أخضر. مع كل نقرة، يجب أن ينتقل المؤشر إلى الموضع التالي. عندما يعود المؤشر إلى وضع الإيقاف، يجب أن يعود القرص إلى اللون الرمادي مرة أخرى.

يوضّح هذا المثال الآليات الأساسية لاستخدام السمات المخصّصة مع العرض المخصّص. يمكنك تحديد سمات مخصّصة للفئة DialView باستخدام لون مختلف لكل موضع من مواضع قرص المروحة.

  1. أنشئ الملف res/values/attrs.xml وافتحه.
  2. داخل <resources>، أضِف عنصر مورد <declare-styleable>.
  3. داخل عنصر المورد <declare-styleable>، أضِف ثلاثة عناصر attr، عنصرًا واحدًا لكل سمة، مع name وformat. format هو نوع، وفي هذه الحالة، يكون color.
<?xml version="1.0" encoding="utf-8"?>
<resources>
       <declare-styleable name="DialView">
           <attr name="fanColor1" format="color" />
           <attr name="fanColor2" format="color" />
           <attr name="fanColor3" format="color" />
       </declare-styleable>
</resources>
  1. افتح ملف التنسيق activity_main.xml.
  2. في DialView، أضِف سمات fanColor1 وfanColor2 وfanColor3، واضبط قيمها على الألوان الموضّحة أدناه. استخدِم app: كبادئة للسمة المخصّصة (كما في app:fanColor1) بدلاً من android: لأنّ سماتك المخصّصة تنتمي إلى مساحة الاسم schemas.android.com/apk/res/your_app_package_name وليس إلى مساحة الاسم android.
app:fanColor1="#FFEB3B"
app:fanColor2="#CDDC39"
app:fanColor3="#009688"

لاستخدام السمات في الفئة DialView، عليك استردادها. يتم تخزينها في AttributeSet يتم تسليمه إلى صفك عند إنشائه، إذا كان متوفّرًا. يمكنك استرداد السمات في init، وتعيين قيم السمات إلى متغيرات محلية للتخزين المؤقت.

  1. افتح ملف الصف DialView.kt.
  2. داخل DialView، عرِّف متغيرات لتخزين قيم السمات مؤقتًا.
private var fanSpeedLowColor = 0
private var fanSpeedMediumColor = 0
private var fanSeedMaxColor = 0
  1. في الحظر init، أضِف الرمز التالي باستخدام دالة الإضافة withStyledAttributes. عليك توفير السمات وطريقة العرض وتحديد المتغيرات المحلية. سيؤدي استيراد withStyledAttributes أيضًا إلى استيراد الدالة getColor() الصحيحة.
context.withStyledAttributes(attrs, R.styleable.DialView) {
   fanSpeedLowColor = getColor(R.styleable.DialView_fanColor1, 0)
   fanSpeedMediumColor = getColor(R.styleable.DialView_fanColor2, 0)
   fanSeedMaxColor = getColor(R.styleable.DialView_fanColor3, 0)
}
  1. استخدِم المتغيرات المحلية في onDraw()لضبط لون القرص استنادًا إلى سرعة المروحة الحالية. استبدِل السطر الذي تم فيه ضبط لون الطلاء (paint.color = if (fanSpeed == FanSpeed.OFF) Color.GRAY else Color.GREEN) بالرمز أدناه.
paint.color = when (fanSpeed) {
   FanSpeed.OFF -> Color.GRAY
   FanSpeed.LOW -> fanSpeedLowColor
   FanSpeed.MEDIUM -> fanSpeedMediumColor
   FanSpeed.HIGH -> fanSeedMaxColor
} as Int
  1. شغِّل تطبيقك، وانقر على القرص، ويجب أن يكون إعداد اللون مختلفًا لكل موضع، كما هو موضّح أدناه.

لمزيد من المعلومات عن سمات العرض المخصّصة، اطّلِع على إنشاء فئة عرض.

تسهيل الاستخدام هو مجموعة من تقنيات التصميم والتنفيذ والاختبار التي تتيح للجميع استخدام تطبيقك، بما في ذلك الأشخاص ذوو الاحتياجات الخاصة.

تشمل الإعاقات الشائعة التي يمكن أن تؤثر في استخدام جهاز Android العمى وضعف النظر وعمى الألوان والصمم أو فقدان السمع ومحدودية المهارات الحركية. عند تطوير تطبيقاتك مع مراعاة سهولة الاستخدام، فإنّك تحسّن تجربة المستخدم ليس فقط للمستخدمين الذين يعانون من هذه الإعاقات، ولكن أيضًا لجميع المستخدمين الآخرين.

يوفّر نظام التشغيل Android العديد من ميزات تسهيل الاستخدام تلقائيًا في طرق عرض واجهة المستخدم العادية، مثل TextView وButton. عند إنشاء طريقة عرض مخصّصة، عليك مراعاة كيفية توفير ميزات تسهيل الاستخدام، مثل الأوصاف المنطوقة للمحتوى الظاهر على الشاشة.

في هذه المهمة، ستتعرّف على TalkBack، وهو قارئ الشاشة في Android، وستعدّل تطبيقك لتضمين تلميحات وأوصاف قابلة للقراءة بصوت عالٍ لعرض DialView المخصّص.

الخطوة 1: استكشاف TalkBack

‫TalkBack هو قارئ شاشة مدمَج في Android. عند تفعيل ميزة TalkBack، يمكن للمستخدم التفاعل مع جهاز Android بدون النظر إلى الشاشة، لأنّ Android يصف عناصر الشاشة بصوت عالٍ. قد يعتمد المستخدمون الذين يعانون من ضعف في النظر على TalkBack لاستخدام تطبيقك.

في هذه المهمة، ستفعّل ميزة TalkBack للتعرّف على طريقة عمل قارئات الشاشة وكيفية التنقّل في التطبيقات.

  1. على جهاز Android أو محاكي Android، انتقِل إلى الإعدادات > تسهيل الاستخدام > TalkBack.
  2. انقر على زر التبديل تفعيل/إيقاف لتفعيل TalkBack.
  3. انقر على حسنًا لتأكيد الأذونات.
  4. أكِّد كلمة مرور جهازك إذا طُلب منك ذلك. إذا كانت هذه هي المرة الأولى التي تستخدم فيها TalkBack، سيتم تشغيل برنامج تعليمي. (قد لا يتوفّر البرنامج التعليمي على الأجهزة القديمة).
  5. قد يكون من المفيد التنقّل في البرنامج التعليمي وعيناك مغلقتان. لفتح البرنامج التعليمي مرة أخرى في المستقبل، انتقِل إلى الإعدادات > تسهيل الاستخدام > TalkBack > الإعدادات > تشغيل البرنامج التعليمي عن TalkBack.
  6. يمكنك تجميع تطبيق CustomFanController وتشغيله، أو فتحه باستخدام الزر نظرة عامة أو التطبيقات الحديثة على جهازك. عند تفعيل TalkBack، لاحظ أنّه يتم الإعلان عن اسم التطبيق، بالإضافة إلى نص التصنيف TextView ("التحكّم في المروحة"). ومع ذلك، إذا نقرت على DialView العرض نفسه، لن يتم نطق أي معلومات عن حالة العرض (الإعداد الحالي للقرص) أو الإجراء الذي سيتم تنفيذه عند النقر على العرض لتفعيله.

الخطوة 2: إضافة أوصاف للمحتوى الخاص بتصنيفات الاتصال

تصف أوصاف المحتوى معنى وطريقة استخدام طرق العرض في تطبيقك. وتتيح هذه التصنيفات لبرامج قراءة الشاشة، مثل ميزة TalkBack في Android، شرح وظيفة كل عنصر بدقة. بالنسبة إلى طرق العرض الثابتة، مثل ImageView، يمكنك إضافة وصف المحتوى إلى طريقة العرض في ملف التصميم باستخدام السمة contentDescription. تستخدِم طرق عرض النصوص (TextView وEditText) تلقائيًا النص المعروض كوصف للمحتوى.

بالنسبة إلى طريقة عرض عنصر التحكّم المخصّص في سرعة المروحة، عليك تعديل وصف المحتوى بشكل ديناميكي في كل مرة يتم فيها النقر على طريقة العرض، وذلك للإشارة إلى إعدادات المروحة الحالية.

  1. في أسفل فئة DialView، عرِّف الدالة updateContentDescription() بدون وسيطات أو نوع إرجاع.
fun updateContentDescription() {
}
  1. داخل updateContentDescription()، غيِّر السمة contentDescription للعرض المخصّص إلى مورد السلسلة المرتبط بسرعة المروحة الحالية (إيقاف أو 1 أو 2 أو 3). هذه هي التصنيفات نفسها المستخدَمة في onDraw() عند رسم القرص على الشاشة.
fun updateContentDescription() {
   contentDescription = resources.getString(fanSpeed.label)
}
  1. انتقِل للأعلى إلى الحظر init()، وفي نهاية هذا الحظر، أضِف طلبًا إلى updateContentDescription(). يؤدي ذلك إلى تهيئة وصف المحتوى عند تهيئة طريقة العرض.
init {
   isClickable = true
   // ...

   updateContentDescription()
}
  1. أضِف مكالمة أخرى إلى updateContentDescription() في طريقة performClick()، قبل invalidate() مباشرةً.
override fun performClick(): Boolean {
   if (super.performClick()) return true
   fanSpeed = fanSpeed.next()
   updateContentDescription()
   invalidate()
   return true
}
  1. جمِّع التطبيق وشغِّله، وتأكَّد من تفعيل TalkBack. انقر لتغيير إعداد عرض القرص، ولاحظ أنّ TalkBack يعلن الآن عن التصنيف الحالي (إيقاف، 1، 2، 3) بالإضافة إلى العبارة "انقر مرّتين للتفعيل".

الخطوة 3: إضافة المزيد من المعلومات لإجراء النقر

يمكنك التوقف عند هذا الحدّ وسيصبح العرض قابلاً للاستخدام في TalkBack. ولكن من المفيد أن يشير العرض إلى أنّه يمكن تفعيله ("انقر مرّتين للتفعيل")، وأن يوضّح أيضًا ما سيحدث عند تفعيل العرض ("انقر مرّتين للتغيير" أو "انقر مرّتين لإعادة الضبط").

لإجراء ذلك، يمكنك إضافة معلومات حول إجراء العرض (هنا، إجراء النقر أو اللمس) إلى عنصر معلومات عقدة إمكانية الوصول، وذلك من خلال مفوّض إمكانية الوصول. يتيح لك عنصر التحكّم في تسهيل الاستخدام تخصيص الميزات ذات الصلة بتسهيل الاستخدام في تطبيقك من خلال التركيب (بدلاً من الوراثة).

لهذه المهمة، ستستخدم فئات تسهيل الاستخدام في مكتبات Android Jetpack‏ (androidx.*) لضمان التوافق مع الإصدارات القديمة.

  1. في DialView.kt، ضمن الحظر init، اضبط مفوّض إمكانية الوصول على العرض ككائن AccessibilityDelegateCompat جديد. استورِد androidx.core.view.ViewCompat وandroidx.core.view.AccessibilityDelegateCompat عند الطلب. تتيح هذه الاستراتيجية أكبر قدر من التوافق مع الإصدارات القديمة في تطبيقك.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   
})
  1. داخل العنصر AccessibilityDelegateCompat، استبدِل الدالة onInitializeAccessibilityNodeInfo() بالعنصر AccessibilityNodeInfoCompat، واستدعِ طريقة العنصر الأصل. استورِد androidx.core.view.accessibility.AccessibilityNodeInfoCompat عندما يُطلب منك ذلك.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)

   }  
})

يحتوي كل عرض على شجرة من عُقد تسهيل الاستخدام، وقد تتطابق أو لا تتطابق مع مكوّنات التنسيق الفعلية للعرض. تتصفّح خدمات تسهيل الاستخدام في Android هذه العُقد للعثور على معلومات حول طريقة العرض (مثل أوصاف المحتوى القابلة للنطق أو الإجراءات المحتملة التي يمكن تنفيذها في طريقة العرض هذه). عند إنشاء عرض مخصّص، قد تحتاج أيضًا إلى تجاهل معلومات العقدة لتوفير معلومات مخصّصة لتسهيل الاستخدام. في هذه الحالة، ستتجاوز معلومات العقدة للإشارة إلى أنّ هناك معلومات مخصّصة لإجراء العرض.

  1. داخل onInitializeAccessibilityNodeInfo()، أنشِئ عنصر AccessibilityNodeInfoCompat.AccessibilityActionCompat جديدًا، وعيّنه للمتغير customClick. مرِّر الثابت AccessibilityNodeInfo.ACTION_CLICK وسلسلة عنصر نائب إلى الدالة الإنشائية. انقر على AccessibilityNodeInfo استيراد عند الطلب.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        "placeholder"
      )
   }  
})

يمثّل صف AccessibilityActionCompat إجراءً في طريقة العرض لأغراض تسهيل الاستخدام. الإجراء النموذجي هو النقر أو اللمس، كما هو الحال هنا، ولكن يمكن أن تشمل الإجراءات الأخرى اكتساب التركيز أو فقدانه، أو عملية الحافظة (القص/النسخ/اللصق) أو التمرير داخل العرض. يتطلّب الدالة الإنشائية لهذه الفئة ثابت إجراء (AccessibilityNodeInfo.ACTION_CLICK هنا)، وسلسلة تستخدمها TalkBack للإشارة إلى ماهية الإجراء.

  1. استبدِل السلسلة "placeholder" بطلب إلى context.getString() لاسترداد مورد سلسلة. بالنسبة إلى المورد المحدّد، اختبِر سرعة المروحة الحالية. إذا كانت السرعة الحالية FanSpeed.HIGH، تكون السلسلة "Reset". إذا كانت سرعة المروحة أي شيء آخر، ستكون السلسلة "Change." ستنشئ موارد السلسلة هذه في خطوة لاحقة.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
      super.onInitializeAccessibilityNodeInfo(host, info)
      val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
         AccessibilityNodeInfo.ACTION_CLICK,
        context.getString(if (fanSpeed !=  FanSpeed.HIGH) R.string.change else R.string.reset)
      )
   }  
})
  1. بعد أقواس الإغلاق لتعريف customClick، استخدِم طريقة addAction() لإضافة إجراء تسهيل الاستخدام الجديد إلى عنصر معلومات العقدة.
ViewCompat.setAccessibilityDelegate(this, object : AccessibilityDelegateCompat() {
   override fun onInitializeAccessibilityNodeInfo(host: View, 
                            info: AccessibilityNodeInfoCompat) {
       super.onInitializeAccessibilityNodeInfo(host, info)
       val customClick = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
           AccessibilityNodeInfo.ACTION_CLICK,
           context.getString(if (fanSpeed !=  FanSpeed.HIGH) 
                                 R.string.change else R.string.reset)
       )
       info.addAction(customClick)
   }
})
  1. في res/values/strings.xml، أضِف موارد السلسلة الخاصة بـ "تغيير" و "إعادة الضبط".
<string name="change">Change</string>
<string name="reset">Reset</string>
  1. جمِّع التطبيق وشغِّله وتأكَّد من تفعيل TalkBack. لاحظ الآن أنّ العبارة "النقر مرّتين للتفعيل" أصبحت إما "النقر مرّتين للتغيير" (إذا كانت سرعة المروحة أقل من عالية أو 3) أو "النقر مرّتين لإعادة الضبط" (إذا كانت سرعة المروحة عالية أو 3). يُرجى العِلم أنّ الطلب "انقر مرّتين لتنفيذ..." مقدَّم من خدمة TalkBack نفسها.

نزِّل الرمز البرمجي للدرس التطبيقي حول الترميز المكتمل.

$  git clone https://github.com/googlecodelabs/android-kotlin-drawing-custom-views


يمكنك بدلاً من ذلك تنزيل المستودع كملف Zip وفك ضغطه وفتحه في Android Studio.

تنزيل ملف Zip

  • لإنشاء طريقة عرض مخصّصة ترث شكل وسلوك فئة فرعية من View مثل EditText، أضِف فئة جديدة توسّع هذه الفئة الفرعية، وأجرِ تعديلات عن طريق تجاوز بعض طرق الفئة الفرعية.
  • لإنشاء عرض مخصّص بأي حجم وشكل، أضِف فئة جديدة توسّع View.
  • يمكنك إلغاء طرق View، مثل onDraw()، لتحديد شكل العرض ومظهره الأساسي.
  • استخدِم invalidate() لفرض رسم أو إعادة رسم طريقة العرض.
  • لتحسين الأداء، خصِّص المتغيرات وعيِّن أي قيم مطلوبة للرسم والتلوين قبل استخدامها في onDraw()، مثل تهيئة متغيرات الأعضاء.
  • يمكنك إلغاء performClick() بدلاً من OnClickListener() للعرض المخصّص من أجل توفير السلوك التفاعلي للعرض. يتيح ذلك لك أو لمطوّري تطبيقات Android الآخرين الذين قد يستخدمون فئة العرض المخصّص استخدام onClickListener() لتوفير سلوك إضافي.
  • أضِف العرض المخصّص إلى ملف تنسيق XML مع سمات لتحديد مظهره، كما تفعل مع عناصر واجهة المستخدم الأخرى.
  • أنشئ ملف attrs.xml في المجلد values لتحديد السمات المخصّصة. يمكنك بعد ذلك استخدام السمات المخصّصة لطريقة العرض المخصّصة في ملف تنسيق XML.

دورة Udacity التدريبية:

مستندات مطوّري تطبيقات Android:

فيديوهات:

يسرد هذا القسم مهامًا منزلية محتملة للطلاب الذين يعملون على هذا الدرس التطبيقي العملي كجزء من دورة تدريبية يقودها مدرّب. على المعلّم تنفيذ ما يلي:

  • حدِّد واجبًا منزليًا إذا لزم الأمر.
  • توضيح كيفية إرسال الواجبات المنزلية للطلاب
  • وضع درجات للواجبات المنزلية

يمكن للمدرّبين استخدام هذه الاقتراحات بالقدر الذي يريدونه، ويجب ألا يترددوا في تكليف الطلاب بأي واجبات منزلية أخرى يرونها مناسبة.

إذا كنت تعمل على هذا الدرس العملي بنفسك، يمكنك استخدام مهام الواجب المنزلي هذه لاختبار معلوماتك.

السؤال 1

لتحديد المواضع والأبعاد وأي قيم أخرى عند تحديد حجم العرض المخصّص لأول مرة، ما هي الطريقة التي يجب إلغاؤها؟

onMeasure()

onSizeChanged()

invalidate()

onDraw()

السؤال 2

للإشارة إلى أنّك تريد إعادة رسم طريقة العرض باستخدام onDraw()، ما هي الطريقة التي تستدعيها من سلسلة محادثات واجهة المستخدم، بعد تغيير قيمة السمة؟

▢ onMeasure()

▢ onSizeChanged()

▢ invalidate()

▢ getVisibility()

السؤال 3

ما هي طريقة View التي يجب إلغاءها لإضافة تفاعلية إلى العرض المخصّص؟

▢ setOnClickListener()

▢ onSizeChanged()

▢ isClickable()

▢ performClick()

للحصول على روابط تؤدي إلى دروس برمجية أخرى في هذه الدورة التدريبية، يمكنك الانتقال إلى الصفحة المقصودة للدروس البرمجية المتقدّمة حول Android بلغة Kotlin.