Custom View في Android — الجوهر، الرسم، ومعالجة الأحداث

المؤلف: IT Sectr نُشر: 2026-07-20 وقت القراءة: 9 دق

Custom View هو مكون واجهة مستخدم مخصص في Android، يتم إنشاؤه عن طريق الوراثة من الفئة View أو فئاتها الفرعية (Button، TextView، ImageView) وتجاوز الطرق الرئيسية: onDraw للرسم، onMeasure لقياس الأحجام و onTouchEvent لمعالجة اللمسات. وفقًا لدليل مطوري Android من Google (2024)، يُستخدم Custom View عندما لا توفر مكونات Android SDK القياسية السلوك أو المظهر المطلوب — على سبيل المثال، الرسوم المتحركة المخصصة، الأشكال غير القياسية، الرسوم البيانية المتخصصة وعناصر الألعاب.

النقاط الرئيسية

  • Custom View — يُنشأ بالوراثة من View أو فئاتها الفرعية مع تجاوز onDraw، onMeasure، onTouchEvent.
  • يتم الرسم عبر Canvas API — الأشكال الهندسية، النص، المسارات، Bitmap والرسوم المتحركة.
  • onMeasure يتعامل مع الأحجام الصحيحة للـ View مع مراعاة معاملات التخطيط والحشو.
  • تُعلن سمات XML المخصصة في res/values/attrs.xml وتُقرأ في مُنشئ الـ View.
  • معالجة اللمسات عبر onTouchEvent تتيح إضافة دعم الإيماءات، السحب والإفلات واللمس المتعدد.

ما هو Custom View في Android؟

Custom View هو فئة ترث من android.view.View (أو إحدى فئاته الفرعية) وتتجاوز طرق النظام لتنفيذ منطق مخصص للرسم والقياس ومعالجة الأحداث. Custom View هو آلية أساسية في Android لإنشاء مكونات واجهة مستخدم فريدة.

يوفر Android طريقتين لإنشاء Custom View: الوراثة من View (رسم مخصص بالكامل عبر onDraw) والوراثة من فئة فرعية موجودة لـ View (على سبيل المثال، تجاوز Button أو TextView لإضافة وظائف مع الحفاظ على السلوك الأساسي).

المُنشئ الأساسي لـ Custom View يجب أن يقبل Context وAttributeSet — وهذا يسمح للنظام بإنشاء الـ View من ترميز XML. إذا كان الـ View سيُستخدم فقط من الكود، يكفي مُنشئ مع Context. المُنشئ الثالث مع النمط ضروري لدعم سمات Android.

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

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        strokeWidth = 4f
        style = Paint.Style.FILL
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(width / 2f,
            height / 2f,
            50f, paint)
    }
}

Custom View الأدنى يتكون من مُنشئ و onDraw. ومع ذلك، للعمل بشكل صحيح في نظام التخطيط في Android، يجب أيضًا تجاوز onMeasure — وإلا فقد يظهر الـ View بأحجام صفرية أو يستجيب بشكل غير صحيح لمعاملات التخطيط wrap_content.

وفقًا Google I/O 2023، تُستخدم Custom Views في 65% من التطبيقات في أعلى 100 على Google Play. الأسباب الأكثر شيوعًا لإنشاء Custom View: الرسوم المتحركة المخصصة (35%)، الرسوم البيانية والمخططات المخصصة (25%)، عناصر التحكم المتخصصة (20%) والمكونات ذات العلامة التجارية (20%).

onMeasure و onLayout: إدارة الأحجام

onMeasure هو الطريقة التي يُبلغ فيها الـ View عن أبعاده المطلوبة بناءً على MeasureSpec المستلم (قيود الأب). بدون تنفيذ صحيح لـ onMeasure، قد يكون للـ View ارتفاع صفري مع wrap_content أو يشغل كامل مساحة الشاشة مع match_parent.

MeasureSpec يتكون من وضع (EXACTLY، AT_MOST، UNSPECIFIED) وقيمة. EXACTLY — حدد الأب حجمًا دقيقًا (match_parent أو قيمة ثابتة). AT_MOST — حدد الأب حدًا أقصى (wrap_content). UNSPECIFIED — بدون قيود (ScrollView، ListView).

kotlin
override fun onMeasure(
    widthMeasureSpec: Int,
    heightMeasureSpec: Int
) {
    val desiredWidth = paddingLeft + paddingRight
        + DEFAULT_WIDTH
    val desiredHeight = paddingTop + paddingBottom
        + DEFAULT_HEIGHT

    val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
    val modeWidth = MeasureSpec.getMode(widthMeasureSpec)

    val resultWidth = when (modeWidth) {
        MeasureSpec.EXACTLY -> measuredWidth
        MeasureSpec.AT_MOST ->
            desiredWidth.coerceAtMost(measuredWidth)
        else -> desiredWidth
    }

    val resultHeight = resolveSize(desiredHeight,
        heightMeasureSpec)

    setMeasuredDimension(resultWidth, resultHeight)
}

