onLayout() هي طريقة من فئة ViewGroup تحدد مواضع وأحجام المشاهدات التابعة على المستوى الإحداثي للحاوية الأم. يستدعي نظام Android onLayout بعد مرحلة القياس (onMeasure)، عندما يكون العرض والارتفاع المقاسان معروفين بالفعل لكل مشاهدة تابعة. وفقًا لوثائق مطوري Android (2026)، فإن onLayout هي طريقة إلزامية للتجاوز في أي ViewGroup مخصصة، نظرًا لأن التطبيق القياسي لـ ViewGroup لا يقوم بتحديد الموضع التلقائي للأبناء.
النقاط الرئيسية
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(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 هي سبب شائع لمشاكل الأداء.
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).
لنقم بإنشاء FlowLayout — ViewGroup مخصصة تقوم بترتيب المشاهدات التابعة في صفوف، وتنقل العناصر إلى سطر جديد عندما يمتلئ الصف الحالي. هذا مماثل لـ Flexbox مع wrap في مستوى واحد. يتكرر onLayout عبر جميع المشاهدات التابعة، ويحسب الموضع لكل منها، ويستدعي child.layout() بالحدود الصحيحة.
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)
}
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 والأبناء بشكل تسلسلي.
تحديد الموضع دون مراعاة الحشو (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 ببساطة حساب جميع العناصر في كل مرة. هذا مقبول مع عدد صغير من الأبناء.
الأسئلة المتكررة
نعم، من الممكن إذا كانت ViewGroup تستخدم LayoutParams القياسية ولا تضيف منطق تحديد موضع مخصص. ومع ذلك، فإن التطبيق القياسي لـ onLayout في ViewGroup لا يقوم بأي إجراءات — لن يتم تحديد موضع العناصر التابعة. عمليًا، جميع ViewGroup (LinearLayout, RelativeLayout, FrameLayout) تتجاوز onLayout.
layout() هي طريقة عامة نهائية من View، يتم استدعاؤها بواسطة النظام أو ViewGroup الأصل. تحدد إحداثيات المشاهدة نفسها وتستدعي onLayout إذا كانت المشاهدة هي ViewGroup. onLayout() هي طريقة محمية (protected) يتجاوزها المطور للترتيب المخصص للعناصر التابعة.
من الناحية الفنية — نعم، يمكن. لكن هذا غير موصى به بشدة، لأنه يؤدي إلى تكرار لا نهائي: requestLayout → onMeasure → onLayout → requestLayout. إذا تم استدعاء requestLayout داخل onLayout، سيرمي النظام استثناء StackOverflowError. يجب إجراء جميع تغييرات الأبعاد قبل onLayout.
رسوم متحركة للـ layout (LayoutTransition) تعترض التغييرات في مواضع المشاهدات التابعة وتطبق رسومًا متحركة انتقالية. عند تمكين LayoutTransition، يقوم onLayout أولاً بتعيين المواضع النهائية، ثم يقوم LayoutTransition بتحريك الحركة من الموضع القديم إلى الجديد. يتطلب هذا تطبيقًا صحيحًا لـ onLayout بإحداثيات نهائية مناسبة.
invalidate() يشغل فقط مرحلة draw (إعادة الرسم)، دون التأثير على measure و layout. لتشغيل onLayout، تحتاج إلى استدعاء requestLayout()، الذي يبدأ الدورة الكاملة: measure → layout → draw. invalidate أكثر كفاءة لتحديث المظهر عندما لا تتغير الأبعاد والمواضع.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.