View Lifecycle — ما هو، عمليات onMeasure onLayout onDraw

المؤلف: IT Sectr نُشر: 2026-03-05 وقت القراءة: 10 دق

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 يتكون من ثلاث مراحل: onMeasure (الأحجام)، onLayout (المواقع)، onDraw (الرسم) — ويتم تشغيله بواسطة invalidate() أو requestLayout().
  • onMeasure يحسب عرض وارتفاع View بناءً على MeasureSpec (AT_MOST، EXACTLY، UNSPECIFIED).
  • onLayout يرتب Views الفرعية داخل ViewGroup، ويحدد إحداثياتها left، top، right، bottom.
  • onDraw يعرض محتوى View على Canvas: الخلفية، النص، الأشكال، الصور.
  • View Lifecycle غير الصحيح هو السبب الرئيسي لمشاكل أداء واجهة المستخدم (jank، frames مسقطة) ومشاكل التسلسل الهرمي.

View Lifecycle — ما هو في Android

View Lifecycle هو العملية التي تمر بها View (وViewGroup) في Android لعرض نفسها على الشاشة. على عكس Activity أو Fragment، View ليس لديها onStart/onStop/onDestroy — “حياتها” تتكون من عملية دورية للقياس والتحديد الموضعي والرسم. يتم تشغيل هذه الدورة كلما احتاجت View إلى العرض أو إعادة الرسم.

المراحل الثلاث لـ View Lifecycle:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — يحدد الأبعاد المطلوبة للـ View. يمرر النظام MeasureSpec — تعليمات حول الأبعاد المسموح بها (قيمة دقيقة، أقصى، أو بدون قيود).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — يحدد موضع View وأطفالها على الشاشة. بالنسبة لـ View، يحدد حدودها الخاصة؛ بالنسبة لـ ViewGroup، يحدد مواضع العناصر الفرعية.
  • onDraw(Canvas canvas) — يرسم محتوى View على Canvas المقدم. يوفر النظام Canvas يترجم الأوامر إلى bitmap أو GPU.

تتضمن دورة View Lifecycle الكاملة أيضًا طرقًا متعلقة بربط View بنافذة: onAttachedToWindow (View مرتبطة بنافذة، لديها تسريع HW) وonDetachedFromWindow (View منفصلة، يتم تحرير الموارد). يتم استدعاء هذه الطرق مرة واحدة طوال عمر View وهي مهمة لتسجيل/إلغاء الرسوم المتحركة وأجهزة الاستشعار.

وفقًا لمدونة أداء Android، 65% من مشاكل أداء واجهة المستخدم (jank، frames مسقطة) مرتبطة بتنفيذ غير صحيح لـ onMeasure وonDraw: تجاوز مفرط، استدعاء requestLayout() بدون ضرورة، إنشاء كائنات في onDraw.

onMeasure: قياس أبعاد View

onMeasure — أهم وأعقد مرحلة في View Lifecycle. في هذه المرحلة، يحدد Android مقدار المساحة التي ستشغلها View على الشاشة. يمرر النظام MeasureSpec — تعليمات معبأة في int تتكون من نمط وحجم.

الأنماط الثلاثة لـ MeasureSpec:

النمطالثابتالمعنىمثال
EXACTLYMeasureSpec.EXACTLYحجم دقيق محدد بواسطة الأصل (match_parent أو عرض ثابت)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTيمكن أن تكون View بحجم يصل إلى الحد الأقصى المحدد (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDبدون قيود — يمكن أن تكون View بأي حجم (ScrollView، RecyclerView)العرض غير محدود → MeasureSpec(0, UNSPECIFIED)

يجب أن يقوم تنفيذ onMeasure بـ:

  • استدعاء setMeasuredDimension(int width, int height) لحفظ الأبعاد المقاسة.
  • مراعاة الحشو — طرح getPaddingLeft() + getPaddingRight() من العرض المتاح.
  • بالنسبة لـ ViewGroup — قياس جميع الأطفال عبر measureChild() أو measureChildWithMargins().
  • بالنسبة لـ wrap_content — حساب الحجم بناءً على المحتوى (نص، صورة).
  • عدم استدعاء requestLayout() داخل onMeasure — سيؤدي ذلك إلى حلقة لا نهائية.

خطأ نموذجي: عدم مراعاة MeasureSpec عند استخدام wrap_content. إذا تم تعيين View إلى wrap_content، لكن onMeasure لا يعالج AT_MOST ويعيد حجمًا ثابتًا، فسيتم قص View أو ستأخذ مساحة أكبر من اللازم.

onLayout: وضع Views على الشاشة

onLayout — المرحلة التي تقوم فيها View أو ViewGroup بترتيب أطفالها داخل حدودها. بالنسبة لـ View عادية (ليست ViewGroup)، onLayout غير مطلوب — يقوم النظام باستدعاء layout() مع المعلمات الممررة من الأصل. بالنسبة لـ ViewGroup، onLayout إلزامي — بدونها، لن يتم وضع Views الفرعية.

توقيع onLayout:

java
@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).
  • لكل طفل، تحديد left، top، right، bottom — الإحداثيات داخل ViewGroup (مع مراعاة الحشو).
  • استدعاء child.layout(l, t, r, b) لكل طفل.
  • مراعاة الجاذبية، الهوامش، المحاذاة.

