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: фон, текст, фигуры, изображения.
  • Incorrect View Lifecycle — главная причина проблем с производительностью UI (jank, dropped frames) и иерархией.

View Lifecycle — что это в Android

View Lifecycle — это процесс, который Android View (и 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 acceleration) и 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, but 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() для переизмерения. Это called «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-потоке. Сложные расчёты должны быть вынесены в background thread или предварительно рассчитаны.
  • Используйте Hardware Acceleration — с API 14+ Canvas может работать через GPU. Для сложной графики (градиенты, тени, повороты) HW-ускорение даёт прирост производительности до 300%.
  • Рисуйте только видимую область — используйте canvas.clipRect() для отсечения невидимых частей.

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

По статистике Android Vitals, самые частые причины пропуска кадров в onDraw — создание объектов внутри метода (48%), вызов decodeResource (22%) и сложные операции с Path без кэширования (15%).

Invalidation: когда View перерисовывается

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: best practices

Кастомные View — мощный инструмент для создания уникального UI, но они требуют строгого соблюдения правил производительности. Вот ключевые рекомендации Google по оптимизации View Lifecycle.

  • Предварительно рассчитывайте всё, что можно рассчитать — размеры, координаты, путь Path, цвета градиента. В onDraw используйте только отрисовку.
  • Кэшируйте результат измерения — если View имеет фиксированные размеры, сохраните MeasureSpec и возвращайте setMeasuredDimension без дополнительных расчётов.
  • Используйте ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — для обработки касаний.
  • Минимизируйте количество View в иерархии — кастомные View, объединяющие несколько элементов, всегда быстрее, чем ViewGroup из 3–5 вложенных View. Google рекомендует не более 10 вложенных View в одном экране.
  • Используйте ConstraintLayout для плоской иерархии — он строит единственную ViewGroup с performance, близкой к RelativeLayout, но без вложенности.
  • Отключайте hardware layer при перерисовке — используйте 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 рисует фон и дугу прогресса. Invalidat вызывается при изменении прогресса — 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 для поддержки margin у дочерних 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 и dropped frames. По данным Google, одна requestLayout занимает в среднем 2–5 мс на ViewGroup из 10 элементов. Для анимаций используйте invalidate().

Когда вызывается onAttachedToWindow?

onAttachedToWindow вызывается, когда View присоединяется к окну (Window) — становится частью видимой иерархии. В этот момент View получает HW acceleration и доступ к ресурсам Window (WindowManager, Display). onAttachedToWindow — правильное место для регистрации слушателей анимаций и BroadcastReceiver, который живёт, пока View видим.

Что такое overdraw и как его уменьшить?

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

Нужен ли super.onDraw() в кастомном View?

Да, если View имеет фон (background). 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.
  • Для сложной графики используйте Hardware Acceleration и кэшируйте Path/Bitmap.

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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