resolveSize هو طريقة مساعدة في Android تبسط تنفيذ onMeasure. تأخذ الحجم المطلوب و MeasureSpec وتعيد القيمة الصحيحة: لـ EXACTLY — الحجم الدقيق، لـ AT_MOST — الحد الأدنى بين المطلوب والحد الأقصى، لـ UNSPECIFIED — الحجم المطلوب.

onLayout لـ View (وليس ViewGroup) عادة لا يُتجاوز — يُستدعى من قبل الأب لتحديد موضع العناصر الفرعية. بالنسبة لـ ViewGroup، onLayout إلزامي — يجب استدعاء layout() لكل View فرعي.

onDraw و Canvas API: الرسم

onDraw هو قلب Custom View. هذه الطريقة تُستدعى من قبل Android في كل إعادة رسم للـ View. يُمرر إليها كائن Canvas، الذي يتم من خلاله تنفيذ كل الرسم: الأشكال الهندسية، النص، المسارات، Bitmap والرسوم المتحركة.

Canvas API يوفر طرقًا لجميع العمليات الأساسية: drawCircle، drawRect، drawLine، drawPath، drawText، drawBitmap، drawArc، drawOval. لتكوين النمط، يُستخدم Paint — كائن يحدد اللون، السُمك، نمط التعبئة، الخط، الظلال والتأثيرات.

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

    // خلفية
    canvas.drawRect(0f, 0f,
        width.toFloat(), height.toFloat(), bgPaint)

    // نص
    canvas.drawText("Custom View",
        paddingLeft.toFloat(),
        paddingTop.toFloat() + textPaint.textSize,
        textPaint)

    // دائرة
    canvas.drawCircle(
        width / 2f,
        height / 2f,
        radius, circlePaint)

    // مسار (منحنى بيزير)
    val path = Path().apply {
        moveTo(0f, height.toFloat())
        quadTo(width / 2f, 0f,
            width.toFloat(), height.toFloat())
    }
    canvas.drawPath(path, pathPaint)
}

Paint هو مُكوّن نمط الرسم. منع التعرج (Paint.ANTI_ALIAS_FLAG) إلزامي للحواف الناعمة. النص تحت البكسل (SUBPIXEL_TEXT_FLAG) يحسن جودة النص. Style.FILL، STROKE و FILL_AND_STROKE تحدد ما إذا كان الشكل سيُملأ، يُحدد بخطوط أو كلاهما.

مهم: لا تنشئ كائنات Paint، Path، Rect في onDraw — هذا يؤدي إلى تخصيصات في حلقة الرسم ويسبب جمع القمامة، مما يقلل FPS. أنشئ جميع كائنات الرسم في المُنشئ أو في طريقة init. للرسوم المتحركة، استخدم ValueAnimator أو ObjectAnimator مع invalidate() لبدء إعادة الرسم.

سمات XML المخصصة في attrs.xml

السمات المخصصة تسمح بتكوين Custom View من ترميز XML تمامًا مثل android:layout_width أو android:background القياسية. تُعلن السمات في ملف res/values/attrs.xml مع الاسم والنوع والقيمة الافتراضية الاختيارية.

أنواع السمات: string، integer، float، boolean، color، dimension، enum، flag، fraction، reference (مرجع مورد). لكل نوع، يقوم Android تلقائيًا بتحليل القيمة من XML وتمريرها إلى TypedArray.

xml
<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CustomView">
        <attr name="circleColor"
            format="color" />
        <attr name="circleRadius"
            format="dimension" />
        <attr name="labelText"
            format="string" />
        <attr name="showAnimation"
            format="منطقي" />
    </declare-styleable>
</resources>

في مُنشئ Custom View، تُقرأ السمات عبر context.obtainStyledAttributes، الذي يُعيد TypedArray. يوفر TypedArray طرق وصول محددة النوع: getColor، getDimension، getString، getBoolean، getInt. بعد القراءة، يجب استدعاء recycle() على TypedArray لتحرير الموارد.

kotlin
init {
    val typedArray = context.obtainStyledAttributes(
        attrs,
        R.styleable.CustomView
    )

    circleColor = typedArray.getColor(
        R.styleable.CustomView_circleColor,
        Color.BLUE
    )
    circleRadius = typedArray.getDimension(
        R.styleable.CustomView_circleRadius,
        50f
    )
    labelText = typedArray.getString(
        R.styleable.CustomView_labelText
    ) ?: "Default"

    typedArray.recycle()
}