يتم استدعاء onLayout بعد onMeasure — الأبعاد المقاسة متاحة عبر getMeasuredWidth()/getMeasuredHeight(). إذا كان لدى View فرعية أبعاد فعلية مختلفة بعد layout()، سيتم استدعاء requestLayout() لإعادة القياس. يُسمى هذا “تمريرة layout” ويمكن أن يؤدي إلى سلسلة من إعادة الحسابات.

onDraw: رسم محتوى Canvas

onDraw — المرحلة التي ترسم فيها View نفسها على Canvas. هذه هي المرحلة الوحيدة التي يمكن استدعاؤها عدة مرات بدون onMeasure وonLayout — إذا تم وضع علامة على View كـ invalidate(). يوفر Canvas واجهة برمجة تطبيقات للرسم: drawLine، drawRect، drawCircle، drawText، drawBitmap، وdrawPath.

قواعد onDraw:

  • لا تنشئ كائنات في onDraw — كل استدعاء لـ onDraw يجب أن يستخدم كائنات منشأة مسبقًا (Path، Paint، Rect). إنشاء كائنات في onDraw يسبب توقف GC وframes مسقطة.
  • لا تستدعي requestLayout() أو invalidate() داخل onDraw — سيؤدي ذلك إلى حلقة لا نهائية من إعادة الرسم.
  • لا تقم بإجراء حسابات طويلة — يتم تنفيذ onDraw على سلسلة واجهة المستخدم. يجب نقل الحسابات المعقدة إلى سلسلة خلفية أو حسابها مسبقًا.
  • استخدم تسريع الأجهزة — منذ API 14+، يمكن لـ Canvas العمل عبر GPU. للرسومات المعقدة (التدرجات، الظلال، التدويرات)، يوفر تسريع HW تحسنًا في الأداء يصل إلى 300%.
  • ارسم فقط المنطقة المرئية — استخدم canvas.clipRect() لقص الأجزاء غير المرئية.

ترتيب الرسم في ViewGroup: الخلفية (setBackgroundDrawable) → onDraw (المحتوى) → dispatchDraw (Views فرعية) → onDrawForeground (المقدمة). dispatchDraw يستدعي onDraw لكل طفل. استخدام dispatchDraw لتطبيق تأثيرات فوق العناصر الفرعية.

وفقًا لإحصائيات Android Vitals، الأسباب الأكثر شيوعًا لسقوط الإطارات في onDraw هي إنشاء كائنات داخل الطريقة (48%)، استدعاء decodeResource (22%)، وعمليات Path المعقدة بدون تخزين مؤقت (15%).

الإبطال: متى يعاد رسم View

الإبطال — الآلية التي تشغل إعادة رسم 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 المخصصة: أفضل الممارسات

Views المخصصة هي أداة قوية لإنشاء واجهة مستخدم فريدة، لكنها تتطلب الالتزام الصارم بقواعد الأداء. إليك التوصيات الرئيسية من Google لتحسين View Lifecycle.

  • احسب مسبقًا كل ما يمكن حسابه — الأحجام، الإحداثيات، المسار، ألوان التدرج. في onDraw، قم فقط بالرسم.
  • خزّن نتائج القياس مؤقتًا — إذا كانت View ذات أبعاد ثابتة، احفظ MeasureSpec وأعد setMeasuredDimension بدون حسابات إضافية.
  • استخدم ViewConfiguration — getScaledTouchSlop، getScaledMinimumFlingVelocity — لمعالجة اللمس.
  • قلل عدد Views في التسلسل الهرمي — Views المخصصة التي تجمع عدة عناصر هي دائمًا أسرع من ViewGroup بها 3–5 Views متداخلة. توصي Google بعدم تجاوز 10 Views متداخلة في كل شاشة.
  • استخدم ConstraintLayout للتسلسل الهرمي المسطح — يبني ViewGroup واحدة بأداء قريب من RelativeLayout، ولكن بدون تداخل.
  • عطّل طبقة الجهاز بعد إعادة الرسم — استخدم setLayerType(LAYER_TYPE_HARDWARE) للـ Views ذات الرسوم المتحركة و setLayerType(LAYER_TYPE_NONE) بعد الانتهاء.
  • استخدم invalidate() مع Rect — أعد رسم المنطقة المتغيرة فقط، وليس View بأكملها.
  • تجنب overdraw — استخدم Profile GPU Rendering في Android Studio لتحديد عمليات إعادة الرسم غير الضرورية. متوسط overdraw لتطبيقات Google هو 1.5x، والحد الأقصى الموصى به هو 2.5x.

