onLayout(): ما هو، خوارزمية الترتيب ومعاملات الطريقة

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

onLayout() هي طريقة من فئة ViewGroup تحدد مواضع وأحجام المشاهدات التابعة على المستوى الإحداثي للحاوية الأم. يستدعي نظام Android onLayout بعد مرحلة القياس (onMeasure)، عندما يكون العرض والارتفاع المقاسان معروفين بالفعل لكل مشاهدة تابعة. وفقًا لوثائق مطوري Android (2026)، فإن onLayout هي طريقة إلزامية للتجاوز في أي ViewGroup مخصصة، نظرًا لأن التطبيق القياسي لـ ViewGroup لا يقوم بتحديد الموضع التلقائي للأبناء.

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

  • onLayout(boolean changed, int l, int t, int r, int b) — طريقة ViewGroup التي تحدد مواضع العناصر التابعة في إحداثيات الأصل
  • child.layout(l, t, r, b) — استدعاء لكل مشاهدة تابعة، يحدد حدودها النهائية
  • مرحلة layout تتبع مرحلة measure وتسبق مرحلة draw في دورة حياة View
  • getWidth() و getHeight() تصبح متاحة فقط بعد تنفيذ onLayout، على عكس getMeasuredWidth المتاحة بعد onMeasure
  • requestLayout() — طريقة تبدأ استدعاءً متكررًا لـ onMeasure و onLayout عند تغير البيانات التي تؤثر على تحديد الموضع

ما هو onLayout()؟

onLayout(boolean changed, int l, int t, int r, int b) هي طريقة protected من فئة ViewGroup يستدعيها النظام لتحديد موضع المشاهدات التابعة داخل الحاوية الأم. يقوم المطور بتجاوز هذه الطريقة عند إنشاء ViewGroup مخصصة بترتيب غير قياسي للعناصر: متتالية، شبكية، متعرجة أو بإحداثيات عشوائية. تتلقى كل مشاهدة تابعة حدودها النهائية من خلال استدعاء child.layout().

تشير المعامل changed إلى ما إذا كان موضع أو حجم ViewGroup نفسها قد تغير منذ آخر layout. إذا كانت changed صحيحة، فجميع العناصر التابعة تحتاج على الأرجح إلى إعادة تحديد الموضع. المعاملات l, t, r, b هي إحداثيات الزاويتين العلوية اليسرى والسفلية اليمنى لـ ViewGroup في نظام إحداثيات الأصل. داخل onLayout، يستخدم المطور هذه القيم كإحداثيات بداية لترتيب الأبناء.

ViewGroup هي الفئة الوحيدة التي تتجاوز onLayout. المشاهدة العادية (وليست ViewGroup) ليس لها عناصر تابعة ولا تحتاج إلى onLayout — يتم التعامل مع تحديد موضعها بواسطة الحاوية الأم. حتى إذا تجاوزت مشاهدة عادية onLayout، لن يستدعيها النظام. هذا هو الاختلاف الأساسي عن onMeasure، الذي يتم استدعاؤه لأي مشاهدة.

تدفق مرحلة layout

تبدأ مرحلة layout باستدعاء الطريقة العامة layout(int l, int t, int r, int b) على المشاهدة الجذرية. تحدد هذه الطريقة الإحداثيات النهائية للمشاهدة نفسها وتستدعي onLayout إذا كانت المشاهدة هي ViewGroup. ثم يستدعي onLayout بشكل متكرر child.layout() لكل عنصر تابع، وتتكرر العملية لأسفل في التسلسل الهرمي. وبالتالي، ينتشر layout من الجذر إلى الأوراق.

قبل استدعاء onLayout، يتحقق النظام مما إذا كانت أبعاد المشاهدة قد تغيرت مقارنة بالدورة السابقة. إذا لم تتغير الأبعاد ولم يتم استدعاء requestLayout، فقد لا يتم استدعاء onLayout — يستخدم النظام نتائج layout السابقة. هذا هو تحسين يمنع إعادة الحساب غير الضرورية للمواضع أثناء الرسوم المتحركة أو التمرير، عندما يتغير المحتوى فقط وليس الأبعاد.

