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 розраховуються з урахуванням 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 якщо присутній), 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 визначає бажані (виміряні) розміри View, тоді як onLayout встановлює фактичні (фінальні) координати та розміри. Ключова відмінність: в onMeasure розміри можуть бути проміжними та пізніше скоригованими батьком, а в onLayout фіксується остаточне положення кожного дочірнього View.

onMeasure викликається для кожного View, включаючи листові (TextView, ImageView, Button). onLayout викликається лише для ViewGroup. Це пояснюється тим, що позиціонування — відповідальність батьківського контейнера, а не самого View. Листовий View отримує свою позицію через layout(), викликаний із батьківського onLayout.

getMeasuredWidth() та getMeasuredHeight() доступні після onMeasure, а getWidth() та getHeight() — лише після onLayout. Якщо звернутися до getWidth() всередині onMeasure, буде повернено значення попереднього циклу або нуль. Тому для розрахунку розмірів в onMeasure слід використовувати MeasureSpec та дітей послідовно.

Типові помилки при роботі з 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() — захищений метод, який перевизначається розробником для кастомного розташування дочірніх елементів.

Чи може 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 створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також