View Lifecycle — la séquence de méthodes qu’Android appelle pour dessiner et redessiner un élément d’interface utilisateur (View) à l’écran. Contrairement à Activity ou Fragment, View est un composant léger qui n’a pas de cycle de vie étendu, mais passe par un processus strict en trois phases : onMeasure (mesure), onLayout (positionnement), onDraw (dessin). Comprendre View Lifecycle est nécessaire pour créer des Views personnalisées, optimiser les performances et résoudre les problèmes de dessin. Selon Google, les Views personnalisées accélèrent l’interface utilisateur de 15 à 40% par rapport à une combinaison de ViewGroups imbriqués standard lorsqu’elles sont correctement implémentées. La documentation Android sur les Views personnalisées décrit onMeasure, onLayout et onDraw comme les trois piliers de View Lifecycle.
Points clés
View Lifecycle est le processus qu’une View Android (et ViewGroup) suit pour s’afficher à l’écran. Contrairement à Activity ou Fragment, View n’a pas de onStart/onStop/onDestroy — sa « vie » consiste en un processus cyclique de mesure, de positionnement et de dessin. Ce cycle est déclenché chaque fois qu’une View doit être affichée ou redessinée.
Les trois phases de View Lifecycle :
Le cycle complet de View Lifecycle comprend également des méthodes liées à l’attachement d’une View à une fenêtre : onAttachedToWindow (la View est attachée à une fenêtre, dispose d’une accélération HW) et onDetachedFromWindow (la View est détachée, les ressources sont libérées). Ces méthodes sont appelées une fois par durée de vie de la View et sont importantes pour enregistrer/annuler des animations et des capteurs.
Selon le blog Android Performance, 65% des problèmes de performance de l’interface utilisateur (jank, images perdues) sont liés à une implémentation incorrecte de onMeasure et onDraw : remplacement excessif, appel inutile de requestLayout(), création d’objets dans onDraw.
onMeasure — la phase la plus importante et la plus complexe de View Lifecycle. À cette étape, Android détermine l’espace qu’occupera la View à l’écran. Le système transmet MeasureSpec — des instructions emballées dans un int, composées d’un mode et d’une taille.
Les trois modes de MeasureSpec :
| Mode | Constante | Signification | Exemple |
|---|---|---|---|
| EXACTLY | MeasureSpec.EXACTLY | Taille exacte définie par le parent (match_parent ou largeur fixe) | width=400dp → MeasureSpec(400, EXACTLY) |
| AT_MOST | MeasureSpec.AT_MOST | La View peut avoir jusqu’à la taille maximale spécifiée (wrap_content) | width ≤ 400dp → MeasureSpec(400, AT_MOST) |
| UNSPECIFIED | MeasureSpec.UNSPECIFIED | Aucune restriction — la View peut avoir n’importe quelle taille (ScrollView, RecyclerView) | largeur illimitée → MeasureSpec(0, UNSPECIFIED) |
L’implémentation de onMeasure doit :
setMeasuredDimension(int width, int height) pour enregistrer les dimensions mesurées.getPaddingLeft() + getPaddingRight() de la largeur disponible.measureChild() ou measureChildWithMargins().Erreur typique : ne pas tenir compte de MeasureSpec lors de l’utilisation de wrap_content. Si une View est définie sur wrap_content, mais que onMeasure ne gère pas AT_MOST et retourne une taille fixe, la View sera soit coupée, soit prendra plus de place que nécessaire.
onLayout — la phase dans laquelle une View ou ViewGroup dispose ses enfants à l’intérieur de ses limites. Pour une View normale (pas une ViewGroup), onLayout n’est pas requis — le système appelle layout() avec les paramètres transmis par le parent. Pour une ViewGroup, onLayout est obligatoire — sans lui, les Views enfants ne seront pas placées.
Signature de onLayout :
@Override
protected void onLayout(boolean changed,
int left, int top,
int right, int bottom) {
// disposition des Views enfants
}
Le paramètre changed indique si la position ou la taille de la View a changé par rapport au layout précédent. Si false, la View peut ignorer le recalcul des positions des enfants pour l’optimisation.
Pour ViewGroup, onLayout doit :
getChildCount() et getChildAt(i).child.layout(l, t, r, b) pour chaque enfant.onLayout est appelé après onMeasure — les dimensions mesurées sont disponibles via getMeasuredWidth()/getMeasuredHeight(). Si une View enfant a des dimensions réelles différentes après layout(), requestLayout() sera appelé pour remesurer. Cela s’appelle un « passage de layout » et peut déclencher une réaction en chaîne de recalculs.
onDraw — la phase dans laquelle une View se dessine sur Canvas. C’est la seule phase qui peut être appelée plusieurs fois sans onMeasure et onLayout — si la View est marquée comme invalidate(). Canvas fournit une API de dessin : drawLine, drawRect, drawCircle, drawText, drawBitmap et drawPath.
Règles de onDraw :
canvas.clipRect() pour masquer les parties invisibles.Ordre de dessin dans ViewGroup : arrière-plan (setBackgroundDrawable) → onDraw (contenu) → dispatchDraw (Views enfants) → onDrawForeground (premier plan). dispatchDraw appelle onDraw de chaque enfant. La redéfinition de dispatchDraw est utilisée pour appliquer des effets au-dessus des éléments enfants.
Selon les statistiques d’Android Vitals, les causes les plus fréquentes de perte d’images dans onDraw sont la création d’objets dans la méthode (48%), l’appel de decodeResource (22%) et les opérations complexes sur Path sans mise en cache (15%).
Invalidation — le mécanisme qui déclenche le redessin de la View. L’appel de invalidate() marque la View comme « sale » et planifie l’appel de onDraw dans le prochain cycle de dessin. L’appel de requestLayout() est une opération plus « lourde », déclenchant le cycle complet : onMeasure → onLayout → onDraw.
| Méthode | Ce qu’elle fait | Quand l’utiliser |
|---|---|---|
| invalidate() | Déclenche onDraw sans onMeasure/onLayout | Seule l’apparence a changé (couleur, texte, progression) |
| invalidate(Rect) | Redessine uniquement la zone spécifiée | Une partie de la View a changé — animation, sélection |
| postInvalidate() | Appelle invalidate depuis un thread non-UI | Un thread d’arrière-plan a mis à jour les données de dessin |
| requestLayout() | Déclenche onMeasure → onLayout → onDraw | La taille du contenu a changé (texte, image) |
| forceLayout() | Marque la View pour remesure forcée | L’état interne a changé, la taille a pu changer |
Animations et View Lifecycle : ViewPropertyAnimator et ValueAnimator appellent invalidate() à chaque frame d’animation. ObjectAnimator appelle un setter sur la View, qui, si le setter modifie la taille (largeur/hauteur), appelle automatiquement requestLayout(). Cela peut être coûteux pour les ViewGroups complexes : chaque requestLayout déclenche la hiérarchie complète jusqu’à la vue racine.
Règle d’optimisation : invalidate() au lieu de requestLayout() partout où seule l’apparence change (couleur, transparence, rotation sans changement de taille). Utilisez requestLayout uniquement lors du changement de tailles ou de contenu qui affecte la taille.
Les Views personnalisées sont un outil puissant pour créer une interface utilisateur unique, mais elles nécessitent le respect strict des règles de performance. Voici les principales recommandations de Google pour l’optimisation de View Lifecycle.
setLayerType(LAYER_TYPE_HARDWARE) pour les Views avec animations et setLayerType(LAYER_TYPE_NONE) après la fin.Un indicateur de progression circulaire simple avec une implémentation correcte de onMeasure, onDraw et 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
}
}
Barre de progression circulaire : onMeasure retourne une taille carrée basée sur MeasureSpec, onSizeChanged mémorise les dimensions, onDraw dessine l’arrière-plan et l’arc de progression. Invalidate est appelé lorsque la progression change — onMeasure/onLayout ne sont pas affectés. Paint est créé une fois dans le constructeur, pas dans onDraw.
Une ViewGroup personnalisée qui dispose les Views enfants en lignes (comme 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 redéfinit onMeasure : mesure chaque enfant, passe à une nouvelle ligne lorsque la largeur est dépassée, calcule la hauteur totale. onLayout positionne les enfants par coordonnées en tenant compte des sauts de ligne. generateLayoutParams retourne MarginLayoutParams pour prendre en charge les marges sur les Views enfants.
Une View personnalisée dessine une courbe de Bezier lisse, en précalculant le Path et en le mettant en 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)
}
}
Mise en cache de Path : isPathDirty = true uniquement lorsque les dimensions de la View changent ou que refreshWave() est appelé. Dans onDraw, le Path n’est recalculé que s’il est « sale ». Cela évite de recalculer la courbe de Bezier à chaque frame d’animation, économisant le CPU.
Foire aux questions
View Lifecycle est un processus de dessin cyclique (onMeasure → onLayout → onDraw) indépendant de la création/destruction d’Activity. View n’a pas de onStart/onStop — elle est soit visible (attachée à une fenêtre), soit non. Activity Lifecycle gère l’état du composant d’application, View Lifecycle gère le dessin de l’interface utilisateur.
requestLayout() déclenche le cycle complet onMeasure → onLayout → onDraw pour toute l’arborescence des Views depuis la racine. Si requestLayout() est appelé fréquemment (par exemple, à chaque frame d’animation), cela provoque du jank et des images perdues. Selon Google, un requestLayout prend en moyenne 2–5 ms sur une ViewGroup de 10 éléments. Pour les animations, utilisez invalidate().
onAttachedToWindow est appelé lorsqu’une View est attachée à une Window — elle devient partie de la hiérarchie visible. À ce moment, la View reçoit l’accélération HW et l’accès aux ressources de la Window (WindowManager, Display). onAttachedToWindow est l’endroit approprié pour enregistrer des écouteurs d’animation et des BroadcastReceivers qui vivent tant que la View est visible.
Le overdraw est une situation où un pixel est dessiné plusieurs fois dans une seule trame. Chaque passage supplémentaire gaspille du temps GPU. Méthodes de réduction : définir windowBackground dans le thème (ne pas dessiner d’arrière-plan dans le layout), utiliser canvas.clipRect(), éviter de fusionner les arrière-plans imbriqués, utiliser ConstraintLayout au lieu de LinearLayout imbriqué. Android Studio → Profile GPU Rendering → Overdraw montre une carte des couleurs du overdraw (bleu = 1x, rouge = 3x+).
Oui, si la View a un arrière-plan. super.onDraw() dessine l’arrière-plan de la View. Si votre View personnalisée n’a pas d’arrière-plan ou si vous dessinez votre propre arrière-plan, super.onDraw() peut être omis — cela économise un passage de dessin. Pour ViewGroup, super.dispatchDraw() est obligatoire — il dessine les Views enfants.
Résumé
Nous développerons une application mobile clé en main
IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.
Lisez aussi