View Lifecycle — la sequenza di metodi che Android chiama per disegnare e ridisegnare un elemento dell’interfaccia utente (View) sullo schermo. A differenza di Activity o Fragment, View è un componente leggero che non ha un ciclo di vita esteso, ma attraversa un rigoroso processo trifasico: onMeasure (misurazione), onLayout (posizionamento), onDraw (disegno). Comprendere View Lifecycle è necessario per creare View personalizzate, ottimizzare le prestazioni e risolvere problemi di disegno. Secondo Google, le View personalizzate accelerano l’interfaccia utente del 15–40% rispetto a una combinazione di ViewGroups annidati standard quando implementate correttamente. La documentazione Android sulle View personalizzate descrive onMeasure, onLayout e onDraw come i tre pilastri di View Lifecycle.
Punti chiave
View Lifecycle è il processo che una View Android (e ViewGroup) attraversa per visualizzarsi sullo schermo. A differenza di Activity o Fragment, View non ha onStart/onStop/onDestroy — la sua “vita” consiste in un processo ciclico di misurazione, posizionamento e disegno. Questo ciclo viene attivato ogni volta che una View deve essere visualizzata o ridisegnata.
Le tre fasi di View Lifecycle:
Il ciclo completo di View Lifecycle include anche metodi relativi all’attaccamento di una View a una finestra: onAttachedToWindow (la View è attaccata a una finestra, ha accelerazione HW) e onDetachedFromWindow (la View è staccata, le risorse vengono liberate). Questi metodi vengono chiamati una volta per vita della View e sono importanti per registrare/cancellare animazioni e sensori.
Secondo Android Performance Blog, 65% dei problemi di prestazioni dell’interfaccia utente (jank, frame persi) sono correlati a un’implementazione errata di onMeasure e onDraw: override eccessivo, chiamata non necessaria di requestLayout(), creazione di oggetti in onDraw.
onMeasure — la fase più importante e più complessa di View Lifecycle. In questa fase, Android determina quanto spazio la View occuperà sullo schermo. Il sistema passa MeasureSpec — istruzioni impacchettate in int costituite da una modalità e una dimensione.
Le tre modalità di MeasureSpec:
| Modalità | Costante | Significato | Esempio |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Dimensione esatta impostata dal genitore (match_parent o larghezza fissa) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | La View può avere fino alla dimensione massima specificata (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Senza restrizioni — la View può avere qualsiasi dimensione (ScrollView, RecyclerView) | larghezza illimitata → MeasureSpec(0, UNSPECIFIED) |
L’implementazione di onMeasure deve:
setMeasuredDimension(int width, int height) per salvare le dimensioni misurate.getPaddingLeft() + getPaddingRight() dalla larghezza disponibile.measureChild() o measureChildWithMargins().Errore tipico: non considerare MeasureSpec quando si usa wrap_content. Se una View è impostata su wrap_content, ma onMeasure non gestisce AT_MOST e restituisce una dimensione fissa, la View verrà ritagliata o occuperà più spazio del necessario.
onLayout — la fase in cui una View o ViewGroup dispone i suoi figli all’interno dei propri confini. Per una View normale (non ViewGroup), onLayout non è richiesto — il sistema chiama layout() con i parametri passati dal genitore. Per una ViewGroup, onLayout è obbligatorio — senza di esso, le View figlie non verranno posizionate.
Firma di onLayout:
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// disposizione delle View figlie
}
Il parametro changed indica se la posizione o la dimensione della View è cambiata rispetto al layout precedente. Se false, la View può saltare il ricalcolo delle posizioni dei figli per ottimizzare.
Per ViewGroup, onLayout deve:
getChildCount() e getChildAt(i).child.layout(l, t, r, b) per ogni figlio.onLayout viene chiamato dopo onMeasure — le dimensioni misurate sono disponibili tramite getMeasuredWidth()/getMeasuredHeight(). Se una View figlia ha dimensioni effettive diverse dopo layout(), viene chiamato requestLayout() per rimisurare. Questo è chiamato “passaggio di layout” e può innescare una reazione a catena di ricalcoli.
onDraw — la fase in cui una View si disegna sul Canvas. Questa è l’unica fase che può essere chiamata più volte senza onMeasure e onLayout — se la View è marcata come invalidate(). Canvas fornisce API di disegno: drawLine, drawRect, drawCircle, drawText, drawBitmap e drawPath.
Regole di onDraw:
canvas.clipRect() per ritagliare le parti invisibili.Ordine di disegno in ViewGroup: sfondo (setBackgroundDrawable) → onDraw (contenuto) → dispatchDraw (View figlie) → onDrawForeground (primo piano). dispatchDraw chiama onDraw di ogni figlio. L’override di dispatchDraw viene utilizzato per applicare effetti sopra gli elementi figli.
Secondo le statistiche di Android Vitals, le cause più comuni di frame persi in onDraw sono la creazione di oggetti all’interno del metodo (48%), la chiamata a decodeResource (22%) e le operazioni complesse su Path senza caching (15%).
Invalidazione — il meccanismo che innesca il ridisegno della View. Chiamare invalidate() marca la View come “sporca” e programma la chiamata di onDraw nel prossimo ciclo di disegno. Chiamare requestLayout() è un’operazione più “pesante”, che innesca il ciclo completo: onMeasure → onLayout → onDraw.
| Metodo | Cosa fa | Quando usarlo |
|---|---|---|
| invalidate() | Innesca onDraw senza onMeasure/onLayout | Solo l’aspetto è cambiato (colore, testo, progresso) |
| invalidate(Rect) | Ridisegna solo l’area specificata | Parte della View è cambiata — animazione, selezione |
| postInvalidate() | Chiama invalidate da un thread non UI | Thread in background ha aggiornato i dati per il disegno |
| requestLayout() | Innesca onMeasure → onLayout → onDraw | La dimensione del contenuto è cambiata (testo, immagine) |
| forceLayout() | Marca la View per rimisurazione forzata | Lo stato interno è cambiato, la dimensione potrebbe essere cambiata |
Animazioni e View Lifecycle: ViewPropertyAnimator e ValueAnimator chiamano invalidate() su ogni frame di animazione. ObjectAnimator chiama un setter sulla View, che se il setter modifica la dimensione (larghezza/altezza), chiama automaticamente requestLayout(). Questo può essere costoso per ViewGroups complesse: ogni requestLayout innesca l’intera gerarchia fino alla vista radice.
Regola di ottimizzazione: invalidate() invece di requestLayout() ovunque cambi solo l’aspetto (colore, trasparenza, rotazione senza cambiamento di dimensione). Utilizzare requestLayout solo quando si modificano dimensioni o contenuti che influenzano la dimensione.
Le View personalizzate sono uno strumento potente per creare interfacce utente uniche, ma richiedono il rigoroso rispetto delle regole di prestazione. Ecco le principali raccomandazioni di Google per l’ottimizzazione di View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) per View con animazioni e setLayerType(LAYER_TYPE_NONE) dopo il completamento.Un semplice indicatore di progresso circolare con corretta implementazione di onMeasure, onDraw e 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
}
}
Barra di progresso circolare: onMeasure restituisce una dimensione quadrata basata su MeasureSpec, onSizeChanged ricorda le dimensioni, onDraw disegna lo sfondo e l’arco di progresso. Invalidate viene chiamato quando il progresso cambia — onMeasure/onLayout non vengono influenzati. Paint viene creato una volta nel costruttore, non in onDraw.
Una ViewGroup personalizzata che dispone le View figlie in righe (come 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 esegue l’override di onMeasure: misura ogni figlio, passa a una nuova riga quando la larghezza viene superata, calcola l’altezza totale. onLayout posiziona i figli per coordinate considerando le interruzioni di riga. generateLayoutParams restituisce MarginLayoutParams per supportare i margini sulle View figlie.
Una View personalizzata disegna una curva di Bezier morbida, pre-calcolando il Path e memorizzandolo nella 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)
}
}
Caching di Path: isPathDirty = true solo quando le dimensioni della View cambiano o viene chiamato refreshWave(). In onDraw, Path viene ricalcolato solo se è “sporco”. Ciò impedisce il ricalcolo della curva di Bezier ad ogni frame di animazione, risparmiando CPU.
Domande frequenti
View Lifecycle è un processo di disegno ciclico (onMeasure → onLayout → onDraw) indipendente dalla creazione/distruzione di Activity. View non ha onStart/onStop — o è visibile (attaccata a una finestra) o non lo è. Activity Lifecycle gestisce lo stato del componente dell’applicazione, View Lifecycle gestisce il disegno dell’interfaccia utente.
requestLayout() innesca il ciclo completo onMeasure → onLayout → onDraw per l’intero albero delle View dalla radice. Se requestLayout() viene chiamato frequentemente (ad esempio, ad ogni frame di animazione), causa jank e frame persi. Secondo Google, un requestLayout richiede in media 2–5 ms su una ViewGroup di 10 elementi. Per le animazioni, utilizzare invalidate().
onAttachedToWindow viene chiamato quando una View viene attaccata a una Window — diventa parte della gerarchia visibile. In questo momento, la View riceve l’accelerazione HW e l’accesso alle risorse della Window (WindowManager, Display). onAttachedToWindow è il luogo appropriato per registrare listener di animazioni e BroadcastReceiver che vivono finché la View è visibile.
L’overdraw è una situazione in cui un pixel viene disegnato più volte in un singolo frame. Ogni passaggio extra spreca tempo GPU. Metodi di riduzione: impostare windowBackground nel tema (non disegnare lo sfondo nel layout), utilizzare canvas.clipRect(), evitare di unire sfondi annidati, utilizzare ConstraintLayout invece di LinearLayout annidato. Android Studio → Profile GPU Rendering → Overdraw mostra una mappa a colori dell’overdraw (blu = 1x, rosso = 3x+).
Sì, se la View ha uno sfondo. super.onDraw() disegna lo sfondo della View. Se la tua View personalizzata non ha sfondo o disegni il tuo sfondo, super.onDraw() può essere omesso — questo risparmia un passaggio di disegno. Per ViewGroup, super.dispatchDraw() è obbligatorio — disegna le View figlie.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche