Guide rapide sur les animations dans Compose

Compose dispose de nombreux mécanismes d'animation intégrés, et il peut être difficile de savoir lequel choisir. Voici une liste de cas d'utilisation courants des animations. Pour en savoir plus sur l'ensemble des options d'API disponibles, consultez la documentation sur l'animation Compose.

Animer des propriétés composables courantes

Compose fournit des API pratiques qui vous permettent de résoudre de nombreux cas d'utilisation d'animation courants. Cette section explique comment animer les propriétés courantes d'un composable.

Animer l'apparition / la disparition

Composable vert qui s'affiche et se masque
Figure 1. Animer l'apparition et la disparition d'un élément dans une colonne

Utilisez AnimatedVisibility pour masquer ou afficher un composable. Les enfants dans AnimatedVisibility peuvent utiliser Modifier.animateEnterExit() pour leur propre transition d'entrée ou de sortie.

var visible by remember {
    mutableStateOf(true)
}
// Animated visibility will eventually remove the item from the composition once the animation has finished.
AnimatedVisibility(visible) {
    // your composable here
    // ...
}

Les paramètres d'entrée et de sortie de AnimatedVisibility vous permettent de configurer le comportement d'un composable lorsqu'il apparaît et disparaît. Pour en savoir plus, consultez la documentation complète.

Pour animer la visibilité d'un composable, vous pouvez également animer l'alpha au fil du temps à l'aide de animateFloatAsState :

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}

Toutefois, la modification de l'alpha s'accompagne d'une mise en garde : le composable reste dans la composition et continue d'occuper l'espace dans lequel il est disposé. Les lecteurs d'écran et d'autres mécanismes d'accessibilité pourraient alors toujours considérer l'élément comme étant à l'écran. En revanche, AnimatedVisibility finit par supprimer l'élément de la composition.

Animer l'alpha d'un composable
Figure 2. Animer l'alpha d'un composable

Animer la couleur d'arrière-plan

Composable dont la couleur d'arrière-plan change au fil du temps sous forme d'animation, où les couleurs s'estompent les unes dans les autres.
Figure 3. Animer la couleur d'arrière-plan d'un composable

val animatedColor by animateColorAsState(
    if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}

Cette option est plus performante que l'utilisation de Modifier.background(). Modifier.background() est acceptable pour un paramètre de couleur ponctuel, mais lorsque vous animez une couleur au fil du temps, cela peut entraîner plus de recompositions que nécessaire.

Pour animer indéfiniment la couleur d'arrière-plan, consultez la section Répéter une animation.

Animer la taille d'un composable

Composable vert animant son changement de taille de manière fluide.
Figure 4. Composable s'animant de manière fluide entre une petite et une grande taille

Compose vous permet d'animer la taille des composables de différentes manières. Utilisez animateContentSize() pour les animations entre les changements de taille des composables.

Par exemple, si vous avez une zone contenant du texte qui peut s'étendre sur une ou plusieurs lignes, vous pouvez utiliser Modifier.animateContentSize() pour obtenir une transition plus fluide :

var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}

Vous pouvez également utiliser AnimatedContent avec un SizeTransform pour décrire la façon dont les changements de taille doivent s'effectuer.

Animer la position d'un composable

Composable vert s'animant de manière fluide vers le bas et vers la droite
Figure 5 : Déplacement d'un composable selon un décalage

Pour animer la position d'un composable, utilisez Modifier.offset{ } combiné à animateIntOffsetAsState().

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)

Si vous souhaitez vous assurer que les composables ne sont pas dessinés au-dessus ou en dessous d'autres composables lors de l'animation de la position ou de la taille, utilisez Modifier.layout{ }. Ce modificateur propage les modifications de taille et de position au parent, ce qui affecte ensuite les autres enfants.

Par exemple, si vous déplacez un Box dans un Column et que les autres enfants doivent se déplacer lorsque le Box se déplace, incluez les informations de décalage avec Modifier.layout{ } comme suit :

var toggled by remember {
    mutableStateOf(false)
}
val interactionSource = remember {
    MutableInteractionSource()
}
Column(
    modifier = Modifier
        .padding(16.dp)
        .fillMaxSize()
        .clickable(indication = null, interactionSource = interactionSource) {
            toggled = !toggled
        }
) {
    val offsetTarget = if (toggled) {
        IntOffset(150, 150)
    } else {
        IntOffset.Zero
    }
    val offset = animateIntOffsetAsState(
        targetValue = offsetTarget, label = "offset"
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
    Box(
        modifier = Modifier
            .layout { measurable, constraints ->
                val offsetValue = if (isLookingAhead) offsetTarget else offset.value
                val placeable = measurable.measure(constraints)
                layout(placeable.width + offsetValue.x, placeable.height + offsetValue.y) {
                    placeable.placeRelative(offsetValue)
                }
            }
            .size(100.dp)
            .background(colorGreen)
    )
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(colorBlue)
    )
}

Deux cases,la deuxième animant sa position X, Y et la troisième réagissant en se déplaçant également de la valeur Y.
Figure 6 : Animation avec Modifier.layout{ }

Animer la marge intérieure d'un composable

Composable vert qui rétrécit et s'agrandit au clic, avec une marge intérieure animée
Figure 7. Composable avec son remplissage animé

Pour animer la marge intérieure d'un composable, utilisez animateDpAsState combiné à Modifier.padding() :

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)

Animer l'élévation d'un composable

Figure 8. Élévation du composable s'animant au clic

Pour animer l'élévation d'un composable, utilisez animateDpAsState combiné à Modifier.graphicsLayer{ }. Pour les changements d'élévation ponctuels, utilisez Modifier.shadow(). Si vous animez l'ombre, l'utilisation du modificateur Modifier.graphicsLayer{ } est l'option la plus performante.

val mutableInteractionSource = remember {
    MutableInteractionSource()
}
val pressed = mutableInteractionSource.collectIsPressedAsState()
val elevation = animateDpAsState(
    targetValue = if (pressed.value) {
        32.dp
    } else {
        8.dp
    },
    label = "elevation"
)
Box(
    modifier = Modifier
        .size(100.dp)
        .align(Alignment.Center)
        .graphicsLayer {
            this.shadowElevation = elevation.value.toPx()
        }
        .clickable(interactionSource = mutableInteractionSource, indication = null) {
        }
        .background(colorGreen)
) {
}

Vous pouvez également utiliser le composable Card et définir la propriété d'élévation sur différentes valeurs par état.

Animer l'ajustement, la translation ou la rotation du texte

Composant de texte affichant "Hello" et s'animant entre une petite et une grande taille.
Figure 9. Texte s'animant de manière fluide entre deux tailles

Lorsque vous animez l'échelle, la translation ou la rotation du texte, définissez le paramètre textMotion sur TextStyle sur TextMotion.Animated. Cela garantit des transitions plus fluides entre les animations de texte. Utilisez Modifier.graphicsLayer{ } pour traduire, faire pivoter ou mettre à l'échelle le texte.

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}

Animer la couleur du texte

Texte "Hello Compose" dont la couleur passe du vert au bleu
Figure 10. Exemple montrant l'animation de la couleur du texte

Pour animer la couleur du texte, utilisez le lambda color sur le composable BasicText :

val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val animatedColor by infiniteTransition.animateColor(
    initialValue = Color(0xFF60DDAD),
    targetValue = Color(0xFF4285F4),
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "color"
)

BasicText(
    text = "Hello Compose",
    color = {
        animatedColor
    },
    // ...
)

Passer d'un type de contenu à un autre

Un écran vert indiquant "Chargement", un écran bleu indiquant "Chargé" et un écran blanc indiquant "Erreur". Les différents composables sont parcourus avec une animation simple.
Figure 11. Utiliser AnimatedContent pour animer les changements entre différents composables (ralenti)

Utilisez AnimatedContent pour animer la transition entre différents composables. Si vous souhaitez simplement un fondu standard entre les composables, utilisez Crossfade.

var state by remember {
    mutableStateOf(UiState.Loading)
}
AnimatedContent(
    state,
    transitionSpec = {
        fadeIn(
            animationSpec = tween(3000)
        ) togetherWith fadeOut(animationSpec = tween(3000))
    },
    modifier = Modifier.clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = null
    ) {
        state = when (state) {
            UiState.Loading -> UiState.Loaded
            UiState.Loaded -> UiState.Error
            UiState.Error -> UiState.Loading
        }
    },
    label = "Animated Content"
) { targetState ->
    when (targetState) {
        UiState.Loading -> {
            LoadingScreen()
        }
        UiState.Loaded -> {
            LoadedScreen()
        }
        UiState.Error -> {
            ErrorScreen()
        }
    }
}