requestLayout() هي طريقة من View تخبر النظام أن layout المشاهدة قديم ويحتاج إلى إعادة حساب. يؤدي استدعاء requestLayout إلى دورة كاملة: أولاً يتم استدعاء onMeasure، ثم onLayout، ثم onDraw. على عكس invalidate، الذي يشغل فقط إعادة الرسم، يقوم requestLayout بتشغيل إعادة حساب كاملة للأبعاد والمواضع. الاستدعاءات المفرطة لـ requestLayout هي سبب شائع لمشاكل الأداء.

معاملات onLayout: l, t, r, b

l (left) — الإحداثي X للحافة اليسرى لـ ViewGroup في نظام إحداثيات الأصل. t (top) — الإحداثي Y للحافة العلوية. r (right) — الإحداثي X للحافة اليمنى. b (bottom) — الإحداثي Y للحافة السفلية. يتم حساب عرض ViewGroup كـ r - l، والارتفاع كـ b - t. تتضمن هذه الإحداثيات بالفعل جميع الحشو (padding) لـ ViewGroup نفسها.

داخل onLayout، يستدعي المطور child.layout(int childLeft, int childTop, int childRight, int childBottom) لكل مشاهدة تابعة. يجب أن تكون الإحداثيات الممررة إلى child.layout في نظام إحداثيات ViewGroup الأصل. عادةً childLeft و childTop يتم حسابهما مع مراعاة حشو الأصل: childLeft = l + paddingLeft + offsetX، childTop = t + paddingTop + offsetY.

المعاملالوصفالاستخدام النموذجي
l (left)إحداثي الحافة اليسرى لـ ViewGroup في الأصلنقطة البداية على المحور X للعناصر التابعة
t (top)إحداثي الحافة العلوية لـ ViewGroup في الأصلنقطة البداية على المحور Y للعناصر التابعة
r (right)إحداثي الحافة اليمنى لـ ViewGroup في الأصلالحد الأعلى للعرض، r - l = getWidth()
b (bottom)إحداثي الحافة السفلية لـ ViewGroup في الأصلالحد الأعلى للارتفاع، b - t = getHeight()

حساب الإحداثيات للعناصر التابعة

يتم حساب الإحداثيات التابعة باستخدام الصيغة: childLeft = l + paddingLeft + (marginLeft إذا وجد)، childRight = childLeft + child.getMeasuredWidth(). بالمثل للمحور الرأسي: childTop = t + paddingTop + (marginTop)، childBottom = childTop + child.getMeasuredHeight(). بعد حساب هذه القيم الأربعة، يتم استدعاء child.layout(childLeft, childTop, childRight, childBottom).

مثال ViewGroup مخصصة مع onLayout بلغة Kotlin

لنقم بإنشاء FlowLayout — ViewGroup مخصصة تقوم بترتيب المشاهدات التابعة في صفوف، وتنقل العناصر إلى سطر جديد عندما يمتلئ الصف الحالي. هذا مماثل لـ Flexbox مع wrap في مستوى واحد. يتكرر onLayout عبر جميع المشاهدات التابعة، ويحسب الموضع لكل منها، ويستدعي child.layout() بالحدود الصحيحة.

