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 внутри родительского контейнера. Разработчик переопределяет 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 начинается с вызова публичного метода 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 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) координаты и размеры. 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.

Типовые ошибки при работе с onLayout

Позиционирование без учёта 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 просто пересчитывает все элементы каждый раз. Это допустимо при небольшом числе детей.

Часто задаваемые вопросы

Можно ли не переопределять 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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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