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, который система 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 — это 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: если режим EXACTLY — используйте переданный размер как окончательный; если AT_MOST — выберите минимальное из желаемого размера (содержимого View) и переданного максимума; если UNSPECIFIED — используйте желаемый размер View без ограничений. Этот паттерн гарантирует корректное поведение при любых ограничениях родителя.
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)
}
Для упрощения стандартной логики 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 может не требоваться.
MeasureSpec включает не только размер и режим, но и биты состояния (state bits), доступные через MeasureSpec.getMode(). После вызова setMeasuredDimension состояние становится частью измеренных размеров View и может быть проверено через getMeasuredState(). Это используется в ScrollView и других скроллируемых контейнерах для корректной передачи ограничений детям.
Рассмотрим практический пример создания кастомного View с переопределением onMeasure для квадратного отображения. Класс SquareView наследует View и гарантирует, что ширина и высота всегда равны, независимо от переданных MeasureSpec. В onMeasure определяется минимальная сторона и устанавливается квадратный размер.
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 требует более сложной логики onMeasure, поскольку сначала измеряются дочерние элементы, затем определяется размер самой ViewGroup. Пример CascadeLayout распределяет дочерние элементы каскадом с отступом. После измерения всех дочерних элементов через measureChildWithMargins вычисляются общие ширина и высота.
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
}
}
}
Пропуск вызова 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.
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, а не полагаться на кэши.
Часто задаваемые вопросы
Да, если кастомный View наследует непосредственно от класса View. Если наследование идёт от TextView, ImageView или Button с их стандартными размерами, onMeasure можно не переопределять. Для ViewGroup переопределение onMeasure требуется всегда — иначе дочерние элементы не будут корректно измерены.
Система Android выбрасывает IllegalStateException с текстом «The View did not call setMeasuredDimension». Это исключение возникает в методе measure() после завершения onMeasure, если итоговые размеры остались нулевыми. Исключение крашит приложение, если не обработано через try-catch.
getMeasuredWidth() возвращает размер, установленный в onMeasure (фаза измерения). getWidth() возвращает фактический размер, который View получил в onLayout после учёта всех поправок позиционирования. Для большинства View эти значения совпадают, но в кастомных ViewGroup они могут отличаться.
Нет, onMeasure предназначен исключительно для расчёта размеров. Изменение состояния, запуск анимаций, работа с сетью или обновление данных в этом методе нарушает архитектуру Android и может привести к рекурсивным вызовам measure, так как изменение состояния может вызвать requestLayout.
ConstraintLayout самостоятельно управляет измерением дочерних элементов на основе заданных констрейнтов. Если кастомный View внутри ConstraintLayout переопределяет onMeasure, он должен корректно обрабатывать MeasureSpec, переданный от ConstraintLayout, иначе констрейнты могут не сработать. ConstraintLayout использует двухпроходный алгоритм с собственным WidgetContainer для расчёта.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также