onLayout(): چیست، الگوریتم مکان‌یابی و پارامترهای متد

نویسنده: IT Sectr منتشر شده: 2026-07-22 زمان مطالعه: 9 دقیقه

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) — روش ViewGroup برای تعیین موقعیت المان فرزند در مختصات والد
  • child.layout(l, t, r, b) — فراخوانی برای هر View فرزند که مرزهای نهایی آن را تعیین می‌کند
  • مرحله 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 است که توسط سیستم برای موقعیت‌یابی 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 با فراخوانی روش عمومی 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 علت شایع مشکلات کارایی است.

پارامترهای 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، برنامه‌نویس برای هر 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) فراخوان می‌شود.

مثال ViewGroup سفارشی با onLayout در Kotlin

یک FlowLayout ایجاد می‌کنیم — یک ViewGroup سفارشی که View های فرزند را در سطرها قرار می‌دهد، وقتی سطر فعلی پر شود، المان‌ها را به سطر جدید منتقل می‌کند. این معادل Flexbox با wrap در یک صفحه است. onLayout تمام View های فرزند را پیمایش می‌کند، موقعیت هر کدام را محاسبه کرده و 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 اندازه‌های مورد نظر (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 استفاده کرد.

اشتباهات رایج در کار با onLayout

موقعیت‌یابی بدون در نظر گرفتن 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 صرفاً هر بار تمام عناصر را مجدداً محاسبه می‌کنند. این در صورت تعداد کم فرزندان قابل قبول است.

سوالات متداول

آیا می‌توان onLayout را در ViewGroup بازنویسی نکرد؟

می‌توان، اگر ViewGroup از LayoutParams استاندارد استفاده کند و منطق موقعیت‌یابی سفارشی اضافه نکند. اما پیاده‌سازی استاندارد onLayout در ViewGroup هیچ کاری انجام نمی‌دهد — المان فرزند موقعیت‌یابی نخواهند شد. در عمل همه ViewGroup ها (LinearLayout، RelativeLayout، FrameLayout) onLayout را بازنویسی می‌کنند.

تفاوت بین layout و onLayout چیست؟

layout() — روش final عمومی View است که توسط سیستم یا ViewGroup والد فراخوان می‌شود. آن مختصات خود View را تعیین می‌کند و اگر View از نوع ViewGroup باشد، onLayout را فراخوان می‌کند. onLayout() — روشی protected است که توسط برنامه‌نویس برای چیدمان سفارشی المان فرزند بازنویسی می‌شود.

آیا onLayout می‌تواند requestLayout را فراخوان کند؟

از نظر فنی — بله، می‌تواند. اما این به شدت توصیه نمی‌شود، زیرا منجر به بازگشت بی‌نهایت می‌شود: requestLayout → onMeasure → onLayout → requestLayout. اگر درون onLayout requestLayout فراخوان شود، سیستم استثنای StackOverflowError صادر می‌کند. تمامی تغییرات اندازه باید قبل از onLayout انجام شوند.

onLayout چگونه با انیمیشن‌ها کار می‌کند؟

انیمیشن‌های layout (LayoutTransition) تغییرات موقعیت View های فرزند را گیرنده شده و انیمیشن انتقال اعمال می‌کنند. وقتی LayoutTransition فعال است، onLayout ابتدا موقعیت های نهایی را تعیین می‌کند، سپس LayoutTransition جابجایی از موقعیت قدیم به جدید را انیمیت می‌کند. این مستلزم پیاده‌سازی درست onLayout با مختصات نهایی مناسب است.

چرا onLayout پس از invalidate فراخوان نمی‌شود؟

invalidate() تنها مرحله draw (ترسیم مجدد) را آغاز می‌کند و تاثیری بر measure و layout ندارد. برای فراخوانی onLayout باید requestLayout() را فراخوان کرد که چرخه کاملی را آغاز می‌کند: measure → layout → draw. invalidate برای به‌روزرسانی ظاهر وقتی اندازه‌ها و موقعیت‌ها تغییر نمی‌کنند کارامدتر است.

نتیجه‌گیری

  • onLayout() — روش ViewGroup برای تعیین موقعیت های نهایی View های فرزند پس از پایان مرحله اندازه‌گیری
  • child.layout(l, t, r, b) — مکانیسم اصلی تعیین مختصات برای هر عضو فرزند
  • پارامترهای l، t، r، b — مختصات لبه‌های ViewGroup در سیستم والد، عرض = r - l، ارتفاع = b - t
  • مرحله layout به‌صورت بازگشتی از View ریشه به عناصر فرزند منتشر شده، onLayout را در هر ViewGroup فراخوان می‌کند
  • requestLayout() چرخه کامل محاسبه مجدد اندازه‌ها و موقعیت‌ها را آغاز می‌کند، بر خلاف invalidate که تنها ترسیم مجدد را آغاز می‌کند
  • در نظر گرفتن padding در onLayout اجباری است — مختصات آغازینه فرزندان باید شامل paddingLeft و paddingTop والد باشد

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید