Custom View هو مكون واجهة مستخدم مخصص في Android، يتم إنشاؤه عن طريق الوراثة من الفئة View أو فئاتها الفرعية (Button، TextView، ImageView) وتجاوز الطرق الرئيسية: onDraw للرسم، onMeasure لقياس الأحجام و onTouchEvent لمعالجة اللمسات. وفقًا لدليل مطوري Android من Google (2024)، يُستخدم Custom View عندما لا توفر مكونات Android SDK القياسية السلوك أو المظهر المطلوب — على سبيل المثال، الرسوم المتحركة المخصصة، الأشكال غير القياسية، الرسوم البيانية المتخصصة وعناصر الألعاب.
النقاط الرئيسية
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.
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 هو الطريقة التي يُبلغ فيها الـ View عن أبعاده المطلوبة بناءً على MeasureSpec المستلم (قيود الأب). بدون تنفيذ صحيح لـ onMeasure، قد يكون للـ View ارتفاع صفري مع wrap_content أو يشغل كامل مساحة الشاشة مع match_parent.
MeasureSpec يتكون من وضع (EXACTLY، AT_MOST، UNSPECIFIED) وقيمة. EXACTLY — حدد الأب حجمًا دقيقًا (match_parent أو قيمة ثابتة). AT_MOST — حدد الأب حدًا أقصى (wrap_content). UNSPECIFIED — بدون قيود (ScrollView، ListView).
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 هو قلب Custom View. هذه الطريقة تُستدعى من قبل Android في كل إعادة رسم للـ View. يُمرر إليها كائن Canvas، الذي يتم من خلاله تنفيذ كل الرسم: الأشكال الهندسية، النص، المسارات، Bitmap والرسوم المتحركة.
Canvas API يوفر طرقًا لجميع العمليات الأساسية: drawCircle، drawRect، drawLine، drawPath، drawText، drawBitmap، drawArc، drawOval. لتكوين النمط، يُستخدم Paint — كائن يحدد اللون، السُمك، نمط التعبئة، الخط، الظلال والتأثيرات.
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() لبدء إعادة الرسم.
السمات المخصصة تسمح بتكوين 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.
<!-- 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 لتحرير الموارد.
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 هو الطريقة المُستدعاة عند كل لمسة للـ View. يستقبل كائن MotionEvent بمعلومات عن نوع الحدث (ACTION_DOWN، ACTION_MOVE، ACTION_UP، ACTION_CANCEL)، الإحداثيات، الضغط وعدد الأصابع (اللمس المتعدد).
لمعالجة الإيماءات المعقدة (التمرير، الضغط المزدوج، الضغط الطويل)، استخدم GestureDetector أو ScaleGestureDetector مع onTouchEvent. يُبسط GestureDetector التعرف على onSingleTapUp، onFling، onLongPress، onDoubleTap والإيماءات القياسية الأخرى.
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 قد يصبح عنق الزجاجة في الأداء إذا تم استدعاء onDraw بشكل متكرر (رسوم متحركة، تمرير) أو احتوى على عمليات ثقيلة. يوفر Android عدة آليات لتحسين رسم المكونات المخصصة.
View.setLayerType يسمح بتبديل عرض الـ View إلى طبقة برمجية (LAYER_TYPE_SOFTWARE) أو طبقة عتادية (LAYER_TYPE_HARDWARE). الطبقة البرمجية مفيدة لرسومات Canvas المعقدة غير المدعومة بالتسريع العتادي — على سبيل المثال، drawTextOnPath أو تأثيرات Path المعقدة.
invalidate(Rect) يُعيد رسم فقط المنطقة المحددة من الـ View، وليس المكون بأكمله. هذا مهم بشكل حاسم لـ Custom Views الكبيرة (الرسومات، الخرائط، لوحات الرسم)، حيث إعادة الرسم الكاملة لكل إطار تسبب انخفاض FPS. استخدم postInvalidateOnAnimation() للمزامنة مع vsync.
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 (Canvas API) مبرر عندما تحتاج أقصى أداء للرسم المخصص (الرسومات، محررات الفيديو، الخرائط)، التكامل مع كود موجود قائم على View أو دعم إصدارات Android الأقل من API 21. Jetpack Compose هو نهج حديث لمعظم المشاريع الجديدة، يستخدم واجهة مستخدم تصريحية ومناسب أكثر للواجهات الديناميكية.
نعم، إذا كان الـ View يستخدم wrap_content في تخطيط XML. بدون onMeasure، سيعمل wrap_content مثل match_parent لأن التنفيذ الافتراضي لـ View.onMeasure لا يحدد حجمًا افتراضيًا. إذا كان الـ View دائمًا بحجم ثابت أو match_parent، يمكن ترك onMeasure دون تجاوز.
استخدم ValueAnimator أو ObjectAnimator لتغيير خصائص الـ View (اللون، نصف القطر، الموضع) واستدعِ invalidate() في رد الاتصال للرسوم المتحركة لإعادة الرسم. يعمل ValueAnimator على الخيط الرئيسي ومتزامن مع vsync. للرسوم المتحركة المعقدة (الفيزياء، الجسيمات)، استخدم Choreographer.FrameCallback أو Android Animation Framework.
في onTouchEvent، استخدم MotionEvent.getPointerCount() لتحديد عدد الأصابع و getPointerId(i) لتتبع كل إصبع بمعرف فريد. ACTION_POINTER_DOWN / ACTION_POINTER_UP — أحداث إضافة/إزالة الإصبع. للتكبير بالقرص، استخدم ScaleGestureDetector مع onTouchEvent — يُبسط اكتشاف القرص.
نعم، عبر AndroidView — دالة composable تُدرج عنصرًا قائمًا على View في تسلسل Compose. تقبل AndroidView مصنعًا لإنشاء الـ View ورد اتصال update لمزامنة الحالة. هذا يسمح بإعادة استخدام Custom Views الموجودة في مشاريع Compose الجديدة دون إعادة كتابة كاملة.
الخلاصة
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.