onMeasure(): что это, режимы MeasureSpec и переопределение метода

Автор: IT Sectr Опубликовано: 2026-07-22 Время чтения: 9 мин

onMeasure() — это protected метод класса android.view.View, который вызывается системой Android для определения размеров View. Система передаёт в метод два объекта MeasureSpec, каждый из которых содержит режим измерения (EXACTLY, AT_MOST или UNSPECIFIED) и размер, предлагаемый родительским контейнером. По данным Android Developers Documentation (2026), переопределение onMeasure с корректной обработкой MeasureSpec — обязательный этап для всех кастомных View и ViewGroup, требующих точного контроля над размерами.

Главное

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — метод View для измерения размеров с передачей MeasureSpec от родителя
  • MeasureSpec — 32-битное значение, кодирующее режим измерения (UNSPECIFIED, EXACTLY, AT_MOST) и размер
  • setMeasuredDimension(int w, int h) — обязательный вызов внутри onMeasure, фиксирующий итоговые размеры View
  • Двухпроходный алгоритм измерения: родитель измеряет детей, затем дети сообщают свои размеры, и родитель принимает окончательное решение
  • measureChildWithMargins — вспомогательный метод для измерения дочерних View в кастомных ViewGroup

Что такое onMeasure()?

onMeasure(int widthMeasureSpec, int heightMeasureSpec) — это метод класса View, который система Android вызывает для определения ширины и высоты представления. Разработчик переопределяет этот метод, чтобы указать, какого размера должен быть View на основе ограничений, переданных в MeasureSpec. Без корректного переопределения onMeasure кастомный View может отображаться неправильно или не отображаться вовсе.

Система вызывает onMeasure в фазе measure жизненного цикла View, которая предшествует фазам layout (onLayout) и draw (onDraw). Если View не переопределяет onMeasure, используется реализация из суперкласса, которая устанавливает размеры по умолчанию на основе background drawable или layout_params. Вызов super.onMeasure(widthMeasureSpec, heightMeasureSpec) работает только для стандартных подклассов View, таких как TextView или ImageView.

Ключевое требование к onMeasure — вызов setMeasuredDimension(int, int) обязательно должен присутствовать в конце метода. Если этот вызов отсутствует, система генерирует исключение IllegalStateException с сообщением о том, что View не зафиксировал измеренные размеры. Итоговые размеры становятся доступны через геттеры getMeasuredWidth() и getMeasuredHeight() после завершения фазы measure.

Режимы MeasureSpec: три ключевых значения

MeasureSpec — это 32-битное целое число, в котором старшие 2 бита кодируют режим измерения, а младшие 30 бит — размер. Режим определяет, насколько свободен View в выборе своего размера. Android предоставляет три режима: EXACTLY, AT_MOST и UNSPECIFIED. Каждый режим диктует разную логику обработки в onMeasure.

Режим MeasureSpecЗначениеПоведение
EXACTLYРодитель указал точный размерView должен ровно вписаться в переданный размер, если не хочет выйти за границы
AT_MOSTРодитель задал максимальный размерView может выбрать любой размер от 0 до переданного максимума
UNSPECIFIEDРодитель не накладывает ограниченийView может выбрать любой желаемый размер без ограничений сверху

Для извлечения режима и размера из MeasureSpec используются статические методы класса MeasureSpec: MeasureSpec.getMode(int) возвращает один из трёх режимов (EXACTLY, AT_MOST, UNSPECIFIED), а MeasureSpec.getSize(int) возвращает числовой размер в пикселях. Для создания собственного MeasureSpec используется MeasureSpec.makeMeasureSpec(int size, int mode). Эти три метода покрывают все сценарии работы с размерами в onMeasure.

Типичная логика обработки MeasureSpec

Стандартный паттерн обработки MeasureSpec: если режим EXACTLY — используйте переданный размер как окончательный; если AT_MOST — выберите минимальное из желаемого размера (содержимого View) и переданного максимума; если UNSPECIFIED — используйте желаемый размер View без ограничений. Этот паттерн гарантирует корректное поведение при любых ограничениях родителя.

kotlin
override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
    val desiredWidth = 200
    val desiredHeight = 100

    val widthMode = MeasureSpec.getMode(widthMeasureSpec)
    val widthSize = MeasureSpec.getSize(widthMeasureSpec)
    val heightMode = MeasureSpec.getMode(heightMeasureSpec)
    val heightSize = MeasureSpec.getSize(heightMeasureSpec)

    val width = when (widthMode) {
        MeasureSpec.EXACTLY -> widthSize
        MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize)
        else -> desiredWidth
    }
    val height = when (heightMode) {
        MeasureSpec.EXACTLY -> heightSize
        MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize)
        else -> desiredHeight
    }
    setMeasuredDimension(width, height)
}

Использование resolveSize

Для упрощения стандартной логики Android предоставляет метод resolveSizeAndState, который принимает желаемый размер, MeasureSpec и возвращает итоговый размер с корректным режимом. Этот метод реализует описанный выше паттерн в одной строке кода. Также доступна функция resolveSize(int size, int measureSpec), возвращающая чистый размер без state-битов.

Двухпроходный алгоритм измерения

Android использует двухпроходный алгоритм измерения (two-pass measure), который гарантирует, что каждый View в иерархии получает корректные размеры с учётом ограничений родителя и предпочтений дочерних элементов. На первом проходе родитель передаёт дочерним View MeasureSpec с ограничениями, а дочерние View вычисляют свои желаемые размеры. На втором проходе родитель принимает окончательное решение о размерах.

Для ViewGroup процесс измерения сложнее: родитель должен сначала измерить всех своих дочерних элементов, а затем определить собственный размер на основе их размеров. Вызов measureChildren(int widthMeasureSpec, int heightMeasureSpec) перебирает всех дочерних View и вызывает для каждого measure(child, childWidthSpec, childHeightSpec). После измерения всех дочерних элементов ViewGroup вызывает setMeasuredDimension со своими размерами.

Важный нюанс: метод measure (публичный, final) нельзя переопределять — вместо него переопределяется onMeasure. Это гарантирует, что система может выполнять служебные действия до и после вызова onMeasure, такие как проверка изменения размеров и расчёт dirty-области для последующей отрисовки. Если View имеет фиксированные размеры, переопределение onMeasure может не требоваться.

Флаг MEASURED_SIZE_STATE

MeasureSpec включает не только размер и режим, но и биты состояния (state bits), доступные через MeasureSpec.getMode(). После вызова setMeasuredDimension состояние становится частью измеренных размеров View и может быть проверено через getMeasuredState(). Это используется в ScrollView и других скроллируемых контейнерах для корректной передачи ограничений детям.

Пример переопределения onMeasure на Kotlin

Рассмотрим практический пример создания кастомного View с переопределением onMeasure для квадратного отображения. Класс SquareView наследует View и гарантирует, что ширина и высота всегда равны, независимо от переданных MeasureSpec. В onMeasure определяется минимальная сторона и устанавливается квадратный размер.

kotlin
class SquareView(context: Context)
    : View(context) {

    override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
        val widthSize =
            MeasureSpec.getSize(widthMeasureSpec)
        val heightSize =
            MeasureSpec.getSize(heightMeasureSpec)
        val size = minOf(widthSize, heightSize)
        setMeasuredDimension(size, size)
    }
}

Кастомная ViewGroup с измерением дочерних элементов

ViewGroup требует более сложной логики onMeasure, поскольку сначала измеряются дочерние элементы, затем определяется размер самой ViewGroup. Пример CascadeLayout распределяет дочерние элементы каскадом с отступом. После измерения всех дочерних элементов через measureChildWithMargins вычисляются общие ширина и высота.

kotlin
class CascadeLayout(context: Context)
    : ViewGroup(context) {

    private val cascadeOffset = 40

    override fun onMeasure(widthMeasureSpec: Int,
                       heightMeasureSpec: Int) {
        var maxWidth = 0
        var totalHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child,
                widthMeasureSpec,
                cascadeOffset * i,
                heightMeasureSpec, 0)
            maxWidth = maxOf(maxWidth,
                child.measuredWidth +
                cascadeOffset * i)
            totalHeight += child.measuredHeight
        }
        setMeasuredDimension(
            resolveSize(maxWidth, widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec))
    }

    override fun generateLayoutParams(attrs: AttributeSet?)
        : LayoutParams = MarginLayoutParams(context, attrs)

    override fun onLayout(changed: Boolean,
                       l: Int, t: Int,
                       r: Int, b: Int) {
        var top = t
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val left = l + cascadeOffset * i
            child.layout(left, top,
                left + child.measuredWidth,
                top + child.measuredHeight)
            top += child.measuredHeight
        }
    }
}

Типовые ошибки при переопределении onMeasure

Пропуск вызова setMeasuredDimension — самая распространённая ошибка. Если разработчик переопределил onMeasure, но не вызвал setMeasuredDimension, приложение упадёт с IllegalStateException. Это особенно часто случается, когда в методе присутствуют условные ветвления, и в одной из веток вызов отсутствует. Каждая ветка кода в onMeasure должна заканчиваться вызовом setMeasuredDimension.

