View Lifecycle — ce este, procesele onMeasure onLayout onDraw

Autor: IT Sectr Publicat: 2026-03-05 Timp de citire: 10 min

View Lifecycle — secvența de metode pe care Android le apelează pentru redarea și re-redarea elementului de interfață (View) pe ecran. Spre deosebire de Activity sau Fragment, View este o componentă ușoară care nu are un ciclu de viață extins, dar trece printr-un proces strict în trei faze: onMeasure (măsurare), onLayout (aranjare), onDraw (desenare). Înțelegerea View Lifecycle este necesară pentru crearea View-urilor personalizate, optimizarea performanței și rezolvarea problemelor de redare. Conform Google, View-urile personalizate accelerează UI cu 15–40% comparativ cu combinația de ViewGroup-uri standard imbricate atunci când sunt implementate corect. Documentația Android despre View-urile personalizate descrie onMeasure, onLayout și onDraw ca cei trei stâlpi ai View Lifecycle.

Principalele puncte

  • View Lifecycle constă din trei faze: onMeasure (dimensiuni), onLayout (poziții), onDraw (desenare) — și este declanșat la invalidate() sau requestLayout().
  • onMeasure calculează lățimea și înălțimea View pe baza MeasureSpec (AT_MOST, EXACTLY, UNSPECIFIED).
  • onLayout aranjează View-urile copil în interiorul ViewGroup, determinând coordonatele lor left, top, right, bottom.
  • onDraw redă conținutul View pe Canvas: fundal, text, forme, imagini.
  • View Lifecycle incorect — principala cauză a problemelor de performanță UI (jank, dropped frames) și ierarhie.

View Lifecycle — ce este în Android

View Lifecycle — este procesul prin care View-ul Android (și ViewGroup) trece pentru a se afișa pe ecran. Spre deosebire de Activity sau Fragment, View nu are onStart/onStop/onDestroy — „viața” sa constă dintr-un proces ciclic de măsurare, aranjare și desenare. Acest ciclu este declanșat de fiecare dată când View trebuie afișat sau re-redat.

Trei faze ale View Lifecycle:

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — determină dimensiunile dorite ale View. Sistemul transmite MeasureSpec — o instrucțiune despre dimensiunile permise (valoare exactă, maxim sau fără restricții).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — aranjează View și descendenții săi pe ecran. Pentru View își stabilește propriile limite, pentru ViewGroup — pozițiile elementelor copil.
  • onDraw(Canvas canvas) — desenează conținutul View pe Canvas-ul primit. Sistemul furnizează Canvas care traduce comenzile în bitmap sau GPU.

Ciclul complet al View Lifecycle include și metode legate de atașarea View la fereastră: onAttachedToWindow (View este atașat la fereastră, are HW acceleration) și onDetachedFromWindow (View este detașat, resursele sunt eliberate). Aceste metode sunt apelate o dată în viața View și sunt importante pentru înregistrarea/anularea animațiilor, senzorilor.

Conform Android Performance Blog, 65% dintre problemele de performanță UI (jank, omiterea cadrelor) sunt legate de implementarea incorectă a onMeasure și onDraw: suprascriere excesivă, apelarea requestLayout() fără necesitate, crearea de obiecte în onDraw.

onMeasure: măsurarea dimensiunilor View

onMeasure — cea mai importantă și cea mai complexă fază a View Lifecycle. În această etapă Android determină cât spațiu va ocupa View pe ecran. Sistemul transmite MeasureSpec — instrucțiuni împachetate în int, constând din mod și dimensiune.

Trei moduri MeasureSpec:

