onMeasure() هي طريقة محمية (protected) من فئة android.view.View تستدعيها نظام Android لتحديد أبعاد العرض. يمرر النظام كائنين MeasureSpec إلى الطريقة، كل منهما يحتوي على وضع قياس (EXACTLY أو AT_MOST أو UNSPECIFIED) وحجم يقترحه الحاوية الأم. وفقًا لوثائق مطوري Android (2026)، فإن إعادة تعريف onMeasure مع معالجة صحيحة لـ MeasureSpec هي خطوة إلزامية لجميع العروض المخصصة ومجموعات العروض التي تتطلب تحكمًا دقيقًا في الأحجام.
النقاط الرئيسية
onMeasure(int widthMeasureSpec, int heightMeasureSpec) هي طريقة من فئة View يستدعيها نظام Android لتحديد عرض وارتفاع العرض. يقوم المطور بإعادة تعريف هذه الطريقة لتحديد الحجم الذي يجب أن يكون عليه العرض بناءً على القيود المرسلة في MeasureSpec. بدون إعادة تعريف صحيحة لـ onMeasure، قد يظهر العرض المخصص بشكل غير صحيح أو لا يظهر على الإطلاق.
يستدعي النظام onMeasure خلال مرحلة القياس من دورة حياة العرض، والتي تسبق مرحلتي التخطيط (onLayout) والرسم (onDraw). إذا لم يعيد العرض تعريف onMeasure، يتم استخدام التنفيذ من الفئة الفائقة، والذي يحدد الأحجام الافتراضية بناءً على drawable الخلفية أو layout_params. استدعاء super.onMeasure(widthMeasureSpec, heightMeasureSpec) يعمل فقط مع الفئات الفرعية القياسية للعرض مثل TextView أو ImageView.
أحد المتطلبات الأساسية لـ onMeasure هو أن استدعاء setMeasuredDimension(int, int) يجب أن يكون موجودًا في نهاية الطريقة. إذا كان هذا الاستدعاء مفقودًا، يقوم النظام بإلقاء IllegalStateException تشير إلى أن العرض لم يثبت الأبعاد المقاسة. تصبح الأبعاد النهائية متاحة من خلال getMeasuredWidth() و getMeasuredHeight() بعد اكتمال مرحلة القياس.
MeasureSpec هو عدد صحيح 32 بت حيث ترمز البتتان العلويتان إلى وضع القياس وترمز البتات الثلاثون السفلية إلى الحجم. يحدد الوضع مدى حرية العرض في اختيار حجمه الخاص. يوفر Android ثلاثة أوضاع: EXACTLY و AT_MOST و UNSPECIFIED. يملي كل وضع منطق معالجة مختلفًا في onMeasure.
| وضع MeasureSpec | القيمة | السلوك |
|---|---|---|
| EXACTLY | حدد الأصل حجمًا دقيقًا | يجب أن يتطابق العرض تمامًا مع الحجم المعطى إذا كان لا يريد تجاوز الحدود |
| AT_MOST | حدد الأصل حجمًا أقصى | يمكن للعرض اختيار أي حجم من 0 إلى الحد الأقصى المعطى |
| UNSPECIFIED | لا يفرض الأصل أي قيود | يمكن للعرض اختيار أي حجم مرغوب دون حد أعلى |
لاستخراج الوضع والحجم من MeasureSpec، تُستخدم الطرق الثابتة لفئة MeasureSpec: MeasureSpec.getMode(int) يعيد أحد الأوضاع الثلاثة (EXACTLY, AT_MOST, UNSPECIFIED)، و MeasureSpec.getSize(int) يعيد الحجم الرقمي بالبكسلات. لإنشاء MeasureSpec مخصص، يُستخدم MeasureSpec.makeMeasureSpec(int size, int mode). تغطي هذه الطرق الثلاث جميع سيناريوهات العمل مع الأحجام في onMeasure.
النمط القياسي لمعالجة MeasureSpec: إذا كان الوضع EXACTLY، استخدم الحجم المعطى كحجم نهائي؛ إذا كان AT_MOST، اختر الحد الأدنى بين الحجم المرغوب (محتوى العرض) والحد الأقصى المعطى؛ إذا كان UNSPECIFIED، استخدم الحجم المرغوب للعرض دون قيود. يضمن هذا النمط سلوكًا صحيحًا تحت أي قيود من الأصل.
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
val desiredWidth = 200
val desiredHeight = 100
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize
MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
else -> desiredWidth
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
else -> desiredHeight
}
setMeasuredDimension(width, height)
}
لتبسيط المنطق القياسي، يوفر Android طريقة resolveSizeAndState التي تأخذ الحجم المرغوب و MeasureSpec وتعيد الحجم النهائي مع الوضع الصحيح. تنفذ هذه الطريقة النمط الموصوف أعلاه في سطر واحد من الكود. تتوفر أيضًا الدالة resolveSize(int size, int measureSpec) التي تعيد حجمًا نقيًا دون بتات الحالة.
Android يستخدم خوارزمية قياس ثنائية المسار تضمن أن كل عرض في التسلسل الهرمي يحصل على أبعاد صحيحة مع مراعاة قيود الأصل وتفضيلات العناصر الفرعية. في المسار الأول، يمرر الأصل MeasureSpec مع القيود إلى العروض الفرعية، وتحسب العروض الفرعية أحجامها المرغوبة. في المسار الثاني، يتخذ الأصل القرار النهائي بشأن الأحجام.
بالنسبة لـ ViewGroup، تكون عملية القياس أكثر تعقيدًا: يجب على الأصل أولاً قياس جميع أطفاله، ثم تحديد حجمه الخاص بناءً على أحجامهم. استدعاء measureChildren(int widthMeasureSpec, int heightMeasureSpec) يتكرر عبر جميع العروض الفرعية ويستدعي measure(child, childWidthSpec, childHeightSpec) لكل منها. بعد قياس جميع العناصر الفرعية، تستدعي ViewGroup setMeasuredDimension بأبعادها الخاصة.
فارق دقيق مهم: طريقة measure (عامة، نهائية) لا يمكن إعادة تعريفها — بدلاً من ذلك يتم إعادة تعريف onMeasure. يضمن هذا أن النظام يمكنه تنفيذ مهام الصيانة قبل وبعد onMeasure، مثل التحقق من تغييرات الحجم وحساب المنطقة المتسخة للرسم اللاحق. إذا كان للعرض أبعاد ثابتة، فقد لا تكون إعادة تعريف onMeasure ضرورية.
MeasureSpec لا يشمل فقط الحجم والوضع ولكن أيضًا بتات الحالة، التي يمكن الوصول إليها عبر MeasureSpec.getMode(). بعد استدعاء setMeasuredDimension، تصبح الحالة جزءًا من الأبعاد المقاسة للعرض ويمكن التحقق منها عبر getMeasuredState(). يُستخدم هذا في ScrollView والحاويات الأخرى القابلة للتمرير لتمرير القيود بشكل صحيح إلى العناصر الفرعية.
دعنا نفحص مثالًا عمليًا لإنشاء عرض مخصص مع إعادة تعريف onMeasure للعرض المربع. الفئة SquareView تمد View وتضمن أن العرض والارتفاع متساويان دائمًا، بغض النظر عن MeasureSpec المرسل. في onMeasure، يتم تحديد الجانب الأدنى وتعيين الحجم المربع.
class SquareView(context: Context)
: View(context) {
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
val widthSize =
MeasureSpec.getSize(widthMeasureSpec)
val heightSize =
MeasureSpec.getSize(heightMeasureSpec)
val size = minOf(widthSize, heightSize)
setMeasuredDimension(size, size)
}
}
ViewGroup تتطلب منطق onMeasure أكثر تعقيدًا لأنه يجب قياس العناصر الفرعية أولاً، ثم تحديد حجم ViewGroup نفسها. مثال CascadeLayout يوزع العناصر الفرعية بشكل متتالي مع إزاحة. بعد قياس جميع العناصر الفرعية عبر measureChildWithMargins، يتم حساب العرض والارتفاع الإجماليين.
class CascadeLayout(context: Context)
: ViewGroup(context) {
private val cascadeOffset = 40
override fun onMeasure(widthMeasureSpec: Int,
heightMeasureSpec: Int) {
var maxWidth = 0
var totalHeight = 0
for (i in 0 until childCount) {
val child = getChildAt(i)
measureChildWithMargins(child,
widthMeasureSpec,
cascadeOffset * i,
heightMeasureSpec, 0)
maxWidth = maxOf(maxWidth,
child.measuredWidth +
cascadeOffset * i)
totalHeight += child.measuredHeight
}
setMeasuredDimension(
resolveSize(maxWidth, widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec))
}
override fun generateLayoutParams(attrs: AttributeSet?)
: LayoutParams = MarginLayoutParams(context, attrs)
override fun onLayout(changed: Boolean,
l: Int, t: Int,
r: Int, b: Int) {
var top = t
for (i in 0 until childCount) {
val child = getChildAt(i)
val left = l + cascadeOffset * i
child.layout(left, top,
left + child.measuredWidth,
top + child.measuredHeight)
top += child.measuredHeight
}
}
}
فقدان استدعاء setMeasuredDimension هو الخطأ الأكثر شيوعًا. إذا قام المطور بإعادة تعريف onMeasure ولكن لم يستدع setMeasuredDimension، يتعطل التطبيق مع IllegalStateException. يحدث هذا بشكل خاص عندما تحتوي الطريقة على فروع شرطية ويفتقد أحد الفروع الاستدعاء. يجب أن ينتهي كل فرع كود في onMeasure باستدعاء setMeasuredDimension.
تجاهل وضع AT_MOST هو ثاني أكثر الأخطاء شيوعًا. إذا كان العرض في وضع AT_MOST يستخدم دائمًا الحجم المرسل بدلاً من الحساب بناءً على المحتوى، فلا يمكن للحاوية الأم توزيع المساحة بشكل صحيح. على سبيل المثال، TextView في AT_MOST يجب أن يحسب عرض النص ويستخدم الحد الأدنى بين العرض المرغوب والمرسل. تجاهل AT_MOST يؤدي إلى أن يشغل العرض كل المساحة المتاحة حتى مع محتوى صغير.
إنشاء كائنات داخل onMeasure هو خطأ أداء كلاسيكي. نظرًا لأنه يمكن استدعاء onMeasure عدة مرات (عند كل طلب تخطيط)، فإن إنشاء كائنات (Paint, Rect, String) داخل هذه الطريقة يلوث الذاكرة ويؤدي إلى جمع القمامة. يجب إنشاء جميع الكائنات مرة واحدة في مُنشئ العرض، ويجب أن يتم فقط منطق حساب الأحجام في onMeasure. تنطبق نفس القاعدة على onDraw و onLayout.
measureChildWithMargins هي طريقة محمية لـ ViewGroup تقيس عرضًا فرعيًا واحدًا مع مراعاة MarginLayoutParams الخاصة به. تقبل الطريقة MeasureSpec للأصل والإزاحات المتراكمة للعرض والارتفاع. تقوم تلقائيًا بضبط MeasureSpec للعرض الفرعي عن طريق طرح paddings الأصل و margins الطفل، ثم تمرر MeasureSpec المضبوط إلى child.measure().
لـ منطق قياس متقدم، يمكن لـ ViewGroup إعادة تعريف measureChild(View child, int parentWidthSpec, int parentHeightSpec) أو العمل مباشرة مع MeasureSpec لكل طفل. على سبيل المثال، LinearLayout في onMeasure يتكرر عبر جميع العروض الفرعية، ويقيس كل منها مع مراعاة layout_weight الخاص به ويوزع المساحة المتبقية بشكل متناسب. يسمح هذا النهج بتنفيذ خوارزميات تخطيط عشوائية.
التخزين المؤقت لنتائج القياس عبر آلية ذاكرة التخزين المؤقت للقياس متاح من خلال علامة setMeasureWithLargestChildEnabled في ViewGroups معينة. ومع ذلك، في معظم الحالات يتم استدعاء onMeasure مرة أخرى عند أي تغيير في التخطيط، ولا يتم تطبيق التخزين المؤقت. في ViewGroups المخصصة، يوصى بتقليل الحسابات في onMeasure بدلاً من الاعتماد على التخزين المؤقت.
الأسئلة الشائعة
نعم، إذا كان العرض المخصص يرث مباشرة من فئة View. إذا كان يرث من TextView أو ImageView أو Button بأحجامها القياسية، يمكن ترك onMeasure دون تغيير. بالنسبة لـ ViewGroup، إعادة تعريف onMeasure مطلوبة دائمًا — وإلا فلن يتم قياس العناصر الفرعية بشكل صحيح.
يقوم نظام Android بإلقاء IllegalStateException مع الرسالة “The View did not call setMeasuredDimension”. يحدث هذا الاستثناء في طريقة measure() بعد اكتمال onMeasure، إذا بقيت الأبعاد النهائية صفرًا. يتسبب الاستثناء في تعطل التطبيق إذا لم يتم معالجته عبر try-catch.
getMeasuredWidth() يعيد الحجم المحدد في onMeasure (مرحلة القياس). getWidth() يعيد الحجم الفعلي الذي استلمه العرض في onLayout بعد جميع تعديلات الموضع. بالنسبة لمعظم العروض، تتطابق هذه القيم، لكن في ViewGroups المخصصة قد تختلف.
لا، onMeasure مخصص حصريًا لحساب الأحجام. تغيير الحالة أو بدء الرسوم المتحركة أو العمل مع الشبكة أو تحديث البيانات في هذه الطريقة ينتهك بنية Android وقد يؤدي إلى استدعاءات متكررة لـ measure، حيث أن تغييرات الحالة يمكن أن تؤدي إلى requestLayout.
ConstraintLayout يدير قياس العناصر الفرعية بشكل مستقل بناءً على القيود المحددة. إذا قام عرض مخصص داخل ConstraintLayout بإعادة تعريف onMeasure، فيجب عليه معالجة MeasureSpec المرسل من ConstraintLayout بشكل صحيح، وإلا فقد لا تعمل القيود. يستخدم ConstraintLayout خوارزمية ثنائية المسار مع WidgetContainer الخاص به للحساب.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.