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), яка повертає чистий розмір без бітів стану.

Двопрохідний алгоритм вимірювання

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 може не вимагатись.

Флаг MEASURED_SIZE_STATE

MeasureSpec включає не тільки розмір та режим, але й біти стану, доступні через 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, віднімаючи відступи батька та відступи дитини, і передає скоригований MeasureSpec в child.measure().

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

Кешування результатів вимірювання через механізм кешів вимірювання доступне через прапорець 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

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