ModConstantăSemnificațieExemplu
EXACTLYMeasureSpec.EXACTLYDimensiune exactă stabilită de părinte (match_parent sau lățime fixă)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTView poate avea dimensiunea până la maximul specificat (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDFără restricții — View poate avea orice dimensiune (ScrollView, RecyclerView)lățime nelimitată → MeasureSpec(0, UNSPECIFIED)

Implementarea onMeasure trebuie:

  • Să apeleze setMeasuredDimension(int width, int height) pentru a salva dimensiunile măsurate.
  • Să ia în considerare padding — să scadă getPaddingLeft() + getPaddingRight() din lățimea disponibilă.
  • Pentru ViewGroup — să măsoare toți descendenții prin measureChild() sau measureChildWithMargins().
  • Pentru wrap_content — să calculeze dimensiunea după conținut (text, imagine).
  • Să nu apeleze requestLayout() în interiorul onMeasure — aceasta va cauza o buclă infinită.

Eroare tipică: neincluderea MeasureSpec la wrap_content. Dacă View este setat la wrap_content, dar onMeasure nu procesează AT_MOST și returnează o dimensiune fixă, View va fi fie tăiat, fie va ocupa mai mult spațiu decât este necesar.

onLayout: aranjarea View pe ecran

onLayout — faza în care View sau ViewGroup își aranjează descendenții în interiorul limitelor sale. Pentru un View obișnuit (nu ViewGroup) onLayout nu este necesar — sistemul însuși apelează layout() cu parametrii primiți de la părinte. Pentru ViewGroup onLayout este obligatoriu — fără el, View-urile copil nu vor fi aranjate.

Semnătura onLayout:

java
@Override
protected void onLayout(boolean changed,
        int left, int top,
        int right, int bottom) {
    // aranjarea View-urilor copil
}

Parametrul changed indică dacă poziția sau dimensiunea View s-a schimbat față de layout-ul anterior. Dacă false — View poate omite recalcularea pozițiilor descendenților pentru optimizare.

Pentru ViewGroup onLayout trebuie:

  • Să parcurgă toți descendenții prin getChildCount() și getChildAt(i).
  • Pentru fiecare descendent să determine left, top, right, bottom — coordonatele în interiorul ViewGroup (cu padding inclus).
  • Să apeleze child.layout(l, t, r, b) pentru fiecare descendent.
  • Să ia în considerare gravity, margini, aliniere.

onLayout este apelat după onMeasure — dimensiunile măsurate sunt disponibile prin getMeasuredWidth()/getMeasuredHeight(). Dacă un View copil are dimensiuni reale diferite după layout(), se va apela requestLayout() pentru remăsurare. Aceasta se numește „layout pass” și poate provoca o reacție în lanț de recalculări.

onDraw: desenarea conținutului Canvas

onDraw — faza în care View se desenează pe Canvas. Este singura fază care poate fi apelată de mai multe ori fără onMeasure și onLayout — dacă View este marcat ca invalidate(). Canvas oferă API pentru desenare: drawLine, drawRect, drawCircle, drawText, drawBitmap și drawPath.

Reguli onDraw:

  • Nu creați obiecte în onDraw — fiecare apel onDraw trebuie să folosească obiecte create în prealabil (Path, Paint, Rect). Crearea de obiecte în onDraw cauzează pauze GC și cadre omise.
  • Nu apelați requestLayout() sau invalidate() în interiorul onDraw — aceasta va declanșa o buclă infinită de re-redare.
  • Nu faceți calcule lungi — onDraw se execută pe firul UI. Calculele complexe trebuie mutate în firul de fundal sau calculate în prealabil.
  • Utilizați Hardware Acceleration — de la API 14+ Canvas poate funcționa prin GPU. Pentru grafică complexă (gradiente, umbre, rotații) accelerarea HW oferă o creștere a performanței de până la 300%.
  • Desenați doar zona vizibilă — folosiți canvas.clipRect() pentru a tăia părțile invizibile.

Ordinea desenării în ViewGroup: fundal (setBackgroundDrawable) → onDraw (conținut) → dispatchDraw (View-uri copil) → onDrawForeground (prim-plan). dispatchDraw apelează onDraw fiecărui copil. Suprascrierea dispatchDraw este utilizată pentru aplicarea efectelor peste elementele copil.

Conform statisticilor Android Vitals, cele mai frecvente cauze ale omiterii cadrelor în onDraw — crearea de obiecte în interiorul metodei (48%), apelarea decodeResource (22%) și operații complexe cu Path fără cache (15%).

Invalidation: când View este re-redat

Invalidation — mecanismul care declanșează re-redarea View. Apelul invalidate() marchează View ca „deteriorat” și planifică apelul onDraw în următorul ciclu de desenare. Apelul requestLayout() — o operație mai „grea”, care declanșează ciclul complet: onMeasure → onLayout → onDraw.

MetodăCe faceCând să utilizați
invalidate()Apelează onDraw fără onMeasure/onLayoutS-a schimbat doar aspectul (culoare, text, progres)
invalidate(Rect)Re-redă doar zona specificatăS-a schimbat o parte a View — animație, selecție
postInvalidate()Apelează invalidate dintr-un fir non-UIFirul de fundal a actualizat datele pentru desenare
requestLayout()Declanșează onMeasure → onLayout → onDrawS-a schimbat dimensiunea conținutului (text, imagine)
forceLayout()Marchează View pentru remăsurare forțatăStarea internă s-a schimbat, dimensiunea s-ar putea fi modificat

Animațiile și View Lifecycle: ViewPropertyAnimator și ValueAnimator apelează invalidate() la fiecare cadru al animației. ObjectAnimator apelează setter-ul pe View care, dacă setter-ul modifică dimensiunea (width/height), apelează automat requestLayout(). Aceasta poate fi costisitor pentru ViewGroup-uri complexe: fiecare requestLayout declanșează ierarhia completă până la view-ul rădăcină.

Regula de optimizare: invalidate() în loc de requestLayout() oriunde se schimbă doar aspectul (culoare, transparență, rotație fără modificarea dimensiunii). Utilizați requestLayout doar la modificarea dimensiunilor sau a conținutului care afectează dimensiunea.

Optimizarea View-urilor personalizate: cele mai bune practici

View-urile personalizate — un instrument puternic pentru crearea unui UI unic, dar necesită respectarea strictă a regulilor de performanță. Iată recomandările cheie Google pentru optimizarea View Lifecycle.

  • Calculați în avans tot ce poate fi calculat — dimensiuni, coordonate, calea Path, culorile gradientului. În onDraw folosiți doar desenarea.
  • Faceți cache la rezultatul măsurării — dacă View are dimensiuni fixe, salvați MeasureSpec și returnați setMeasuredDimension fără calcule suplimentare.
  • Utilizați ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — pentru gestionarea atingerilor.
  • Minimizați numărul de View-uri în ierarhie — View-urile personalizate care combină mai multe elemente sunt întotdeauna mai rapide decât un ViewGroup cu 3–5 View-uri imbricate. Google recomandă nu mai mult de 10 View-uri imbricate pe un ecran.
  • Utilizați ConstraintLayout pentru o ierarhie plată — construiește un singur ViewGroup cu performanță apropiată de RelativeLayout, dar fără imbricare.
  • Dezactivați stratul hardware la re-redare — utilizați setLayerType(LAYER_TYPE_HARDWARE) pentru View-urile cu animații și setLayerType(LAYER_TYPE_NONE) după finalizare.
  • Utilizați invalidate() cu Rect — re-redați doar zona modificată, nu întregul View.
  • Evitați overdraw — utilizați Profile GPU Rendering în Android Studio pentru a depista redările inutile. Overdraw-ul mediu pentru aplicațiile Google — 1.5x, maximul recomandat — 2.5x.

Exemple de cod View în Kotlin

Exemplul 1: View personalizat — indicator de progres

Indicator de progres circular simplu cu implementare corectă a onMeasure, onDraw și 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
    }
}