الاستخدام في XML: أضف مساحة الاسم app (xmlns:app="http://schemas.android.com/apk/res-auto") واستخدم السمات المخصصة مثل app:circleColor="@color/red". سيقوم Android Studio بالإكمال التلقائي والتحقق من أنواع السمات إذا كان التصريح في attrs.xml صحيحًا.

معالجة اللمسات باستخدام onTouchEvent

onTouchEvent هو الطريقة المُستدعاة عند كل لمسة للـ View. يستقبل كائن MotionEvent بمعلومات عن نوع الحدث (ACTION_DOWN، ACTION_MOVE، ACTION_UP، ACTION_CANCEL)، الإحداثيات، الضغط وعدد الأصابع (اللمس المتعدد).

لمعالجة الإيماءات المعقدة (التمرير، الضغط المزدوج، الضغط الطويل)، استخدم GestureDetector أو ScaleGestureDetector مع onTouchEvent. يُبسط GestureDetector التعرف على onSingleTapUp، onFling، onLongPress، onDoubleTap والإيماءات القياسية الأخرى.

kotlin
private val gestureDetector = GestureDetector(
    context, object : GestureDetector.SimpleOnGestureListener() {
    override fun onSingleTapUp(e: MotionEvent): Boolean {
        handleTap(e.x, e.y)
        return true
    }
})

override fun onTouchEvent(event: MotionEvent): Boolean {
    val handled = gestureDetector.onTouchEvent(event)

    when (event.action) {
        MotionEvent.ACTION_MOVE -> {
            currentX = event.x
            currentY = event.y
            invalidate()
            return true
        }
    }

    return handled || super.onTouchEvent(event)
}

اختبار الاصطدام — تحديد ما إذا كانت اللمسة قد أصابت منطقة معينة من الـ View. للمناطق المستطيلة، استخدم Rect.contains(x, y). للدوائر — التحقق من المسافة من المركز: sqrt(dx^2 + dy^2) < radius. للأشكال العشوائية — Path.op() أو Region.contains().

اللمس المتعدد يُعالج عبر MotionEvent.getPointerCount() و getPointerId(index). كل إصبع يحصل على معرف فريد يُحفظ من ACTION_DOWN إلى ACTION_POINTER_UP. ACTION_MOVE قد يحتوي على بيانات لجميع الأصابع النشطة — استخدم getHistoricalX/Y لاستيفاء الحركة.

تحسين أداء Custom View

Custom View قد يصبح عنق الزجاجة في الأداء إذا تم استدعاء onDraw بشكل متكرر (رسوم متحركة، تمرير) أو احتوى على عمليات ثقيلة. يوفر Android عدة آليات لتحسين رسم المكونات المخصصة.

نوع الطبقة للرسم المعقد

View.setLayerType يسمح بتبديل عرض الـ View إلى طبقة برمجية (LAYER_TYPE_SOFTWARE) أو طبقة عتادية (LAYER_TYPE_HARDWARE). الطبقة البرمجية مفيدة لرسومات Canvas المعقدة غير المدعومة بالتسريع العتادي — على سبيل المثال، drawTextOnPath أو تأثيرات Path المعقدة.

إبطال فقط المنطقة المطلوبة

invalidate(Rect) يُعيد رسم فقط المنطقة المحددة من الـ View، وليس المكون بأكمله. هذا مهم بشكل حاسم لـ Custom Views الكبيرة (الرسومات، الخرائط، لوحات الرسم)، حيث إعادة الرسم الكاملة لكل إطار تسبب انخفاض FPS. استخدم postInvalidateOnAnimation() للمزامنة مع vsync.

kotlin
class EfficientCustomView
    @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : View(context, attrs) {

    // كائنات تُنشأ مرة واحدة في المُنشئ
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val rect = Rect()
    private val position = PointF()

    fun updatePosition(x: Float, y: Float) {
        position.set(x, y)
        // إعادة رسم فقط المنطقة حول النقطة
        rect.set(
            (x - 10).toInt(),
            (y - 10).toInt(),
            (x + 10).toInt(),
            (y + 10).toInt()
        )
        invalidate(rect)
    }
}

التسريع العتادي مفعل افتراضيًا على Android 3.0+ (API 11+). عمليات Canvas drawCircle، drawRect، drawBitmap مسرعة عتاديًا وتُنفذ على GPU. ومع ذلك، drawTextOnPath، drawVertices، عمليات القص المعقدة غير مسرعة — استخدم LAYER_TYPE_SOFTWARE لها.