AnimatedContent peut être personnalisé pour afficher de nombreux types de transitions d'entrée et de sortie. Pour en savoir plus, consultez la documentation sur AnimatedContent ou lisez cet article de blog sur AnimatedContent.

Animer lors de la navigation vers différentes destinations

Deux composables, l'un vert indiquant "Landing" et l'autre bleu indiquant "Detail", s'animent en faisant glisser le composable "Detail" sur le composable "Landing".
Figure 12 : Animer les transitions entre les composables à l'aide de navigation-compose

Pour animer les transitions entre les composables lorsque vous utilisez l'artefact navigation-compose, spécifiez enterTransition et exitTransition sur un composable. Vous pouvez également définir l'animation par défaut à utiliser pour toutes les destinations au niveau supérieur NavHost :

val navController = rememberNavController()
NavHost(
    navController = navController, startDestination = "landing",
    enterTransition = { EnterTransition.None },
    exitTransition = { ExitTransition.None }
) {
    composable("landing") {
        ScreenLanding(
            // ...
        )
    }
    composable(
        "detail/{photoUrl}",
        arguments = listOf(navArgument("photoUrl") { type = NavType.StringType }),
        enterTransition = {
            fadeIn(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideIntoContainer(
                animationSpec = tween(300, easing = EaseIn),
                towards = AnimatedContentTransitionScope.SlideDirection.Start
            )
        },
        exitTransition = {
            fadeOut(
                animationSpec = tween(
                    300, easing = LinearEasing
                )
            ) + slideOutOfContainer(
                animationSpec = tween(300, easing = EaseOut),
                towards = AnimatedContentTransitionScope.SlideDirection.End
            )
        }
    ) { backStackEntry ->
        ScreenDetails(
            // ...
        )
    }
}

Il existe de nombreux types de transitions d'entrée et de sortie qui appliquent différents effets au contenu entrant et sortant. Pour en savoir plus, consultez la documentation.

Répéter une animation

Un arrière-plan vert qui se transforme en arrière-plan bleu, à l'infini, en alternant entre les deux couleurs.
Figure 13. Couleur d'arrière-plan animée entre deux valeurs, à l'infini

Utilisez rememberInfiniteTransition avec un infiniteRepeatable animationSpec pour répéter votre animation en continu. Modifiez RepeatModes pour spécifier comment il doit aller et venir.

Utilisez repeatable pour répéter un nombre défini de fois.

val infiniteTransition = rememberInfiniteTransition(label = "infinite")
val color by infiniteTransition.animateColor(
    initialValue = Color.Green,
    targetValue = Color.Blue,
    animationSpec = infiniteRepeatable(
        animation = tween(1000, easing = LinearEasing),
        repeatMode = RepeatMode.Reverse
    ),
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(color)
    }
) {
    // your composable here
}

Démarrer une animation au lancement d'un composable

LaunchedEffect s'exécute lorsqu'un composable entre dans la composition. Il lance une animation au lancement d'un composable. Vous pouvez l'utiliser pour piloter le changement d'état de l'animation. Utilisez Animatable avec la méthode animateTo pour démarrer l'animation au lancement :

val alphaAnimation = remember {
    Animatable(0f)
}
LaunchedEffect(Unit) {
    alphaAnimation.animateTo(1f)
}
Box(
    modifier = Modifier.graphicsLayer {
        alpha = alphaAnimation.value
    }
)

Créer des animations séquentielles

Quatre cercles avec des flèches vertes s'animant entre chacun d'eux, l'un après l'autre.
Figure 14. Schéma indiquant la progression d'une animation séquentielle, une par une.

Utilisez les API de coroutine Animatable pour effectuer des animations séquentielles ou simultanées. L'appel de animateTo sur Animatable l'un après l'autre entraîne l'attente de la fin des animations précédentes avant de continuer. En effet, il s'agit d'une fonction de suspension.

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    alphaAnimation.animateTo(1f)
    yAnimation.animateTo(100f)
    yAnimation.animateTo(500f, animationSpec = tween(100))
}