Bara de progres circulară: onMeasure returnează dimensiunea pătrată pe baza MeasureSpec, onSizeChanged memorează dimensiunile, onDraw desenează fundalul și arcul de progres. Invalidate este apelat la modificarea progresului — onMeasure/onLayout nu sunt afectate. Paint este creat o dată în constructor, nu în onDraw.

Exemplul 2: ViewGroup — un FlowLayout simplu

ViewGroup personalizată care aranjează View-urile copil în rânduri (ca 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 suprascrie onMeasure: măsoară fiecare copil, trece pe un rând nou la depășirea lățimii, calculează înălțimea totală. onLayout aranjează copiii după coordonate ținând cont de trecerile pe rânduri. generateLayoutParams returnează MarginLayoutParams pentru suportul marginilor la View-urile copil.

Exemplul 3: onDraw cu cache Path

View personalizat desenează o curbă Bezier netedă, calculând Path în avans și făcându-i cache.

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)
    }
}

Cache Path: isPathDirty = true doar la modificarea dimensiunilor View sau la apelarea refreshWave(). În onDraw Path este recalculat doar dacă este „deteriorat”. Aceasta previne recalcularea curbei Bezier la fiecare cadru de animație, economisind CPU.

Întrebări frecvente

Cu ce se deosebește View Lifecycle de Activity Lifecycle?

