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 — процесът, през който 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, но 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, полета, подравняване.

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 нишката. Сложните изчисления трябва да бъдат преместени във фонова нишка или предварително изчислени.
  • Използвайте Hardware Acceleration — от 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%).

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: най-добри практики

Персонализираните 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 ms на ViewGroup с 10 елемента. За анимации използвайте invalidate().

Кога се извиква onAttachedToWindow?

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

Какво е overdraw и как да го намалим?

Overdraw — ситуация, при която пиксел се рисува няколко пъти в един кадър. Всяко допълнително преминаване е загуба на GPU време. Методи за намаляване: задайте windowBackground в theme (не рисувайте фон в layout), използвайте canvas.clipRect(), избягвайте сливане на вложени фонове, приложете 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 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също