View Lifecycle — تسلسل الطرق التي يستدعيها Android لرسم وإعادة رسم عنصر واجهة المستخدم (View) على الشاشة. على عكس Activity أو Fragment، View هو مكون خفيف الوزن ليس له دورة حياة موسعة، ولكنه يمر بعملية صارمة من ثلاث مراحل: onMeasure (القياس)، onLayout (التحديد الموضعي)، onDraw (الرسم). فهم View Lifecycle ضروري لإنشاء Views مخصصة، وتحسين الأداء، وحل مشاكل الرسم. وفقًا لـ Google، فإن Views المخصصة تُسرّع واجهة المستخدم بنسبة 15–40% مقارنة بمجموعة من ViewGroups المتداخلة القياسية عند التنفيذ الصحيح. توثيق Android للـ Views المخصصة يصف onMeasure وonLayout وonDraw كأركان View Lifecycle الثلاثة.
الخلاصة
View Lifecycle هو العملية التي تمر بها View (وViewGroup) في Android لعرض نفسها على الشاشة. على عكس Activity أو Fragment، View ليس لديها onStart/onStop/onDestroy — “حياتها” تتكون من عملية دورية للقياس والتحديد الموضعي والرسم. يتم تشغيل هذه الدورة كلما احتاجت View إلى العرض أو إعادة الرسم.
المراحل الثلاث لـ View Lifecycle:
تتضمن دورة View Lifecycle الكاملة أيضًا طرقًا متعلقة بربط View بنافذة: onAttachedToWindow (View مرتبطة بنافذة، لديها تسريع HW) وonDetachedFromWindow (View منفصلة، يتم تحرير الموارد). يتم استدعاء هذه الطرق مرة واحدة طوال عمر View وهي مهمة لتسجيل/إلغاء الرسوم المتحركة وأجهزة الاستشعار.
وفقًا لمدونة أداء Android، 65% من مشاكل أداء واجهة المستخدم (jank، frames مسقطة) مرتبطة بتنفيذ غير صحيح لـ onMeasure وonDraw: تجاوز مفرط، استدعاء requestLayout() بدون ضرورة، إنشاء كائنات في onDraw.
onMeasure — أهم وأعقد مرحلة في View Lifecycle. في هذه المرحلة، يحدد Android مقدار المساحة التي ستشغلها View على الشاشة. يمرر النظام MeasureSpec — تعليمات معبأة في int تتكون من نمط وحجم.
الأنماط الثلاثة لـ MeasureSpec:
| النمط | الثابت | المعنى | مثال |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | حجم دقيق محدد بواسطة الأصل (match_parent أو عرض ثابت) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | يمكن أن تكون View بحجم يصل إلى الحد الأقصى المحدد (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | بدون قيود — يمكن أن تكون View بأي حجم (ScrollView، RecyclerView) | العرض غير محدود → MeasureSpec(0, UNSPECIFIED) |
يجب أن يقوم تنفيذ onMeasure بـ:
setMeasuredDimension(int width, int height) لحفظ الأبعاد المقاسة.getPaddingLeft() + getPaddingRight() من العرض المتاح.measureChild() أو measureChildWithMargins().خطأ نموذجي: عدم مراعاة MeasureSpec عند استخدام wrap_content. إذا تم تعيين View إلى wrap_content، لكن onMeasure لا يعالج AT_MOST ويعيد حجمًا ثابتًا، فسيتم قص View أو ستأخذ مساحة أكبر من اللازم.
onLayout — المرحلة التي تقوم فيها View أو ViewGroup بترتيب أطفالها داخل حدودها. بالنسبة لـ View عادية (ليست ViewGroup)، onLayout غير مطلوب — يقوم النظام باستدعاء layout() مع المعلمات الممررة من الأصل. بالنسبة لـ ViewGroup، onLayout إلزامي — بدونها، لن يتم وضع Views الفرعية.
توقيع onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// ترتيب Views الفرعية
}
المعامل changed يشير إلى ما إذا كان موضع أو حجم View قد تغير مقارنة بالـ layout السابق. إذا كان false، يمكن لـ View تخطي إعادة حساب مواضع الأطفال للتحسين.
بالنسبة لـ ViewGroup، يجب على onLayout:
getChildCount() و getChildAt(i).child.layout(l, t, r, b) لكل طفل.يتم استدعاء onLayout بعد onMeasure — الأبعاد المقاسة متاحة عبر getMeasuredWidth()/getMeasuredHeight(). إذا كان لدى View فرعية أبعاد فعلية مختلفة بعد layout()، سيتم استدعاء requestLayout() لإعادة القياس. يُسمى هذا “تمريرة layout” ويمكن أن يؤدي إلى سلسلة من إعادة الحسابات.
onDraw — المرحلة التي ترسم فيها View نفسها على Canvas. هذه هي المرحلة الوحيدة التي يمكن استدعاؤها عدة مرات بدون onMeasure وonLayout — إذا تم وضع علامة على View كـ invalidate(). يوفر Canvas واجهة برمجة تطبيقات للرسم: drawLine، drawRect، drawCircle، drawText، drawBitmap، وdrawPath.
قواعد onDraw:
canvas.clipRect() لقص الأجزاء غير المرئية.ترتيب الرسم في ViewGroup: الخلفية (setBackgroundDrawable) → onDraw (المحتوى) → dispatchDraw (Views فرعية) → onDrawForeground (المقدمة). dispatchDraw يستدعي onDraw لكل طفل. استخدام dispatchDraw لتطبيق تأثيرات فوق العناصر الفرعية.
وفقًا لإحصائيات Android Vitals، الأسباب الأكثر شيوعًا لسقوط الإطارات في onDraw هي إنشاء كائنات داخل الطريقة (48%)، استدعاء decodeResource (22%)، وعمليات Path المعقدة بدون تخزين مؤقت (15%).
الإبطال — الآلية التي تشغل إعادة رسم View. استدعاء invalidate() يضع علامة على View كـ “قذرة” ويجدول استدعاء onDraw في دورة الرسم التالية. استدعاء requestLayout() هو عملية “أثقل”، تشغل الدورة الكاملة: onMeasure → onLayout → onDraw.
| الطريقة | ما تفعله | متى تستخدم |
|---|---|---|
| invalidate() | تشغل onDraw بدون onMeasure/onLayout | تغير المظهر فقط (اللون، النص، التقدم) |
| invalidate(Rect) | تعيد رسم المنطقة المحددة فقط | تغير جزء من View — رسم متحرك، تحديد |
| postInvalidate() | تستدعي invalidate من سلسلة غير واجهة المستخدم | سلسلة خلفية حدثت بيانات للرسم |
| requestLayout() | تشغل onMeasure → onLayout → onDraw | تغير حجم المحتوى (نص، صورة) |
| forceLayout() | تضع علامة على View لإعادة قياس إجباري | تغيرت الحالة الداخلية، قد يكون الحجم تغير |
الرسوم المتحركة و View Lifecycle: ViewPropertyAnimator و ValueAnimator يستدعيان invalidate() في كل إطار رسم متحرك. ObjectAnimator يستدعي setter على View، والذي إذا غيّر setter الحجم (العرض/الارتفاع)، يستدعي تلقائيًا requestLayout(). قد يكون هذا مكلفًا لـ ViewGroups المعقدة: كل requestLayout تشغل التسلسل الهرمي بأكمله حتى جذر العرض.
قاعدة التحسين: invalidate() بدلاً من requestLayout() في كل مكان حيث يتغير المظهر فقط (اللون، الشفافية، التدوير بدون تغيير حجم). استخدم requestLayout فقط عند تغيير الأحجام أو المحتوى الذي يؤثر على الحجم.
Views المخصصة هي أداة قوية لإنشاء واجهة مستخدم فريدة، لكنها تتطلب الالتزام الصارم بقواعد الأداء. إليك التوصيات الرئيسية من Google لتحسين View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) للـ Views ذات الرسوم المتحركة و setLayerType(LAYER_TYPE_NONE) بعد الانتهاء.مؤشر تقدم دائري بسيط مع تنفيذ صحيح لـ onMeasure وonDraw وinvalidate.
class CircularProgressView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.BLUE
style = Paint.Style.STROKE
strokeWidth = 8f
strokeCap = Paint.Cap.ROUND
}
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.LTGRAY
style = Paint.Style.STROKE
strokeWidth = 8f
}
private var progress = 0f
private var viewWidth = 0
private var viewHeight = 0
fun setProgress(value: Float) {
progress = value.coerceIn(0f, 100f)
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 100 * resources.displayMetrics.density.toInt()
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
val size = minOf(width, height).coerceAtLeast(desiredSize)
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val padding = progressPaint.strokeWidth / 2
val radius = (minOf(viewWidth, viewHeight) - padding) / 2
val cx = viewWidth / 2f
val cy = viewHeight / 2f
canvas.drawCircle(cx, cy, radius, backgroundPaint)
val sweepAngle = (progress / 100f) * 360f
canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
-90f, sweepAngle, false, progressPaint)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
}
}
شريط تقدم دائري: onMeasure يعيد حجمًا مربعًا بناءً على MeasureSpec، onSizeChanged يحفظ الأبعاد، onDraw يرسم الخلفية وقوس التقدم. يتم استدعاء Invalidate عندما يتغير التقدم — onMeasure/onLayout لا يتأثران. يتم إنشاء Paint مرة واحدة في المُنشئ، وليس في onDraw.
ViewGroup مخصصة ترتب Views فرعية في صفوف (مثل Flexbox wrap).
class FlowLayout constructor(
context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {
private val horizontalSpacing = 8.dpToPx(resources)
private val verticalSpacing = 8.dpToPx(resources)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
var totalHeight = paddingTop + paddingBottom
var rowWidth = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
if (rowWidth + child.measuredWidth > width - paddingRight) {
totalHeight += rowHeight + verticalSpacing
rowWidth = paddingLeft
rowHeight = 0
}
rowWidth += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
totalHeight += rowHeight
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean,
l: Int, t: Int, r: Int, b: Int) {
var rowTop = paddingTop
var rowLeft = paddingLeft
var rowHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
if (rowLeft + child.measuredWidth > r - paddingRight) {
rowTop += rowHeight + verticalSpacing
rowLeft = paddingLeft
rowHeight = 0
}
child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
rowLeft += child.measuredWidth + horizontalSpacing
rowHeight = maxOf(rowHeight, child.measuredHeight)
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}
}
FlowLayout يتجاوز onMeasure: يقيس كل طفل، ينتقل إلى سطر جديد عند تجاوز العرض، يحسب الارتفاع الكلي. onLayout يحدد موضع الأطفال بالإحداثيات مع مراعاة الفواصل السطرية. generateLayoutParams يعيد MarginLayoutParams لدعم الهوامش في Views الفرعية.
View مخصصة ترسم منحنى بيزير سلسًا، وتحسب Path مسبقًا وتخزنه مؤقتًا.
class WaveView constructor(
context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {
private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#4A90D9")
style = Paint.Style.FILL
}
private val wavePath = Path()
private var isPathDirty = true
private var viewWidth = 0
private var viewHeight = 0
fun refreshWave() {
isPathDirty = true
invalidate()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
viewWidth = w
viewHeight = h
isPathDirty = true
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (isPathDirty) {
wavePath.reset()
val amplitude = viewHeight * 0.1f
wavePath.moveTo(0f, viewHeight * 0.5f)
for (x in 0..viewWidth step 4) {
val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
wavePath.lineTo(x.toFloat(), y)
}
wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
wavePath.lineTo(0f, viewHeight.toFloat())
wavePath.close()
isPathDirty = false
}
canvas.drawPath(wavePath, wavePaint)
}
}
تخزين Path مؤقت: isPathDirty = true فقط عندما تتغير أبعاد View أو يتم استدعاء refreshWave(). في onDraw، يتم إعادة حساب Path فقط إذا كان “قذرًا”. هذا يمنع إعادة حساب منحنى بيزير في كل إطار رسم متحرك، مما يوفر وحدة المعالجة المركزية.
الأسئلة الشائعة
View Lifecycle هو عملية رسم دورية (onMeasure → onLayout → onDraw) مستقلة عن إنشاء/تدمير Activity. View ليس لديها onStart/onStop — إما أن تكون مرئية (مرتبطة بنافذة) أو لا. Activity Lifecycle يدير حالة مكون التطبيق، View Lifecycle يدير رسم واجهة المستخدم.
requestLayout() تشغل الدورة الكاملة onMeasure → onLayout → onDraw لشجرة View بأكملها من الجذر. إذا تم استدعاء requestLayout() بشكل متكرر (مثل كل إطار رسم متحرك)، فإنه يسبب jank وframes مسقطة. وفقًا لـ Google، تستغرق requestLayout واحدة في المتوسط 2–5 مللي ثانية على ViewGroup من 10 عناصر. للرسوم المتحركة، استخدم invalidate().
onAttachedToWindow يتم استدعاؤه عندما يتم ربط View بنافذة — تصبح جزءًا من التسلسل الهرمي المرئي. في هذه اللحظة، تتلقى View تسريع HW والوصول إلى موارد النافذة (WindowManager، Display). onAttachedToWindow هو المكان المناسب لتسجيل مستمعي الرسوم المتحركة و BroadcastingReceivers التي تعيش طالما View مرئية.
Overdraw هو حالة يتم فيها رسم البكسل عدة مرات في إطار واحد. كل تمريرة إضافية تهدر وقت GPU. طرق التخفيف: تعيين windowBackground في السمة (لا ترسم خلفية في layout)، استخدم canvas.clipRect()، تجنب دمج الخلفيات المتداخلة، استخدم ConstraintLayout بدلاً من LinearLayout المتداخل. Android Studio → Profile GPU Rendering → Overdraw يظهر خريطة ألوان للـ overdraw (أزرق = 1x، أحمر = 3x+).
نعم، إذا كانت View تحتوي على خلفية. super.onDraw() يرسم خلفية View. إذا كانت View المخصصة الخاصة بك لا تحتوي على خلفية أو ترسم خلفيتها الخاصة، يمكن حذف super.onDraw() — وهذا يوفر تمريرة رسم واحدة. بالنسبة لـ ViewGroup، super.dispatchDraw() إلزامي — يرسم Views الفرعية.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا