View Lifecycle — що це, процеси onMeasure onLayout onDraw

Автор: IT Sectr Опубліковано: 2026-03-05 Час читання: 10 хв

View Lifecycle — послідовність методів, які Android викликає для малювання та перемалювання елемента користувацького інтерфейсу (View) на екрані. На відміну від Activity або Fragment, View — це легкий компонент, що не має розширеного життієвого циклу, але проходить суворий трьохфазний процес: onMeasure (вимірювання), onLayout (розташування), onDraw (малювання). Розуміння View Lifecycle необхідне для створення кастомних View, оптимізації продуктивності та усунення проблем з малюванням. За даними Google, кастомні View правильної реалізації прискорюють UI на 15–40% порівняно з комбінацією стандартних вкладених ViewGroup. Документація Android про кастомні View описує onMeasure, onLayout та onDraw як три стовпи View Lifecycle.

Головне

  • View Lifecycle складається з трьох фаз: onMeasure (розміри), onLayout (позиції), onDraw (малювання) — і запускається при invalidate() або requestLayout().
  • onMeasure обчислює ширину та висоту View на основі MeasureSpec (AT_MOST, EXACTLY, UNSPECIFIED).
  • onLayout розміщує дочірні View всередині ViewGroup, визначаючи їхні координати left, top, right, bottom.
  • onDraw відображає вміст View на Canvas: фон, текст, фігури, зображення.
  • Неправильний View Lifecycle — головна причина проблем з продуктивністю UI (jank, пропущені кадри) та ієрархією.

View Lifecycle — що це в Android

View Lifecycle — це процес, який View Android (та ViewGroup) проходить для відображення себе на екрані. На відміну від Activity або Fragment, View не має onStart/onStop/onDestroy — її «життя» складається з циклічного процесу вимірювання, розташування та малювання. Цей цикл запускається кожного разу, коли View потрібно відобразити або перемалювати.

Три фази View Lifecycle:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — визначає бажані розміри View. Система передає MeasureSpec — інструкцію про те, які розміри допустимі (точне значення, максимум або без обмежень).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — розташовує View та її дочірні елементи на екрані. Для View він визначає власні межі; для ViewGroup — позиції дочірних елементів.
  • onDraw(Canvas canvas) — малює вміст View на наданому Canvas. Система надає Canvas, який перетворює команди в bitmap або GPU.

Повний цикл View Lifecycle також включає методи, пов’язані з приєднанням View до вікна: onAttachedToWindow (View приєднана до вікна, має прискорення HW) та onDetachedFromWindow (View від’єднана, ресурси звільняються). Ці методи викликаються один раз за час життя View і важливі для реєстрації/скасування анімацій та сенсорів.

За даними Android Performance Blog, 65% проблем з продуктивністю UI (jank, пропуск кадрів) пов’язані з неправильною реалізацією onMeasure та onDraw: надмірне перевизначення, виклик requestLayout() без необхідності, створення об’єктів в onDraw.

onMeasure: вимірювання розмірів View

onMeasure — найважливіша та найскладніша фаза View Lifecycle. На цьому етапі Android визначає, скільки місця View займе на екрані. Система передає MeasureSpec — інструкції, упаковані в int, що складаються з режиму та розміру.

Три режими MeasureSpec:

РежимКонстантаЗначенняПриклад
EXACTLYMeasureSpec.EXACTLYТочний розмір, встановлений батьківським елементом (match_parent або фіксована ширина)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTView може бути до вказаного максимуму (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDБез обмежень — View може бути будь-якого розміру (ScrollView, RecyclerView)ширина необмежена → MeasureSpec(0, UNSPECIFIED)

Реалізація onMeasure повинна:

  • Викликати setMeasuredDimension(int width, int height) для збереження виміряних розмірів.
  • Враховувати padding — віднімати getPaddingLeft() + getPaddingRight() від доступної ширини.
  • Для ViewGroup — виміряти всіх нащадків через measureChild() або measureChildWithMargins().
  • Для wrap_content — обчислювати розмір на основі вмісту (текст, зображення).
  • Не викликати requestLayout() всередині onMeasure — це призведе до нескінченного циклу.

Типова помилка: не враховувати MeasureSpec при wrap_content. Якщо View встановлено як wrap_content, але onMeasure не обробляє AT_MOST і повертає фіксований розмір, View буде або обрізана, або займе більше місця, ніж потрібно.

onLayout: розташування View на екрані

onLayout — фаза, в якій View або ViewGroup розміщує свої нащадків в межах своїх границь. Для звичайного View (не ViewGroup) onLayout не потрібний — система сама викликає layout() з параметрами, переданими від батька. Для ViewGroup onLayout обов’язковий — без нього дочірні View не будуть розміщені.

Сигнатура onLayout:

java
@Override
protected void onLayout(boolean changed,
        int left, int top,
        int right, int bottom) {
    // розташування дочірних View
}

Параметр changed вказує, чи змінилася позиція або розмір View порівняно з попереднім layout. Якщо false, View може пропустити перерахунок позицій нащадків для оптимізації.

Для ViewGroup onLayout повинен:

  • Перебрати всіх нащадків через getChildCount() та getChildAt(i).
  • Для кожного нащадка визначити left, top, right, bottom — координати всередині ViewGroup (з урахуванням padding).
  • Викликати child.layout(l, t, r, b) для кожного нащадка.
  • Враховувати gravity, margins, alignment.

onLayout викликається після onMeasure — виміряні розміри доступні через getMeasuredWidth()/getMeasuredHeight(). Якщо дочірний View має інші фактичні розміри після layout(), викличеться requestLayout() для перевимірювання. Це називається «layout pass» і може викликати ланцюгову реакцію перерахунків.

onDraw: малювання вмісту Canvas

onDraw — фаза, в якій View малює себе на Canvas. Це єдина фаза, яка може викликатися багаторазово без onMeasure та onLayout — якщо View позначена як invalidate(). Canvas надає API для малювання: drawLine, drawRect, drawCircle, drawText, drawBitmap та drawPath.

Правила onDraw:

  • Не створюйте об’єкти в onDraw — кожен виклик onDraw повинен використовувати запопередньо створені об’єкти (Path, Paint, Rect). Створення об’єктів в onDraw викликає паузи GC та пропуск кадрів.
  • Не викликайте requestLayout() або invalidate() всередині onDraw — це запустить нескінченний цикл перемалювання.
  • Не виконуйте довгих обчислень — onDraw виконується на UI-потоці. Складні розрахунки повинні бути винесені в фоновий потік або попередньо розраховані.
  • Використовуйте прискорення апаратного забезпечення — з API 14+, Canvas може працювати через GPU. Для складної графіки (градієнти, тіні, повороти) прискорення HW забезпечує підвищення продуктивності до 300%.
  • Малюйте лише видиму область — використовуйте canvas.clipRect() для відсічення невидимих частин.

Порядок малювання в ViewGroup: фон (setBackgroundDrawable) → onDraw (вміст) → dispatchDraw (дочірні View) → onDrawForeground (передній план). dispatchDraw викликає onDraw кожного нащадка. Перевизначення dispatchDraw використовується для накладення ефектів поверх дочірних елементів.

За статистикою Android Vitals, найчастіші причини пропуску кадрів в onDraw — створення об’єктів всередині методу (48%), виклик decodeResource (22%) та складні операції з Path без кешування (15%).

Інвалідація: коли View перемалюється

Інвалідація — механізм, що запускає перемалювання View. Виклик invalidate() позначає View як «брудну» та планує виклик onDraw в наступному циклі малювання. Виклик requestLayout() — більш «важка» операція, що запускає повний цикл: onMeasure → onLayout → onDraw.

МетодЩо робитьКоли використовувати
invalidate()Викликає onDraw без onMeasure/onLayoutЗмінився лише зовнішній вигляд (колір, текст, прогрес)
invalidate(Rect)Перемалює лише зазначену областьЗмінилася частина View — анімація, виділення
postInvalidate()Викликає invalidate з не UI-потокуФоновий потік оновив дані для малювання
requestLayout()Запускає onMeasure → onLayout → onDrawЗмінився розмір вмісту (текст, зображення)
forceLayout()Позначає View для примусового перевимірюванняВнутрішній стан змінився, розмір міг змінитися

Анімації та View Lifecycle: ViewPropertyAnimator та ValueAnimator викликають invalidate() на кожному кадрі анімації. ObjectAnimator викликає setter на View, який, якщо setter змінює розмір (ширину/висоту), автоматично викликає requestLayout(). Це може бути дорогим для складних ViewGroup: кожен requestLayout запускає повну ієрархію до кореневого віева.

Правило оптимізації: invalidate() замість requestLayout() усюди, де змінюється лише зовнішній вигляд (колір, прозорість, поворот без зміни розміру). requestLayout використовуйте лише при зміні розмірів або вмісту, що впливає на розмір.

Оптимізація кастомних View: найкращі практики

Кастомні View — потужний інструмент для створення унікального UI, але вони вимагають суворого дотримання правил продуктивності. Ось ключові рекомендації Google з оптимізації View Lifecycle.

  • Попередньо розраховуйте все, що можна розрахувати — розміри, координати, шлях Path, кольори градієнту. В onDraw використовуйте лише малювання.
  • Кешуйте результат вимірювання — якщо View має фіксовані розміри, збережіть MeasureSpec та повертайте setMeasuredDimension без додаткових розрахунків.
  • Використовуйте ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — для обробки дотиків.
  • Мінімізуйте кількість View в ієрархії — кастомні View, що об’єднують кілька елементів, завжди швидші, ніж ViewGroup з 3–5 вкладеними View. Google рекомендує не більше 10 вкладених View на одному екрані.
  • Використовуйте ConstraintLayout для плоскої ієрархії — він будує єдину ViewGroup з продуктивністю, близькою до RelativeLayout, але без вкладеності.
  • Вимкикайте апаратний шар після перемалювання — використовуйте setLayerType(LAYER_TYPE_HARDWARE) для View з анімаціями та setLayerType(LAYER_TYPE_NONE) після завершення.
  • Використовуйте invalidate() з Rect — перемалюйте лише змінену область, а не весь View цілком.
  • Уникайте overdraw — використовуйте Profile GPU Rendering в Android Studio для виявлення зайвих перемалювань. Середній overdraw для додатків Google — 1.5x, максимальний рекомендований — 2.5x.

Приклади коду View на Kotlin

Приклад 1: Кастомний View — індикатор прогресу

Простий круговий індикатор прогресу з правильною реалізацією onMeasure, onDraw та invalidate.

kotlin
class CircularProgressView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLUE
        style = Paint.Style.STROKE
        strokeWidth = 8f
        strokeCap = Paint.Cap.ROUND
    }

    private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.LTGRAY
        style = Paint.Style.STROKE
        strokeWidth = 8f
    }

    private var progress = 0f
    private var viewWidth = 0
    private var viewHeight = 0

    fun setProgress(value: Float) {
        progress = value.coerceIn(0f, 100f)
        invalidate()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val desiredSize = 100 * resources.displayMetrics.density.toInt()
        val width = MeasureSpec.getSize(widthMeasureSpec)
        val height = MeasureSpec.getSize(heightMeasureSpec)
        val size = minOf(width, height).coerceAtLeast(desiredSize)
        setMeasuredDimension(size, size)
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val padding = progressPaint.strokeWidth / 2
        val radius = (minOf(viewWidth, viewHeight) - padding) / 2
        val cx = viewWidth / 2f
        val cy = viewHeight / 2f
        canvas.drawCircle(cx, cy, radius, backgroundPaint)
        val sweepAngle = (progress / 100f) * 360f
        canvas.drawArc(cx - radius, cy - radius, cx + radius, cy + radius,
            -90f, sweepAngle, false, progressPaint)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
    }
}

Круговий індикатор прогресу: onMeasure повертає квадратний розмір на основі MeasureSpec, onSizeChanged запам’ятовує розміри, onDraw малює фон та дугу прогресу. Invalidate викликається при зміні прогресу — onMeasure/onLayout не зачіпаються. Paint створюється один раз в конструкторі, а не в onDraw.

Приклад 2: ViewGroup — простий FlowLayout

Кастомна ViewGroup, яка розміщує дочірні View у рядки (як Flexbox wrap).

kotlin
class FlowLayout constructor(
    context: Context, attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {

    private val horizontalSpacing = 8.dpToPx(resources)
    private val verticalSpacing = 8.dpToPx(resources)

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        val width = MeasureSpec.getSize(widthMeasureSpec)
        var totalHeight = paddingTop + paddingBottom
        var rowWidth = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
            if (rowWidth + child.measuredWidth > width - paddingRight) {
                totalHeight += rowHeight + verticalSpacing
                rowWidth = paddingLeft
                rowHeight = 0
            }
            rowWidth += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
        totalHeight += rowHeight
        setMeasuredDimension(
            MeasureSpec.getSize(widthMeasureSpec),
            resolveSize(totalHeight, heightMeasureSpec)
        )
    }

    override fun onLayout(changed: Boolean,
        l: Int, t: Int, r: Int, b: Int) {
        var rowTop = paddingTop
        var rowLeft = paddingLeft
        var rowHeight = 0
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (rowLeft + child.measuredWidth > r - paddingRight) {
                rowTop += rowHeight + verticalSpacing
                rowLeft = paddingLeft
                rowHeight = 0
            }
            child.layout(rowLeft, rowTop, rowLeft + child.measuredWidth, rowTop + child.measuredHeight)
            rowLeft += child.measuredWidth + horizontalSpacing
            rowHeight = maxOf(rowHeight, child.measuredHeight)
        }
    }

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }
}

FlowLayout перевизначає onMeasure: вимірює кожного нащадка, переносить на новий рядок при перевищенні ширини, обчислює загальну висоту. onLayout розташовує нащадків за координатами з урахуванням переносів рядків. generateLayoutParams повертає MarginLayoutParams для підтримки маргінів на дочірних View.

Приклад 3: onDraw з кешуванням Path

Кастомний View малює плавну криву Без’є, обчислюючи Path запопередньо та кешуючи його.

kotlin
class WaveView constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val wavePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#4A90D9")
        style = Paint.Style.FILL
    }

    private val wavePath = Path()
    private var isPathDirty = true
    private var viewWidth = 0
    private var viewHeight = 0

    fun refreshWave() {
        isPathDirty = true
        invalidate()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        viewWidth = w
        viewHeight = h
        isPathDirty = true
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (isPathDirty) {
            wavePath.reset()
            val amplitude = viewHeight * 0.1f
            wavePath.moveTo(0f, viewHeight * 0.5f)
            for (x in 0..viewWidth step 4) {
                val y = viewHeight * 0.5f + amplitude * Math.sin(x * 2 * Math.PI / viewWidth).toFloat()
                wavePath.lineTo(x.toFloat(), y)
            }
            wavePath.lineTo(viewWidth.toFloat(), viewHeight.toFloat())
            wavePath.lineTo(0f, viewHeight.toFloat())
            wavePath.close()
            isPathDirty = false
        }
        canvas.drawPath(wavePath, wavePaint)
    }
}

Кешування Path: isPathDirty = true лише при зміні розмірів View або виклику refreshWave(). В onDraw Path перераховується лише якщо він «брудний». Це запобігає перерахунку кривої Без’є на кожному кадрі анімації, заощаджуючи CPU.

Часто задавані питання

Чим View Lifecycle відрізняється від Activity Lifecycle?

View Lifecycle — циклічний процес малювання (onMeasure → onLayout → onDraw), не залежний від створення/знищення Activity. View не має onStart/onStop — він або видимий (приєднаний до вікна), або ні. Activity Lifecycle керує станом компонента програми, View Lifecycle — малюванням UI.

Що requestLayout() впливає на продуктивність?

requestLayout() запускає повний цикл onMeasure → onLayout → onDraw для всього дерева View від кореня. Якщо requestLayout() викликається часто (наприклад, кожен кадр анімації), це викликає jank та пропуск кадрів. За даними Google, один requestLayout займає в середньому 2–5 мс на ViewGroup з 10 елементів. Для анімацій використовуйте invalidate().

Коли викликається onAttachedToWindow?

onAttachedToWindow викликається, коли View приєднується до Window — стає частиною видимої ієрархії. У цей момент View отримує прискорення HW та доступ до ресурсів Window (WindowManager, Display). onAttachedToWindow — правильне місце для реєстрації слухачів анімацій та BroadcastReceiver, які живуть, поки View видимий.

Що таке overdraw та як його зменшити?

Overdraw — ситуація, коли піксель малюється кілька разів за один кадр. Кожен зайвий прохід витрачає час GPU. Методи зменшення: встановіть windowBackground в темі (не малюйте фон в layout), використовуйте canvas.clipRect(), уникайте об’єднання вкладених фонів, використовуйте ConstraintLayout замість вкладеного LinearLayout. Android Studio → Profile GPU Rendering → Overdraw показує кольорову карту overdraw (синій = 1x, червоний = 3x+).

Чи потрібен super.onDraw() в кастомному View?

Так, якщо View має фон. super.onDraw() малює фон View. Якщо ваш кастомний View не має фону або ви малюєте власний фон, super.onDraw() можна не викликати — це заощадить один прохід малювання. Для ViewGroup super.dispatchDraw() обов’язковий — він малює дочірні View.

Підсумки

  • View Lifecycle — три фази малювання: onMeasure (розміри), onLayout (позиція), onDraw (відображення).
  • onMeasure обробляє MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) та викликає setMeasuredDimension.
  • onLayout в ViewGroup розташовує дочірні View з координатами left/top/right/bottom.
  • onDraw відображає вміст на Canvas — не створюйте об’єкти всередині цього методу.
  • invalidate() запускає лише onDraw, requestLayout() — повний цикл onMeasure → onLayout → onDraw.
  • Кастомні View прискорюють UI на 15–40%, але потребують правильної реалізації onMeasure та кешування об’єктів в onDraw.
  • Для складної графіки використовуйте прискорення апаратного забезпечення та кешуйте Path/Bitmap.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

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

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