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), яка повертає чистий розмір без бітів стану.
Android використовує двопрохідний алгоритм вимірювання, який гарантує, що кожен 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 включає не тільки розмір та режим, але й біти стану, доступні через 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, віднімаючи відступи батька та відступи дитини, і передає скоригований MeasureSpec в child.measure().
Для просунутої логіки вимірювання ViewGroup може перевизначити measureChild(View child, int parentWidthSpec, int parentHeightSpec) або безпосередньо працювати з MeasureSpec для кожного дочірного елемента. Наприклад, LinearLayout в onMeasure перебирає всі дочірні View, вимірює кожен з урахуванням його layout_weight та розподіляє залишковий простір пропорційно. Цей підхід дозволяє реалізувати довільні алгоритми верстання.
Кешування результатів вимірювання через механізм кешів вимірювання доступне через прапорець 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також