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 и извиква onLayout, ако View е ViewGroup. След това 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, разработчикът извиква child.layout(int childLeft, int childTop, int childRight, int childBottom) за всяко дочерно View. Координатите, предадени на 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 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, включително листовните (TextView, ImageView, Button). onLayout се извиква само за ViewGroup. Това се обяснява с факта, че позиционирането е отговорност на родителския контейнер, а не на самото View. Листовното View получава своята позиция чрез layout(), извикван от родителския onLayout.

getMeasuredWidth() и getMeasuredHeight() са достъпни след onMeasure, докато getWidth() и getHeight() — едва след onLayout. Ако достъпите getWidth() вътре в onMeasure, ще бъде върната стойност от предишния цикъл или нула. Затова за изчисляване на размери в onMeasure трябва да използвате MeasureSpec и последователно children.

Типични грешки при работа с onLayout

Позициониране без отчитане на padding — първата грешка при имплементиране на onLayout. Разработчикът често забравя да добави paddingLeft и paddingTop на родителя към началните координати на дочерните View. В резултат на това децата се показват на ръба на ViewGroup, игнорирайки разстоянията, зададени чрез setPadding() или в XML. Правилното изчисляване: childLeft = paddingLeft + offsetX.

Извикване на layout за невидими деца — вторият чест проблем. Ако ViewGroup съдържа дочерни View с visibility, равно на 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. Той задава координатите на самото View и извиква onLayout, ако View е ViewGroup. onLayout() — е protected метод, който разработчикът препокрива за поръчано подреждане на дочерните елементи.

Може ли onLayout да извика requestLayout?

Технически — да, може. Но това категорично не се препоръчва, тъй като води до безкрайна рекурсия: requestLayout → onMeasure → onLayout → requestLayout. Ако requestLayout се извика вътре в onLayout, системът ще хвърли изключение 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 създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също