View Lifecycle — qu’est-ce que c’est, processus onMeasure onLayout onDraw

Auteur : IT Sectr Publié le : 2026-03-05 Temps de lecture : 10 min

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 se compose de trois phases : onMeasure (tailles), onLayout (positions), onDraw (dessin) — et est déclenché par invalidate() ou requestLayout().
  • onMeasure calcule la largeur et la hauteur d’une View en fonction de MeasureSpec (AT_MOST, EXACTLY, UNSPECIFIED).
  • onLayout dispose les Views enfants à l’intérieur d’une ViewGroup, en déterminant leurs coordonnées left, top, right, bottom.
  • onDraw restitue le contenu de la View sur Canvas : arrière-plan, texte, formes, images.
  • Un View Lifecycle incorrect est la principale cause des problèmes de performance de l’interface utilisateur (jank, images perdues) et des problèmes de hiérarchie.

View Lifecycle — qu’est-ce que c’est dans Android

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 :

  • onMeasure(int widthMeasureSpec, int heightMeasureSpec) — détermine les dimensions souhaitées de la View. Le système transmet MeasureSpec — une instruction sur les dimensions autorisées (valeur exacte, maximum ou sans restriction).
  • onLayout(boolean changed, int left, int top, int right, int bottom) — positionne la View et ses enfants à l’écran. Pour une View, il définit ses propres limites ; pour une ViewGroup, il positionne les éléments enfants.
  • onDraw(Canvas canvas) — dessine le contenu de la View sur le Canvas fourni. Le système fournit un Canvas qui traduit les commandes en bitmap ou GPU.

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 : mesure des dimensions de la View

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 :

ModeConstanteSignificationExemple
EXACTLYMeasureSpec.EXACTLYTaille exacte définie par le parent (match_parent ou largeur fixe)width=400dp → MeasureSpec(400, EXACTLY)
AT_MOSTMeasureSpec.AT_MOSTLa View peut avoir jusqu’à la taille maximale spécifiée (wrap_content)width ≤ 400dp → MeasureSpec(400, AT_MOST)
UNSPECIFIEDMeasureSpec.UNSPECIFIEDAucune restriction — la View peut avoir n’importe quelle taille (ScrollView, RecyclerView)largeur illimitée → MeasureSpec(0, UNSPECIFIED)

L’implémentation de onMeasure doit :

  • Appeler setMeasuredDimension(int width, int height) pour enregistrer les dimensions mesurées.
  • Prendre en compte le padding — soustraire getPaddingLeft() + getPaddingRight() de la largeur disponible.
  • Pour ViewGroup — mesurer tous les enfants via measureChild() ou measureChildWithMargins().
  • Pour wrap_content — calculer la taille en fonction du contenu (texte, image).
  • Ne pas appeler requestLayout() à l’intérieur de onMeasure — cela provoquerait une boucle infinie.

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 : placement des Views à l’écran

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 :

java
@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 :

  • Parcourir tous les enfants via getChildCount() et getChildAt(i).
  • Pour chaque enfant, déterminer left, top, right, bottom — les coordonnées à l’intérieur de la ViewGroup (en tenant compte du padding).
  • Appeler child.layout(l, t, r, b) pour chaque enfant.
  • Tenir compte de gravity, margins, alignment.

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 : dessin du contenu du Canvas

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 :

  • Ne créez pas d’objets dans onDraw — chaque appel onDraw doit utiliser des objets précréés (Path, Paint, Rect). La création d’objets dans onDraw provoque des pauses du GC et des images perdues.
  • N’appelez pas requestLayout() ou invalidate() à l’intérieur de onDraw — cela déclencherait une boucle infinie de redessin.
  • N’effectuez pas de calculs longs — onDraw s’exécute sur le thread UI. Les calculs complexes doivent être déplacés vers un thread d’arrière-plan ou précalculés.
  • Utilisez l’accélération matérielle — depuis l’API 14+, Canvas peut fonctionner via le GPU. Pour les graphiques complexes (dégradés, ombres, rotations), l’accélération HW offre jusqu’à 300% d’amélioration des performances.
  • Dessinez uniquement la zone visible — utilisez 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 : quand une View est redessinée

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éthodeCe qu’elle faitQuand l’utiliser
invalidate()Déclenche onDraw sans onMeasure/onLayoutSeule l’apparence a changé (couleur, texte, progression)
invalidate(Rect)Redessine uniquement la zone spécifiéeUne partie de la View a changé — animation, sélection
postInvalidate()Appelle invalidate depuis un thread non-UIUn thread d’arrière-plan a mis à jour les données de dessin
requestLayout()Déclenche onMeasure → onLayout → onDrawLa taille du contenu a changé (texte, image)
forceLayout()Marque la View pour remesure forcéeL’é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.

Optimisation des Views personnalisées : meilleures pratiques

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.

  • Précalculez tout ce qui peut l’être — tailles, coordonnées, chemin, couleurs de dégradé. Dans onDraw, effectuez uniquement le dessin.
  • Mettez en cache les résultats de mesure — si une View a des dimensions fixes, enregistrez MeasureSpec et retournez setMeasuredDimension sans calculs supplémentaires.
  • Utilisez ViewConfiguration — getScaledTouchSlop, getScaledMinimumFlingVelocity — pour la gestion des touches.
  • Minimisez le nombre de Views dans la hiérarchie — les Views personnalisées qui combinent plusieurs éléments sont toujours plus rapides qu’une ViewGroup avec 3–5 Views imbriquées. Google recommande au maximum 10 Views imbriquées par écran.
  • Utilisez ConstraintLayout pour une hiérarchie plate — il construit une seule ViewGroup avec des performances proches de RelativeLayout, mais sans imbrication.
  • Désactivez la couche matérielle après le redessin — utilisez setLayerType(LAYER_TYPE_HARDWARE) pour les Views avec animations et setLayerType(LAYER_TYPE_NONE) après la fin.
  • Utilisez invalidate() avec Rect — redessinez uniquement la zone modifiée, pas la View entière.
  • Évitez le overdraw — utilisez Profile GPU Rendering dans Android Studio pour identifier les redessins inutiles. Le overdraw moyen pour les applications Google est de 1.5x, le maximum recommandé est de 2.5x.

Exemples de code View en Kotlin

Exemple 1 : View personnalisée — indicateur de progression

Un indicateur de progression circulaire simple avec une implémentation correcte de onMeasure, onDraw et 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
    }
}

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.

Exemple 2 : ViewGroup — FlowLayout simple

Une ViewGroup personnalisée qui dispose les Views enfants en lignes (comme 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 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.

Exemple 3 : onDraw avec mise en cache de Path

Une View personnalisée dessine une courbe de Bezier lisse, en précalculant le Path et en le mettant en 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)
    }
}

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

En quoi View Lifecycle diffère-t-il d’Activity Lifecycle ?

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.

Quel effet requestLayout() a-t-il sur les performances ?

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().

Quand onAttachedToWindow est-il appelé ?

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.

Qu’est-ce que le overdraw et comment le réduire ?

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+).

super.onDraw() est-il nécessaire dans une View personnalisée ?

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é

  • View Lifecycle — trois phases de dessin : onMeasure (dimensions), onLayout (position), onDraw (rendu).
  • onMeasure traite MeasureSpec (EXACTLY, AT_MOST, UNSPECIFIED) et appelle setMeasuredDimension.
  • onLayout dans ViewGroup positionne les Views enfants avec les coordonnées left/top/right/bottom.
  • onDraw restitue le contenu sur Canvas — ne créez pas d’objets dans cette méthode.
  • invalidate() déclenche uniquement onDraw, requestLayout() déclenche le cycle complet onMeasure → onLayout → onDraw.
  • Les Views personnalisées accélèrent l’interface utilisateur de 15 à 40%, mais nécessitent une implémentation correcte de onMeasure et la mise en cache d’objets dans onDraw.
  • Pour les graphiques complexes, utilisez l’accélération matérielle et mettez en cache Path/Bitmap.

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.

Discuter du projet

Lisez aussi