View Lifecycle — un proces ciclic de desenare (onMeasure → onLayout → onDraw), independent de crearea/distrugerea Activity. View nu are onStart/onStop — fie este vizibil (atașat la fereastră), fie nu. Activity Lifecycle gestionează starea componentului aplicației, View Lifecycle — desenarea UI.

Ce efect are requestLayout() asupra performanței?

requestLayout() declanșează ciclul complet onMeasure → onLayout → onDraw pentru întregul arbore View de la rădăcină. Dacă requestLayout() este apelat frecvent (de exemplu, la fiecare cadru de animație), aceasta cauzează jank și cadre omise. Conform Google, un requestLayout durează în medie 2–5 ms pe un ViewGroup cu 10 elemente. Pentru animații utilizați invalidate().

Când este apelat onAttachedToWindow?

onAttachedToWindow este apelat când View se atașează la fereastră (Window) — devine parte a ierarhiei vizibile. În acest moment View primește accelerare hardware și acces la resursele Window (WindowManager, Display). onAttachedToWindow este locul potrivit pentru înregistrarea ascultătorilor de animații și a BroadcastReceiver-ului care trăiește cât timp View este vizibil.

Ce este overdraw și cum se reduce?

Overdraw — situația în care un pixel este desenat de mai multe ori într-un singur cadru. Fiecare trecere suplimentară este o pierdere de timp GPU. Metode de reducere: setați windowBackground în theme (nu desenați fundal în layout), folosiți canvas.clipRect(), evitați îmbinarea fundalurilor imbricate, aplicați ConstraintLayout în loc de LinearLayout-uri imbricate. Android Studio → Profile GPU Rendering → Overdraw arată harta colorată a overdraw-ului (albastru = 1x, roșu = 3x+).

Este necesar super.onDraw() într-un View personalizat?

Da, dacă View are fundal (background). super.onDraw() desenează fundalul View. Dacă View-ul personalizat nu are fundal sau desenați propriul fundal, super.onDraw() poate fi omis — aceasta economisește o trecere de desenare. Pentru ViewGroup super.dispatchDraw() este obligatoriu — desenează View-urile copil.

Concluzii

  • View Lifecycle — trei faze de desenare: onMeasure (dimensiuni), onLayout (poziție), onDraw (desenare).
  • onMeasure procesează MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) și apelează setMeasuredDimension.
  • onLayout în ViewGroup aranjează View-urile copil cu coordonatele left/top/right/bottom.
  • onDraw desenează conținutul pe Canvas — nu creați obiecte în interiorul acestei metode.
  • invalidate() apelează doar onDraw, requestLayout() — ciclul complet onMeasure → onLayout → onDraw.
  • View-urile personalizate accelerează UI cu 15–40%, dar necesită implementarea corectă a onMeasure și cache-ul obiectelor în onDraw.
  • Pentru grafică complexă utilizați Hardware Acceleration și faceți cache la Path/Bitmap.

Vom dezvolta o aplicație mobilă la cheie

IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.

Discutați proiectul

Citiți și