Игнорирование режима AT_MOST — вторая по частоте ошибка. Если View в режиме AT_MOST всегда использует переданный размер вместо расчёта по содержимому, родительский контейнер не сможет корректно распределить пространство. Например, TextView в AT_MOST должен рассчитать ширину текста и использовать минимальное из желаемой и переданной ширины. Игнорирование AT_MOST приводит к тому, что View занимает всё доступное пространство даже при маленьком содержимом.

Создание объектов внутри onMeasure — классическая ошибка производительности. Поскольку onMeasure может вызываться многократно (при каждом запросе layout), создание объектов (Paint, Rect, String) внутри этого метода засоряет память и провоцирует сборку мусора. Все объекты должны создаваться один раз в конструкторе View, а в onMeasure должна выполняться только логика вычисления размеров. Это же правило относится к onDraw и onLayout.

Измерение дочерних View в ViewGroup

measureChildWithMargins — это защищённый метод ViewGroup, который измеряет одного дочернего View с учётом его MarginLayoutParams. Метод принимает MeasureSpec родителя и накопленные смещения по ширине и высоте. Он автоматически корректирует MeasureSpec для дочернего View, вычитая отступы родителя и margin ребёнка, и передаёт скорректированный MeasureSpec в child.measure().

Для продвинутой логики измерения ViewGroup может переопределить measureChild(View child, int parentWidthSpec, int parentHeightSpec) или напрямую работать с MeasureSpec для каждого дочернего элемента. Например, LinearLayout в onMeasure перебирает все дочерние View, измеряет каждый с учётом его layout_weight и распределяет оставшееся пространство пропорционально. Этот подход позволяет реализовать произвольные алгоритмы вёрстки.

Кэширование результатов измерения через механизм мерных кэшей (measure cache) доступен через флаг setMeasureWithLargestChildEnabled в определённых ViewGroup. Однако в большинстве случаев onMeasure вызывается повторно при любом изменении layout, и кэширование не применяется. В кастомных ViewGroup рекомендуется минимизировать вычисления в onMeasure, а не полагаться на кэши.

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

Обязательно ли переопределять onMeasure для кастомного View?

Да, если кастомный View наследует непосредственно от класса View. Если наследование идёт от TextView, ImageView или Button с их стандартными размерами, onMeasure можно не переопределять. Для ViewGroup переопределение onMeasure требуется всегда — иначе дочерние элементы не будут корректно измерены.

Что будет, если не вызвать setMeasuredDimension?

Система Android выбрасывает IllegalStateException с текстом «The View did not call setMeasuredDimension». Это исключение возникает в методе measure() после завершения onMeasure, если итоговые размеры остались нулевыми. Исключение крашит приложение, если не обработано через try-catch.

Чем отличается getWidth от getMeasuredWidth?

getMeasuredWidth() возвращает размер, установленный в onMeasure (фаза измерения). getWidth() возвращает фактический размер, который View получил в onLayout после учёта всех поправок позиционирования. Для большинства View эти значения совпадают, но в кастомных ViewGroup они могут отличаться.

Можно ли в onMeasure изменять анимации или состояние View?

Нет, onMeasure предназначен исключительно для расчёта размеров. Изменение состояния, запуск анимаций, работа с сетью или обновление данных в этом методе нарушает архитектуру Android и может привести к рекурсивным вызовам measure, так как изменение состояния может вызвать requestLayout.

Как onMeasure взаимодействует с ConstraintLayout?

ConstraintLayout самостоятельно управляет измерением дочерних элементов на основе заданных констрейнтов. Если кастомный View внутри ConstraintLayout переопределяет onMeasure, он должен корректно обрабатывать MeasureSpec, переданный от ConstraintLayout, иначе констрейнты могут не сработать. ConstraintLayout использует двухпроходный алгоритм с собственным WidgetContainer для расчёта.

Итоги

  • onMeasure() — метод View для определения размеров, вызываемый системой Android на фазе measure
  • MeasureSpec кодирует режим измерения (EXACTLY, AT_MOST, UNSPECIFIED) и размер, передаваемый от родителя
  • setMeasuredDimension — обязательный вызов в конце onMeasure, фиксирующий окончательные размеры
  • resolveSize — вспомогательный метод, реализующий стандартную логику обработки MeasureSpec в одну строку
  • Двухпроходный алгоритм гарантирует корректное измерение в иерархии родитель-дочерние элементы
  • measureChildWithMargins используется для измерения дочерних View в кастомных ViewGroup с учётом отступов
  • Создание объектов внутри onMeasure категорически не рекомендуется из-за риска GC и пропуска кадров

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

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

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