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 — 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:
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 — 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:
| Mod | Constantă | Semnificație | Exemplu |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Dimensiune exactă stabilită de părinte (match_parent sau lățime fixă) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | View poate avea dimensiunea până la maximul specificat (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Fără restricții — View poate avea orice dimensiune (ScrollView, RecyclerView) | lățime nelimitată → MeasureSpec(0, UNSPECIFIED) |
Implementarea onMeasure trebuie:
setMeasuredDimension(int width, int height) pentru a salva dimensiunile măsurate.getPaddingLeft() + getPaddingRight() din lățimea disponibilă.measureChild() sau measureChildWithMargins().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 — 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:
@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:
getChildCount() și getChildAt(i).child.layout(l, t, r, b) pentru fiecare descendent.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 — 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:
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 — 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 face | Când să utilizați |
|---|---|---|
| invalidate() | Apelează onDraw fără onMeasure/onLayout | S-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-UI | Firul de fundal a actualizat datele pentru desenare |
| requestLayout() | Declanșează onMeasure → onLayout → onDraw | S-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.
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.
setLayerType(LAYER_TYPE_HARDWARE) pentru View-urile cu animații și setLayerType(LAYER_TYPE_NONE) după finalizare.Indicator de progres circular simplu cu implementare corectă a onMeasure, onDraw și 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
}
}
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.
ViewGroup personalizată care aranjează View-urile copil în rânduri (ca 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 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.
View personalizat desenează o curbă Bezier netedă, calculând Path în avans și făcându-i cache.
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
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.
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().
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.
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+).
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
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.
Citiți și