0Pricing
Android Academy · درس

واجهات مخصّصة

ارسموا مكوّنات واجهة مستخدم مخصّصة من الصفر. وسّعوا View، وطبّقوا onMeasure وonDraw باستخدام Canvas وPaint، وعرّفوا سمات XML مخصّصة، وابنوا واجهات مركّبة.

واجهات مخصّصة درس مجاني في Android Academy على CoddyKit. هذا هو الدرس 3 من أصل 5. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Android Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Android Academy 5 دروس في المجموع.

لماذا العروض المخصصة؟

أحيانًا لا تنفذ عروض Android المدمجة ما تحتاج إليه تمامًا. تتيح لك العروض المخصصة ما يلي:

  • رسم أشكال أو رسوم بيانية أو حلقات تقدم أو مخططات غير موجودة في SDK
  • إنشاء مكونات واجهة مستخدم تحمل هوية علامتك التجارية وتُستخدم باتساق في أنحاء التطبيق
  • دمج عدة عروض في مكوّن واحد قابل لإعادة الاستخدام (Compound View)
  • تحقيق أداء أفضل عبر إزالة التسلسلات الهرمية المتداخلة للتخطيطات

توسيع View

أنشئ عرضًا مخصصًا عبر توسيع View (أو فئة فرعية مثل ImageView). تجاوز الباني المطلوب:

class CircleProgressView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    var progress: Float = 0f
        set(value) {
            field = value.coerceIn(0f, 100f)
            invalidate()   // trigger redraw
        }

    // onMeasure and onDraw go here
}

onMeasure()

تُخبر onMeasure() العنصر الأب بالحجم الذي يرغب هذا العرض في أن يكون عليه. استدعِ setMeasuredDimension() دائمًا في النهاية:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val desiredSize = 200  // dp → pixels
    val px = (desiredSize * resources.displayMetrics.density).toInt()

    val width  = resolveSize(px, widthMeasureSpec)
    val height = resolveSize(px, heightMeasureSpec)

    // Square: same width and height
    val size = minOf(width, height)
    setMeasuredDimension(size, size)
}

Paint — أداة الرسم

يحتفظ كائن Paint بمعلومات النمط اللازمة للرسم، مثل اللون وعرض الخط ومنع التعرج. أنشئه مرة واحدة — ولا تنشئه أبدًا داخل onDraw():

class CircleProgressView ... : View(...) {

    // Create Paint in init, not in onDraw!
    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.LTGRAY
        style = Paint.Style.STROKE
        strokeWidth = 20f
        strokeCap = Paint.Cap.ROUND
    }

    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#6750A4")
        style = Paint.Style.STROKE
        strokeWidth = 20f
        strokeCap = Paint.Cap.ROUND
    }
}

onDraw() — رسم العرض

يُستدعى onDraw(canvas) في كل مرة يحتاج فيها العرض إلى إعادة الرسم. استخدم Canvas لرسم الأشكال والنصوص والصور النقطية:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    val cx = width / 2f
    val cy = height / 2f
    val radius = (minOf(width, height) / 2f) - 30f

    // Draw background circle
    canvas.drawCircle(cx, cy, radius, backgroundPaint)

    // Draw progress arc
    val oval = RectF(cx - radius, cy - radius, cx + radius, cy + radius)
    val sweepAngle = 360f * (progress / 100f)
    canvas.drawArc(oval, -90f, sweepAngle, false, progressPaint)
}

invalidate() في مقابل requestLayout()

طريقتان لتفعيل تحديث:

  • invalidate() — تعيد رسم العرض (وتستدعي onDraw). استخدمها عندما يتغير المحتوى بينما يبقى الحجم نفسه.
  • requestLayout() — تعيد قياس العرض ورسمه (وتستدعي onMeasure ثم onDraw). استخدمها عندما يتغير حجم العرض.

السمات المخصصة (attrs.xml)

صرّح بسمات XML مخصصة حتى يمكن تهيئة العرض من ملفات التخطيط:

<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CircleProgressView">
        <attr name="progressColor" format="color" />
        <attr name="trackColor"    format="color" />
        <attr name="strokeWidth"   format="dimension" />
        <attr name="progress"      format="float" />
    </declare-styleable>
</resources>

قراءة السمات المخصصة

اقرأ attrs في الباني باستخدام obtainStyledAttributes:

init {
    context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView).use { ta ->
        progressPaint.color = ta.getColor(
            R.styleable.CircleProgressView_progressColor,
            Color.parseColor("#6750A4")
        )
        backgroundPaint.color = ta.getColor(
            R.styleable.CircleProgressView_trackColor,
            Color.LTGRAY
        )
        val sw = ta.getDimension(R.styleable.CircleProgressView_strokeWidth, 20f)
        progressPaint.strokeWidth = sw
        backgroundPaint.strokeWidth = sw
        progress = ta.getFloat(R.styleable.CircleProgressView_progress, 0f)
    }
}

استخدام العرض المخصص في XML

استخدم العرض المخصص في XML الخاص بالتخطيط مع المسار الكامل للفئة والسمات المخصصة:

<!-- In any layout XML -->
<com.example.myapp.CircleProgressView
    android:id="@+id/progressView"
    android:layout_width="120dp"
    android:layout_height="120dp"
    app:progress="65"
    app:progressColor="@color/primary"
    app:trackColor="@color/surface_variant"
    app:strokeWidth="12dp" />

<!-- Control in code: -->
binding.progressView.progress = 75f

العروض المركبة

يجمع العرض المركب عروضًا موجودة في مكوّن واحد قابل لإعادة الاستخدام. وسّع فئة تخطيط مثل LinearLayout:

class SearchBarView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : LinearLayout(context, attrs) {

    private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this)

    init {
        orientation = HORIZONTAL
        binding.ivSearch.setOnClickListener {
            onSearchClick?.invoke(binding.etSearch.text.toString())
        }
    }

    var onSearchClick: ((String) -> Unit)? = null

    fun setHint(hint: String) { binding.etSearch.hint = hint }
}

رسم النص

ارسم النص على Canvas باستخدام drawText(). وحاذِه في المنتصف بدقة باستخدام Paint.getFontMetrics():

private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.BLACK
    textSize = 48f
    textAlign = Paint.Align.CENTER
}

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val cx = width / 2f
    val cy = height / 2f

    // Vertically center the text:
    val fm = textPaint.fontMetrics
    val textY = cy - (fm.ascent + fm.descent) / 2f
    canvas.drawText("${progress.toInt()}%", cx, textY, textPaint)
}

تحقق سريع

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

مراجعة: العروض المخصصة

يمكنك الآن رسم أي شيء على الشاشة:

  • وسّع View باستخدام @JvmOverloads constructor
  • onMeasure() — أبلغ عن الحجم المطلوب باستخدام setMeasuredDimension()
  • Paint — يحدد النمط؛ أنشئه مرة واحدة، وليس داخل onDraw()
  • onDraw(canvas) — ارسم الدوائر والأقواس والنصوص والصور النقطية
  • استخدم invalidate() لإعادة الرسم وrequestLayout() لإعادة القياس
  • السمات المخصصة عبر attrs.xml لتهيئة XML
  • العرض المركب — وسّع تخطيطًا لدمج عروض موجودة

التالي: أضف الحيوية إلى واجهتك باستخدام الرسوم المتحركة والانتقالات.

الأسئلة الشائعة

هل درس «واجهات مخصّصة» مجاني؟

نعم — نص درس «واجهات مخصّصة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Android Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Android Academy 5 دروس في المجموع.

ماذا ستتعلم في «واجهات مخصّصة»؟

ارسموا مكوّنات واجهة مستخدم مخصّصة من الصفر. وسّعوا View، وطبّقوا onMeasure وonDraw باستخدام Canvas وPaint، وعرّفوا سمات XML مخصّصة، وابنوا واجهات مركّبة. تتمرن على Android Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Android Academy؟

لا تُشترط خبرة سابقة. Android Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 3 من أصل 5.

كم من الوقت يستغرق درس «واجهات مخصّصة»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Android Academy هذا؟

نعم. كل درس في Android Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. أساسيات Material Design
  2. Themes وStyles
  3. واجهات مخصّصة
  4. الرسوم المتحركة والانتقالات
  5. التنقّل السفلي وعلامات التبويب
← العودة إلى Android Academy