kotlin
class FlowLayout(context: Context)
    : ViewGroup(context) {

    private val horizontalSpacing = 12
    private val verticalSpacing = 8

    override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
        val parentWidth =
            MeasureSpec.getSize(widthMeasureSpec)
        var rowX = paddingLeft
        var rowY = paddingTop
        var maxRowHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child,
                widthMeasureSpec, 0,
                heightMeasureSpec, 0)

            if (rowX + child.measuredWidth >
                parentWidth - paddingRight) {
                rowX = paddingLeft
                rowY += maxRowHeight + verticalSpacing
                maxRowHeight = 0
            }

            rowX += child.measuredWidth +
                horizontalSpacing
            maxRowHeight = maxOf(maxRowHeight,
                child.measuredHeight)
        }

        val totalHeight = rowY + maxRowHeight +
            paddingBottom
        setMeasuredDimension(
            resolveSize(parentWidth, widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec))
    }

    override fun onLayout(changed: Boolean,
                       l: Int, t: Int,
                       r: Int, b: Int) {
        val parentWidth = r - l
        var rowX = paddingLeft
        var rowY = paddingTop
        var maxRowHeight = 0

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val cw = child.measuredWidth
            val ch = child.measuredHeight

            if (rowX + cw >
                parentWidth - paddingRight) {
                rowX = paddingLeft
                rowY += maxRowHeight + verticalSpacing
                maxRowHeight = 0
            }

            child.layout(rowX, rowY,
                rowX + cw, rowY + ch)
            rowX += cw + horizontalSpacing
            maxRowHeight =
                maxOf(maxRowHeight, ch)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?)
        : LayoutParams =
        MarginLayoutParams(context, attrs)
}

الاختلافات بين onLayout و onMeasure

onMeasure و onLayout هما مرحلتان متتاليتان من دورة حياة View تؤديان مهامًا مختلفة جوهريًا. يحدد onMeasure الأبعاد المطلوبة (المقاسة) لـ View، بينما يحدد onLayout الإحداثيات والأبعاد الفعلية (النهائية). الفرق الرئيسي: في onMeasure، قد تكون الأبعاد وسيطة ويتم تعديلها لاحقًا بواسطة الأصل، بينما في onLayout يتم تثبيت الموضع النهائي لكل مشاهدة تابعة.

يتم استدعاء onMeasure لكل View، بما في ذلك المشاهدات الطرفية (TextView, ImageView, Button). يتم استدعاء onLayout فقط لـ ViewGroup. هذا لأن تحديد الموضع هو مسؤولية الحاوية الأم، وليس المشاهدة نفسها. تستقبل المشاهدة الطرفية موضعها من خلال layout() المستدعى من onLayout للأصل.

getMeasuredWidth() و getMeasuredHeight() متاحان بعد onMeasure، بينما getWidth() و getHeight() متاحان فقط بعد onLayout. إذا قمت بالوصول إلى getWidth() داخل onMeasure، فسيعيد قيمة من الدورة السابقة أو صفرًا. لذلك، لحساب الأبعاد في onMeasure، يجب استخدام MeasureSpec والأبناء بشكل تسلسلي.

الأخطاء الشائعة عند العمل مع onLayout

تحديد الموضع دون مراعاة الحشو (padding) — الخطأ الأول عند تنفيذ onLayout. غالبًا ما ينسى المطور إضافة paddingLeft و paddingTop للأصل إلى الإحداثيات الأولية للمشاهدات التابعة. ونتيجة لذلك، تظهر الأبناء على حافة ViewGroup، متجاهلة الحشو المحدد عبر setPadding() أو في ترميز XML. الحساب الصحيح: childLeft = paddingLeft + offsetX.

استدعاء layout للأبناء غير المرئيين — المشكلة الثانية الشائعة. إذا كانت ViewGroup تحتوي على مشاهدات تابعة برؤية GONE، فلا داعي لتحديد موضعها — فهي لا تشغل مساحة. ومع ذلك، يجب على onLayout التعامل مع هذه الحالة بشكل صحيح، وتخطي الأبناء GONE. للأبناء INVISIBLE، لا يزال من الضروري استدعاء layout — يحتفظون بمساحتهم على الرغم من عدم عرضهم.

