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 та викликає onLayout, якщо View є ViewGroup. Далі 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 розробник викликає 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).
Створимо 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 визначає бажані (виміряні) розміри 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 та дітей послідовно.
Позиціонування без урахування 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 просто перераховує всі елементи щоразу. Це допустимо при невеликій кількості дітей.
Поширені запитання
Можна, якщо ViewGroup використовує стандартні LayoutParams і не додає кастомної логіки позиціонування. Однак стандартна реалізація onLayout у ViewGroup не виконує жодних дій — дочірні елементи не будуть позиціоновані. На практиці всі ViewGroup (LinearLayout, RelativeLayout, FrameLayout) перевизначають onLayout.
layout() — публічний фінальний метод View, що викликається системою або батьківською ViewGroup. Він встановлює координати самого View та викликає onLayout, якщо View є ViewGroup. onLayout() — захищений метод, який перевизначається розробником для кастомного розташування дочірніх елементів.
Технічно — так, може. Але це категорично не рекомендується, оскільки призводить до нескінченної рекурсії: 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 створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також