أمثلة كود View بلغة Kotlin

مثال 1: View مخصصة — مؤشر التقدم

مؤشر تقدم دائري بسيط مع تنفيذ صحيح لـ onMeasure وonDraw وinvalidate.

kotlin
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.

مثال 2: ViewGroup — FlowLayout بسيط

ViewGroup مخصصة ترتب Views فرعية في صفوف (مثل Flexbox wrap).

kotlin
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 الفرعية.

مثال 3: onDraw مع تخزين Path مؤقت

View مخصصة ترسم منحنى بيزير سلسًا، وتحسب Path مسبقًا وتخزنه مؤقتًا.

kotlin
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 عن Activity Lifecycle؟

View Lifecycle هو عملية رسم دورية (onMeasure → onLayout → onDraw) مستقلة عن إنشاء/تدمير Activity. View ليس لديها onStart/onStop — إما أن تكون مرئية (مرتبطة بنافذة) أو لا. Activity Lifecycle يدير حالة مكون التطبيق، View Lifecycle يدير رسم واجهة المستخدم.

ما تأثير requestLayout() على الأداء؟

requestLayout() تشغل الدورة الكاملة onMeasure → onLayout → onDraw لشجرة View بأكملها من الجذر. إذا تم استدعاء requestLayout() بشكل متكرر (مثل كل إطار رسم متحرك)، فإنه يسبب jank وframes مسقطة. وفقًا لـ Google، تستغرق requestLayout واحدة في المتوسط 2–5 مللي ثانية على ViewGroup من 10 عناصر. للرسوم المتحركة، استخدم invalidate().

متى يتم استدعاء onAttachedToWindow؟

onAttachedToWindow يتم استدعاؤه عندما يتم ربط View بنافذة — تصبح جزءًا من التسلسل الهرمي المرئي. في هذه اللحظة، تتلقى View تسريع HW والوصول إلى موارد النافذة (WindowManager، Display). onAttachedToWindow هو المكان المناسب لتسجيل مستمعي الرسوم المتحركة و BroadcastingReceivers التي تعيش طالما View مرئية.

ما هو overdraw وكيفية تقليله؟

Overdraw هو حالة يتم فيها رسم البكسل عدة مرات في إطار واحد. كل تمريرة إضافية تهدر وقت GPU. طرق التخفيف: تعيين windowBackground في السمة (لا ترسم خلفية في layout)، استخدم canvas.clipRect()، تجنب دمج الخلفيات المتداخلة، استخدم ConstraintLayout بدلاً من LinearLayout المتداخل. Android Studio → Profile GPU Rendering → Overdraw يظهر خريطة ألوان للـ overdraw (أزرق = 1x، أحمر = 3x+).

هل super.onDraw() مطلوب في View مخصصة؟

نعم، إذا كانت View تحتوي على خلفية. super.onDraw() يرسم خلفية View. إذا كانت View المخصصة الخاصة بك لا تحتوي على خلفية أو ترسم خلفيتها الخاصة، يمكن حذف super.onDraw() — وهذا يوفر تمريرة رسم واحدة. بالنسبة لـ ViewGroup، super.dispatchDraw() إلزامي — يرسم Views الفرعية.

الملخص

  • View Lifecycle — ثلاث مراحل رسم: onMeasure (الأبعاد)، onLayout (الموضع)، onDraw (العرض).
  • onMeasure يعالج MeasureSpec (EXACTLY، AT_MOST، UNSPECIFIED) ويستدعي setMeasuredDimension.
  • onLayout في ViewGroup يحدد موضع Views الفرعية بإحداثيات left/top/right/bottom.
  • onDraw يعرض المحتوى على Canvas — لا تنشئ كائنات داخل هذه الطريقة.
  • invalidate() يشغل only onDraw، requestLayout() يشغل الدورة الكاملة onMeasure → onLayout → onDraw.
  • Views المخصصة تُسرع واجهة المستخدم بنسبة 15–40%، ولكنها تتطلب تنفيذًا صحيحًا لـ onMeasure وتخزينًا مؤقتًا للكائنات في onDraw.
  • للرسومات المعقدة، استخدم تسريع الأجهزة وخزّن Path/Bitmap مؤقتًا.

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

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

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

اقرأ أيضًا