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 внутри родительского контейнера. Разработчик переопределяет this метод, когда создаёт кастомную 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 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) координаты и размеры. key отличие: в 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.
Позиционирование без учёта padding — первая ошибка при реализации onLayout. Разработчик часто забывает добавить paddingLeft и paddingTop родителя к начальным координатам дочерних View. В результате children отображаются с краю 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 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также