وفقًا Android Performance Patterns (Google, 2023)، الأسباب الرئيسية لبطء Custom View — التخصيصات في onDraw (إنشاء كائنات في كل إطار)، إعادة رسم الـ View بأكمله عند تغيير عنصر واحد، وغياب LayerType.HARDWARE للمحتوى الثابت. إصلاح هذه المشكلات الثلاث يعطي زيادة في FPS من 30 إلى 60 في معظم السيناريوهات.

الطريقةالغرضتكرار الاستدعاء
onDrawرسم محتوى الـ Viewعند كل invalidate()
onMeasureتحديد أحجام الـ Viewعند تغيير التخطيط
onLayoutتحديد موضع الـ Views الفرعيةبعد onMeasure
onTouchEventمعالجة اللمساتعند كل لمسة
onSizeChangedرد فعل على تغيير الحجمعند أول تخطيط وتغيير

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

متى استخدام Custom View بدلاً من Compose؟

Custom View (Canvas API) مبرر عندما تحتاج أقصى أداء للرسم المخصص (الرسومات، محررات الفيديو، الخرائط)، التكامل مع كود موجود قائم على View أو دعم إصدارات Android الأقل من API 21. Jetpack Compose هو نهج حديث لمعظم المشاريع الجديدة، يستخدم واجهة مستخدم تصريحية ومناسب أكثر للواجهات الديناميكية.

هل من الضروري تجاوز onMeasure؟

نعم، إذا كان الـ View يستخدم wrap_content في تخطيط XML. بدون onMeasure، سيعمل wrap_content مثل match_parent لأن التنفيذ الافتراضي لـ View.onMeasure لا يحدد حجمًا افتراضيًا. إذا كان الـ View دائمًا بحجم ثابت أو match_parent، يمكن ترك onMeasure دون تجاوز.

كيفية تحريك Custom View؟

استخدم ValueAnimator أو ObjectAnimator لتغيير خصائص الـ View (اللون، نصف القطر، الموضع) واستدعِ invalidate() في رد الاتصال للرسوم المتحركة لإعادة الرسم. يعمل ValueAnimator على الخيط الرئيسي ومتزامن مع vsync. للرسوم المتحركة المعقدة (الفيزياء، الجسيمات)، استخدم Choreographer.FrameCallback أو Android Animation Framework.

كيفية معالجة اللمس المتعدد في Custom View؟

في onTouchEvent، استخدم MotionEvent.getPointerCount() لتحديد عدد الأصابع و getPointerId(i) لتتبع كل إصبع بمعرف فريد. ACTION_POINTER_DOWN / ACTION_POINTER_UP — أحداث إضافة/إزالة الإصبع. للتكبير بالقرص، استخدم ScaleGestureDetector مع onTouchEvent — يُبسط اكتشاف القرص.

هل يمكن استخدام Custom View في Jetpack Compose؟

نعم، عبر AndroidView — دالة composable تُدرج عنصرًا قائمًا على View في تسلسل Compose. تقبل AndroidView مصنعًا لإنشاء الـ View ورد اتصال update لمزامنة الحالة. هذا يسمح بإعادة استخدام Custom Views الموجودة في مشاريع Compose الجديدة دون إعادة كتابة كاملة.

الخلاصة

  • Custom View — مكون Android مخصص يُنشأ بالوراثة من View مع تجاوز onDraw و onMeasure و onTouchEvent للرسم والمعالجة المخصصين.
  • onDraw و Canvas API — أساس الرسم: الأشكال الهندسية، النص، Bitmap، المسارات — مُكونة عبر Paint للون والنمط والخط.
  • onMeasure مهم للسلوك الصحيح لـ wrap_content؛ يستخدم MeasureSpec (EXACTLY، AT_MOST، UNSPECIFIED) و resolveSize لحساب الأبعاد.
  • السمات المخصصة عبر attrs.xml و TypedArray تسمح بتكوين Custom View من ترميز XML مع دعم الإكمال التلقائي في Android Studio.
  • onTouchEvent مع GestureDetector يوفر معالجة اللمسات والإيماءات واللمس المتعدد، بينما يحدد اختبار الاصطدام منطقة التفاعل.
  • الأداء: أنشئ الكائنات مرة واحدة في المُنشئ، استخدم invalidate(Rect) لإعادة الرسم الجزئي و LayerType للتأثيرات المعقدة.
  • Custom View يتكامل مع Jetpack Compose عبر AndroidView، مما يسمح بإعادة استخدام المكونات الجاهزة في مشاريع Compose الجديدة.

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا