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 — це процес, який View Android (та ViewGroup) проходить для відображення себе на екрані. На відміну від Activity або Fragment, View не має onStart/onStop/onDestroy — її «життя» складається з циклічного процесу вимірювання, розташування та малювання. Цей цикл запускається кожного разу, коли View потрібно відобразити або перемалювати.
Три фази View Lifecycle:
Повний цикл View Lifecycle також включає методи, пов’язані з приєднанням View до вікна: onAttachedToWindow (View приєднана до вікна, має прискорення HW) та onDetachedFromWindow (View від’єднана, ресурси звільняються). Ці методи викликаються один раз за час життя View і важливі для реєстрації/скасування анімацій та сенсорів.
За даними Android Performance Blog, 65% проблем з продуктивністю UI (jank, пропуск кадрів) пов’язані з неправильною реалізацією onMeasure та onDraw: надмірне перевизначення, виклик requestLayout() без необхідності, створення об’єктів в onDraw.
onMeasure — найважливіша та найскладніша фаза View Lifecycle. На цьому етапі Android визначає, скільки місця View займе на екрані. Система передає MeasureSpec — інструкції, упаковані в int, що складаються з режиму та розміру.
Три режими MeasureSpec:
| Режим | Константа | Значення | Приклад |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Точний розмір, встановлений батьківським елементом (match_parent або фіксована ширина) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View може бути до вказаного максимуму (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Без обмежень — View може бути будь-якого розміру (ScrollView, RecyclerView) | ширина необмежена → MeasureSpec(0, UNSPECIFIED) |
Реалізація onMeasure повинна:
setMeasuredDimension(int width, int height) для збереження виміряних розмірів.getPaddingLeft() + getPaddingRight() від доступної ширини.measureChild() або measureChildWithMargins().Типова помилка: не враховувати MeasureSpec при wrap_content. Якщо View встановлено як wrap_content, але onMeasure не обробляє AT_MOST і повертає фіксований розмір, View буде або обрізана, або займе більше місця, ніж потрібно.
onLayout — фаза, в якій View або ViewGroup розміщує свої нащадків в межах своїх границь. Для звичайного View (не ViewGroup) onLayout не потрібний — система сама викликає layout() з параметрами, переданими від батька. Для ViewGroup onLayout обов’язковий — без нього дочірні View не будуть розміщені.
Сигнатура onLayout:
@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).child.layout(l, t, r, b) для кожного нащадка.onLayout викликається після onMeasure — виміряні розміри доступні через getMeasuredWidth()/getMeasuredHeight(). Якщо дочірний View має інші фактичні розміри після layout(), викличеться requestLayout() для перевимірювання. Це називається «layout pass» і може викликати ланцюгову реакцію перерахунків.
onDraw — фаза, в якій View малює себе на Canvas. Це єдина фаза, яка може викликатися багаторазово без onMeasure та onLayout — якщо View позначена як invalidate(). Canvas надає API для малювання: drawLine, drawRect, drawCircle, drawText, drawBitmap та drawPath.
Правила onDraw:
canvas.clipRect() для відсічення невидимих частин.Порядок малювання в ViewGroup: фон (setBackgroundDrawable) → onDraw (вміст) → dispatchDraw (дочірні View) → onDrawForeground (передній план). dispatchDraw викликає onDraw кожного нащадка. Перевизначення dispatchDraw використовується для накладення ефектів поверх дочірних елементів.
За статистикою Android Vitals, найчастіші причини пропуску кадрів в onDraw — створення об’єктів всередині методу (48%), виклик decodeResource (22%) та складні операції з Path без кешування (15%).
Інвалідація — механізм, що запускає перемалювання 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 — потужний інструмент для створення унікального UI, але вони вимагають суворого дотримання правил продуктивності. Ось ключові рекомендації Google з оптимізації View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) для View з анімаціями та setLayerType(LAYER_TYPE_NONE) після завершення.Простий круговий індикатор прогресу з правильною реалізацією onMeasure, onDraw та invalidate.
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.
Кастомна ViewGroup, яка розміщує дочірні View у рядки (як Flexbox wrap).
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.
Кастомний View малює плавну криву Без’є, обчислюючи Path запопередньо та кешуючи його.
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 — циклічний процес малювання (onMeasure → onLayout → onDraw), не залежний від створення/знищення Activity. View не має onStart/onStop — він або видимий (приєднаний до вікна), або ні. Activity Lifecycle керує станом компонента програми, View Lifecycle — малюванням UI.
requestLayout() запускає повний цикл onMeasure → onLayout → onDraw для всього дерева View від кореня. Якщо requestLayout() викликається часто (наприклад, кожен кадр анімації), це викликає jank та пропуск кадрів. За даними Google, один requestLayout займає в середньому 2–5 мс на ViewGroup з 10 елементів. Для анімацій використовуйте invalidate().
onAttachedToWindow викликається, коли View приєднується до Window — стає частиною видимої ієрархії. У цей момент View отримує прискорення HW та доступ до ресурсів Window (WindowManager, Display). onAttachedToWindow — правильне місце для реєстрації слухачів анімацій та BroadcastReceiver, які живуть, поки View видимий.
Overdraw — ситуація, коли піксель малюється кілька разів за один кадр. Кожен зайвий прохід витрачає час GPU. Методи зменшення: встановіть windowBackground в темі (не малюйте фон в layout), використовуйте canvas.clipRect(), уникайте об’єднання вкладених фонів, використовуйте ConstraintLayout замість вкладеного LinearLayout. Android Studio → Profile GPU Rendering → Overdraw показує кольорову карту overdraw (синій = 1x, червоний = 3x+).
Так, якщо View має фон. super.onDraw() малює фон View. Якщо ваш кастомний View не має фону або ви малюєте власний фон, super.onDraw() можна не викликати — це заощадить один прохід малювання. Для ViewGroup super.dispatchDraw() обов’язковий — він малює дочірні View.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також