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 — это процесс, который Android View (и ViewGroup) проходит для отображения себя на экране. В отличие от Activity или Fragment, View не имеет onStart/onStop/onDestroy — его «жизнь» состоит из циклического процесса измерения, размещения и отрисовки. Этот цикл запускается каждый раз, когда View необходимо отобразить или перерисовать.
Три фазы View Lifecycle:
Полный цикл View Lifecycle также включает методы, связанные с прикреплением View к окну: onAttachedToWindow (View привязана к окну, имеет HW acceleration) и 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, but 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() для переизмерения. Это called «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 (foreground). dispatchDraw вызывает onDraw каждого потомка. Переопределение dispatchDraw используется для наложения эффектов поверх дочерних элементов.
По статистике Android Vitals, самые частые причины пропуска кадров в onDraw — создание объектов внутри метода (48%), вызов decodeResource (22%) и сложные операции с Path без кэширования (15%).
Invalidation — механизм, который запускает перерисовку 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 меняет размер (width/height), автоматически вызывает requestLayout(). Это может быть дорого для сложных ViewGroup: каждая requestLayout запускает полную иерархию до корневого view.
Правило оптимизации: 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 рисует фон и дугу прогресса. Invalidat вызывается при изменении прогресса — 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 для поддержки margin у дочерних 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 и dropped frames. По данным Google, одна requestLayout занимает в среднем 2–5 мс на ViewGroup из 10 элементов. Для анимаций используйте invalidate().
onAttachedToWindow вызывается, когда View присоединяется к окну (Window) — становится частью видимой иерархии. В этот момент View получает HW acceleration и доступ к ресурсам Window (WindowManager, Display). onAttachedToWindow — правильное место для регистрации слушателей анимаций и BroadcastReceiver, который живёт, пока View видим.
Overdraw — ситуация, когда пиксель рисуется несколько раз за один кадр. Каждый лишний проход — потеря GPU-времени. Методы уменьшения: установите windowBackground в theme (не рисуйте фон в layout), используйте canvas.clipRect(), избегайте merge вложенных фонов, применяйте ConstraintLayout вместо вложенных LinearLayout. Android Studio → Profile GPU Rendering → Overdraw показывает цветовую карту overdraw (синий = 1x, красный = 3x+).
Да, если View имеет фон (background). super.onDraw() рисует фон View. Если ваша кастомная View не имеет фона или вы рисуете собственный фон, super.onDraw() можно не вызывать — это сэкономит один проход отрисовки. Для ViewGroup super.dispatchDraw() обязателен — он рисует дочерние View.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также