Créer des animations simultanées

Trois cercles avec des flèches vertes animées pointant vers chacun d'eux, s'animant tous en même temps.
Figure 15. Diagramme indiquant la progression d'animations simultanées, toutes en même temps.

Utilisez les API de coroutine (Animatable#animateTo() ou animate) ou l'API Transition pour obtenir des animations simultanées. Si vous utilisez plusieurs fonctions de lancement dans un contexte de coroutine, les animations sont lancées en même temps :

val alphaAnimation = remember { Animatable(0f) }
val yAnimation = remember { Animatable(0f) }

LaunchedEffect("animationKey") {
    launch {
        alphaAnimation.animateTo(1f)
    }
    launch {
        yAnimation.animateTo(100f)
    }
}

Vous pouvez utiliser l'API updateTransition pour utiliser le même état afin de piloter simultanément de nombreuses animations de propriétés différentes. L'exemple suivant anime deux propriétés contrôlées par un changement d'état, rect et borderWidth :

var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "transition")

val rect by transition.animateRect(label = "rect") { state ->
    when (state) {
        BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
        BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
    }
}
val borderWidth by transition.animateDp(label = "borderWidth") { state ->
    when (state) {
        BoxState.Collapsed -> 1.dp
        BoxState.Expanded -> 0.dp
    }
}

Optimiser les performances des animations

Les animations dans Compose peuvent entraîner des problèmes de performances. Cela est dû à la nature même d'une animation : déplacer ou modifier rapidement des pixels à l'écran, image par image, pour créer l'illusion d'un mouvement.

Tenez compte des différentes phases de Compose : composition, mise en page et dessin. Si votre animation modifie la phase de mise en page, tous les composables concernés doivent être réorganisés et redessinés. Si votre animation se produit pendant la phase de dessin, elle est par défaut plus performante que si vous l'exécutiez pendant la phase de mise en page, car elle nécessite moins de travail global.

Pour que votre application effectue le moins d'opérations possible pendant l'animation, choisissez la version lambda d'un Modifier dans la mesure du possible. Cela permet d'ignorer la recomposition et d'effectuer l'animation en dehors de la phase de composition. Sinon, utilisez Modifier.graphicsLayer{ }, car ce modificateur s'exécute toujours pendant la phase de dessin. Pour en savoir plus, consultez la section Différer les lectures de la documentation sur les performances.

Modifier la chronologie de l'animation

Par défaut, Compose utilise des animations de rétroaction pour la plupart des animations. Les animations basées sur la physique, comme les ressorts, semblent plus naturelles. Elles sont également interruptibles, car elles prennent en compte la vitesse actuelle de l'objet, et non une durée fixe. Si vous souhaitez remplacer la valeur par défaut, toutes les API d'animation présentées précédemment permettent de définir un animationSpec pour personnaliser l'exécution d'une animation, que vous souhaitiez qu'elle s'exécute sur une certaine durée ou qu'elle soit plus dynamique.

Voici un récapitulatif des différentes options animationSpec :

  • spring : animation basée sur la physique, par défaut pour toutes les animations. Vous pouvez modifier la rigidité ou le ratio d'amortissement pour obtenir une apparence d'animation différente.
  • tween (abréviation de between) : animation basée sur la durée, qui anime entre deux valeurs avec une fonction Easing.
  • keyframes : spécification permettant de définir des valeurs à certains points clés d'une animation.
  • repeatable : spécification basée sur la durée qui s'exécute un certain nombre de fois, spécifié par RepeatMode.
  • infiniteRepeatable : spécification basée sur la durée qui s'exécute indéfiniment.
  • snap : passe instantanément à la valeur finale sans aucune animation.
Deux animations montrant l'absence de spécification par rapport à une spécification Spring personnalisée.
Figure 16. Aucune spécification définie par rapport à une spécification Spring personnalisée

Pour en savoir plus sur les animationSpecs, consultez la documentation complète.

Ressources supplémentaires

Pour voir d'autres exemples d'animations amusantes dans Compose, consultez les ressources suivantes :