تجاهل المعامل changed — الخطأ الثالث. يشير المعامل changed إلى ما إذا كانت أبعاد أو موضع ViewGroup قد تغيرت. إذا كانت changed == false، يمكن استخدام الإحداثيات المخزنة مؤقتًا دون إعادة حساب layout لجميع العناصر التابعة. ومع ذلك، فإن التخزين المؤقت الكامل لـ layout مهمة معقدة، وفي معظم التطبيقات، يعيد onLayout ببساطة حساب جميع العناصر في كل مرة. هذا مقبول مع عدد صغير من الأبناء.

الأسئلة المتكررة

هل من الممكن عدم تجاوز onLayout في ViewGroup؟

نعم، من الممكن إذا كانت ViewGroup تستخدم LayoutParams القياسية ولا تضيف منطق تحديد موضع مخصص. ومع ذلك، فإن التطبيق القياسي لـ onLayout في ViewGroup لا يقوم بأي إجراءات — لن يتم تحديد موضع العناصر التابعة. عمليًا، جميع ViewGroup (LinearLayout, RelativeLayout, FrameLayout) تتجاوز onLayout.

ما الفرق بين layout و onLayout؟

layout() هي طريقة عامة نهائية من View، يتم استدعاؤها بواسطة النظام أو ViewGroup الأصل. تحدد إحداثيات المشاهدة نفسها وتستدعي onLayout إذا كانت المشاهدة هي ViewGroup. onLayout() هي طريقة محمية (protected) يتجاوزها المطور للترتيب المخصص للعناصر التابعة.

هل يمكن لـ onLayout استدعاء requestLayout؟

من الناحية الفنية — نعم، يمكن. لكن هذا غير موصى به بشدة، لأنه يؤدي إلى تكرار لا نهائي: requestLayout → onMeasure → onLayout → requestLayout. إذا تم استدعاء requestLayout داخل onLayout، سيرمي النظام استثناء StackOverflowError. يجب إجراء جميع تغييرات الأبعاد قبل onLayout.

كيف يعمل onLayout مع الرسوم المتحركة؟

رسوم متحركة للـ layout (LayoutTransition) تعترض التغييرات في مواضع المشاهدات التابعة وتطبق رسومًا متحركة انتقالية. عند تمكين LayoutTransition، يقوم onLayout أولاً بتعيين المواضع النهائية، ثم يقوم LayoutTransition بتحريك الحركة من الموضع القديم إلى الجديد. يتطلب هذا تطبيقًا صحيحًا لـ onLayout بإحداثيات نهائية مناسبة.

لماذا لا يتم استدعاء onLayout بعد invalidate؟

invalidate() يشغل فقط مرحلة draw (إعادة الرسم)، دون التأثير على measure و layout. لتشغيل onLayout، تحتاج إلى استدعاء requestLayout()، الذي يبدأ الدورة الكاملة: measure → layout → draw. invalidate أكثر كفاءة لتحديث المظهر عندما لا تتغير الأبعاد والمواضع.

الملخص

  • onLayout() — طريقة ViewGroup التي تحدد المواضع النهائية للمشاهدات التابعة بعد اكتمال مرحلة القياس
  • child.layout(l, t, r, b) — الآلية الأساسية لتحديد الإحداثيات لكل عنصر تابع
  • المعاملات l, t, r, b — إحداثيات حواف ViewGroup في نظام الأصل، العرض = r - l، الارتفاع = b - t
  • مرحلة layout تنتشر بشكل متكرر من المشاهدة الجذرية إلى العناصر التابعة، مستدعية onLayout في كل ViewGroup
  • requestLayout() يشغل دورة كاملة من إعادة حساب الأبعاد والمواضع، على عكس invalidate الذي يشغل فقط إعادة الرسم
  • مراعاة الحشو (padding) في onLayout إلزامي — يجب أن تتضمن الإحداثيات الأولية للأبناء paddingLeft و paddingTop للأصل

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

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

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

اقرأ أيضًا