onLayout() — روشی از کلاس ViewGroup است که موقعیت و اندازه View های فرزند را در صفحه مختصات کانتینر والد تعیین میکند. سیستم Android onLayout را پس از مرحله اندازهگیری (onMeasure) فراخوان میکند، وقتی برای هر View فرزند عرض و ارتفاع اندازهگیری شده مشخص است. به استناد به Android Developers Documentation (2026)، onLayout یک روش اجباری برای بازنویسی در هر ViewGroup سفارشی است، زیرا پیادهسازی استاندارد ViewGroup موقعیتیابی خودکار فرزندان را انجام نمیدهد.
نکات کلیدی
onLayout(boolean changed, int l, int t, int r, int b) — یک روش protected از کلاس ViewGroup است که توسط سیستم برای موقعیتیابی View های فرزند درون کانتینر والد فراخوان میشود. برنامهنویس این روش را وقتی بازنویسی میکند که ViewGroup سفارشی با چیدمان غیراستاندارد اجاد میکند: به صورت کاسکد، شبکهای، شطرنجی یا مکان دلخواه. هر View فرزند مرزهای نهایی خود را از طریق فراخوانی child.layout() دریافت میکند.
پارامتر changed نشان میدهد که آیا موقعیت یا اندازه ViewGroup از آخرین layout تغییر کرده است. اگر changed برابر true باشد، احتمالاً همه المان فرزند نیز به تغییر موقعیت نیاز دارند. پارامترهای l، t، r، b مختصات گوشه بالای چپ و پایین راست ViewGroup در سیستم مختصات والدش هستند. درون onLayout برنامهنویس از این مقادیر به عنوان مختصات آغازینه برای چیدمان فرزندان استفاده میکند.
ViewGroup — تنها کلاسی است که onLayout را بازنویسی میکند. View عادی (غیر ViewGroup) المان فرزندی ندارد و به onLayout نیازی ندارد — موقعیتیابی خود توسط کانتینر والد مدیریت میشود. حتی اگر View عادی onLayout را بازنویسی کند، سیستم آن را فراخوان نخواهد کرد. این تفاوت بنیادین onLayout با onMeasure است که برای هر View فراخوان میشود.
مرحله layout با فراخوانی روش عمومی layout(int l, int t, int r, int b) در View ریشه آغاز میشود. این روش مختصات نهایی View را تعیین میکند و اگر View از نوع ViewGroup باشد، onLayout را فراخوان میکند. سپس onLayout بهصورت بازگشتی child.layout() را برای هر عضو فرزند فراخوان میکند و فرآیند در سلسلهمراتب به پایین ادامه مییابد. به این ترتیب، layout از ریشه به برگها منتشر میشود.
قبل از فراخوانی onLayout، سیستم بررسی میکند که آیا اندازههای View نسبت به چرخه قبلی تغییر کرده است. اگر اندازهها تغییر نکرده باشند و requestLayout فراخوان نشده باشد، onLayout فراخوان نمیشود — سیستم از نتایج layout قبلی استفاده میکند. این بهینهسازی است که از محاسبات غیرضروری موقعیت در انیمیشنها یا اسکرول که تنها محتوا تغییر میکند و اندازهها تغییر نمیکند، جلوگیری میکند.
requestLayout() — روشی از View است که به سیستم اطلاع میدهد layout View منقضی شده و نیاز به محاسبه مجدد دارد. فراخوانی 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، برنامهنویس برای هر View فرزند روش child.layout(int childLeft, int childTop, int childRight, int childBottom) را فراخوان میکند. مختصاتی که به child.layout ارسال میشوند باید در سیستم مختصات ViewGroup والد باشند. معمولاً childLeft و childTop با در نظر گرفتن padding های والد محاسبه میشوند: 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 if present)، childRight = childLeft + child.getMeasuredWidth(). به همین ترتیب برای عمودی: childTop = t + paddingTop + (marginTop)، childBottom = childTop + child.getMeasuredHeight(). پس از محاسبه این چهار مقدار، child.layout(childLeft, childTop, childRight, childBottom) فراخوان میشود.
یک FlowLayout ایجاد میکنیم — یک ViewGroup سفارشی که View های فرزند را در سطرها قرار میدهد، وقتی سطر فعلی پر شود، المانها را به سطر جدید منتقل میکند. این معادل Flexbox با wrap در یک صفحه است. onLayout تمام View های فرزند را پیمایش میکند، موقعیت هر کدام را محاسبه کرده و 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 اندازههای مورد نظر (measured) View را تعیین میکند، درحالی که onLayout مختصات و اندازههای واقعی (actual) را تعیین میکند. تفاوت کلیدی: در onMeasure اندازهها میتوانند موقتی باشند و بعداً توسط والد تصحیح شوند، اما در onLayout موقعیت نهایی هر View فرزند ثبت میشود.
onMeasure برای هر View، از جمله View های برگی (TextView، ImageView، Button) فراخوان میشود. onLayout تنها برای ViewGroup فراخوان میشود. دلیل این است که موقعیتیابی مسئولیت کانتینر والد است نه خود View. View برگی موقعیت خود را از طریق layout() که از onLayout والد فراخوان شده، دریافت میکند.
getMeasuredWidth() و getMeasuredHeight() پس از onMeasure در دسترس هستند، درحالی که getWidth() و getHeight() تنها پس از onLayout در دسترس هستند. اگر درون onMeasure به getWidth() مراجعه کنید، مقدار چرخه قبلی یا صفر بازگردانده میشود. بنابراین برای محاسبه اندازهها در onMeasure باید از MeasureSpec و بهترتیب children استفاده کرد.
موقعیتیابی بدون در نظر گرفتن padding — اولین اشتباه در پیادهسازی onLayout. برنامهنویس اغلب افزودن paddingLeft و paddingTop والد را به مختصات آغازینه View های فرزند فراموش میکند. در نتیجه children در لبه ViewGroup نمایش دادمیشوند و حاشیههایی که از طریق setPadding() یا در XML تنظیم شده را نادیده میگیرند. محاسبه صحیح: childLeft = paddingLeft + offsetX.
فراخوانی layout برای فرزندان غیرقابل مشاهده — دومین مشکل رایج. اگر ViewGroup حاوی View های فرزند با visibility برابر GONE باشد، نیازی به موقعیتیابی آنها نیست — آنها فضا اشغال نمیکنند. اما onLayout باید این مورد را بهدرستی مدیریت کند و GONE ها را نادیده بگیرد. برای INVISIBLE children هنوز هم layout باید فراخوان شود — آنها جای خود را حفظ میکنند هر چند نمایش داده نمیشوند.
نادیده گرفتن پارامتر changed — سومین اشتباه. پارامتر changed نشان میدهد که آیا اندازه یا موقعیت ViewGroup تغییر کرده است. اگر changed == false، میتوان از مختصات ذخیرهشده استفاده کرد و layout کلیه المان فرزند را مجدداً محاسبه نکرد. اما ذخیرهسازی کامل layout کاری پیچیده است و اکثر پیادهسازیهای onLayout صرفاً هر بار تمام عناصر را مجدداً محاسبه میکنند. این در صورت تعداد کم فرزندان قابل قبول است.
سوالات متداول
میتوان، اگر ViewGroup از LayoutParams استاندارد استفاده کند و منطق موقعیتیابی سفارشی اضافه نکند. اما پیادهسازی استاندارد onLayout در ViewGroup هیچ کاری انجام نمیدهد — المان فرزند موقعیتیابی نخواهند شد. در عمل همه ViewGroup ها (LinearLayout، RelativeLayout، FrameLayout) onLayout را بازنویسی میکنند.
layout() — روش final عمومی View است که توسط سیستم یا ViewGroup والد فراخوان میشود. آن مختصات خود View را تعیین میکند و اگر View از نوع ViewGroup باشد، onLayout را فراخوان میکند. onLayout() — روشی protected است که توسط برنامهنویس برای چیدمان سفارشی المان فرزند بازنویسی میشود.
از نظر فنی — بله، میتواند. اما این به شدت توصیه نمیشود، زیرا منجر به بازگشت بینهایت میشود: requestLayout → onMeasure → onLayout → requestLayout. اگر درون onLayout requestLayout فراخوان شود، سیستم استثنای StackOverflowError صادر میکند. تمامی تغییرات اندازه باید قبل از onLayout انجام شوند.
انیمیشنهای layout (LayoutTransition) تغییرات موقعیت View های فرزند را گیرنده شده و انیمیشن انتقال اعمال میکنند. وقتی LayoutTransition فعال است، onLayout ابتدا موقعیت های نهایی را تعیین میکند، سپس LayoutTransition جابجایی از موقعیت قدیم به جدید را انیمیت میکند. این مستلزم پیادهسازی درست onLayout با مختصات نهایی مناسب است.
invalidate() تنها مرحله draw (ترسیم مجدد) را آغاز میکند و تاثیری بر measure و layout ندارد. برای فراخوانی onLayout باید requestLayout() را فراخوان کرد که چرخه کاملی را آغاز میکند: measure → layout → draw. invalidate برای بهروزرسانی ظاهر وقتی اندازهها و موقعیتها تغییر نمیکنند کارامدتر است.
نتیجهگیری
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید