Animer des éléments dans Jetpack Compose

1. Introduction

Logo Jetpack Compose

Dernière mise à jour : 21/11/2023

Dans cet atelier de programmation, vous apprendrez à utiliser certaines API Animation dans Jetpack Compose.

Jetpack Compose est un kit d'outils moderne conçu pour simplifier le développement des interfaces utilisateurs. Si vous débutez avec Jetpack Compose, nous vous conseillons d'essayer plusieurs ateliers de programmation avant celui-ci.

Points abordés

  • Comment utiliser plusieurs API Animation de base ?

Conditions préalables

Ce dont vous avez besoin

2. Configuration

Téléchargez le code de l'atelier de programmation. Vous pouvez cloner le dépôt comme suit :

$ git clone https://github.com/android/codelab-android-compose.git

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP :

Importez le projet AnimationCodelab dans Android Studio.

Importation d'un atelier de programmation d'animation dans Android Studio

Le projet contient plusieurs modules :

  • start est l'état de départ de l'atelier de programmation.
  • finished est l'état final de l'application une fois cet atelier de programmation terminé.

Assurez-vous que start est sélectionné dans le menu déroulant pour la configuration d'exécution.

Capture d'écran montrant que "start" (commencer) est sélectionné dans Android Studio

Dans le chapitre suivant, nous allons travailler sur plusieurs scénarios d'animation. Un commentaire // TODO est ajouté à chaque extrait de code sur lequel nous travaillons dans cet atelier. Une astuce consiste à ouvrir la fenêtre de l'outil TODO (À FAIRE) dans Android Studio et à accéder à chacun des commentaires du chapitre.

Liste TODO affichée dans Android Studio

3. Animer un changement de valeur simple

Commençons par les API Animation les plus simples dans Compose : les API animate*AsState. Cette API doit être utilisée pour animer des changements de State (d'État).

Exécutez la configuration start et essayez de changer d'onglet en cliquant sur les boutons "Home" (Accueil) et "Work" (Travail) en haut de la page. Le contenu de l'onglet n'est pas vraiment modifié, mais vous pouvez constater que la couleur d'arrière-plan du contenu change.

Onglet "Home" (Accueil) sélectionné

Onglet "Work" sélectionné

Cliquez sur TODO 1 dans la fenêtre de l'outil TODO et découvrez comment cela fonctionne. Vous le trouverez dans le composable Home.

val backgroundColor = if (tabPage == TabPage.Home) Seashell else GreenLight

Ici, tabPage est un TabPage reposant sur un objet State. Selon sa valeur, l'arrière-plan passe de la couleur pêche au vert. Nous voulons animer ce changement de valeur.

Pour animer un changement de valeur simple comme celui-ci, nous pouvons utiliser les API animate*AsState. Vous pouvez créer une valeur d'animation en encapsulant la valeur qui change avec la variante correspondante des composables animate*AsState (animateColorAsState dans ce cas). La valeur renvoyée est un objet State<T>. Nous pouvons donc utiliser une propriété déléguée locale avec une déclaration by pour la traiter comme une variable normale.

val backgroundColor by animateColorAsState(
        targetValue = if (tabPage == TabPage.Home) Seashell else GreenLight,
        label = "background color")

Exécutez à nouveau l'application et essayez de changer d'onglet. Le changement de couleur est maintenant animé.

Animation montrant le changement de couleur entre les onglets

4. Visibilité de l'animation

Si vous faites défiler le contenu de l'application, vous remarquerez que le bouton d'action flottant se développe et se réduit en fonction de la direction de votre défilement.

Bouton d'action flottant "Edit" (Modifier) développé

Bouton d'action flottant "Edit" (Modifier) réduit

Recherchez TODO 2-1 et découvrez comment cela fonctionne. Vous le trouverez dans le composable HomeFloatingActionButton. Le texte indiquant "EDIT" est affiché ou masqué à l'aide d'une instruction if.

if (extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}

Pour animer ce changement de visibilité, il suffit de remplacer if par un composable AnimatedVisibility.

AnimatedVisibility(extended) {
    Text(
        text = stringResource(R.string.edit),
        modifier = Modifier
            .padding(start = 8.dp, top = 3.dp)
    )
}

Exécutez l'application et découvrez comment le bouton d'action flottant se développe et se réduit maintenant.

Animation du bouton d'action flottant Edit

AnimatedVisibility exécute son animation à chaque fois que la valeur Boolean spécifiée change. Par défaut, AnimatedVisibility affiche l'élément en faisant un fondu et en le développant, et le masque en faisant un fondu et en le réduisant. Ce comportement est idéal pour cet exemple avec le bouton d'action flottant, mais nous pouvons aussi le personnaliser.

Essayez de cliquer sur le bouton d'action flottant pour afficher le message "Edit feature is not supported" ("La fonctionnalité de modification n'est pas disponible"). Elle utilise également AnimatedVisibility pour animer son apparence et sa disparition. Vous allez ensuite personnaliser ce comportement de sorte que le message apparaisse par le haut et disparaisse en glissant par le même endroit.

Message indiquant que la fonctionnalité de modification n'est pas disponible

Recherchez TODO 2-2 et consultez le code dans le composable EditMessage.

AnimatedVisibility(
    visible = shown
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colorScheme.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}

Pour personnaliser l'animation, ajoutez les paramètres enter et exit au composable AnimatedVisibility.

Le paramètre enter doit être une instance de EnterTransition. Pour cet exemple, nous pouvons utiliser la fonction slideInVertically afin de créer un EnterTransition et unslideOutVertically pour la transition de sortie. Modifiez le code comme suit :

AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(),
    exit = slideOutVertically()
)

Exécutez à nouveau l'application. Si vous cliquez sur le bouton de modification, vous pouvez constater que l'animation semble meilleure, mais qu'elle n'est pas tout à fait correcte. En effet, slideInVertically et slideOutVertically utilisent par défaut la moitié de la hauteur de l'élément.

Glissement de sortie coupé à la moitié de l'animation

Pour la transition d'entrée, nous pouvons ajuster le comportement par défaut afin d'utiliser toute la hauteur de l'élément pour l'animer correctement en définissant le paramètre initialOffsetY. initialOffsetY doit être un lambda renvoyant la position initiale.

Le lambda reçoit un argument, la hauteur de l'élément. Pour que l'élément glisse depuis le haut de l'écran, nous renvoyons sa valeur négative, car le haut de l'écran a pour valeur 0. Nous voulons que l'animation commence à-height et finisse à 0 (sa position finale) pour qu'elle apparaisse à partir du haut.

Lorsque vous utilisez slideInVertically, le décalage cible pour une diapositive est toujours de 0 (pixel). initialOffsetY peut être spécifié en tant que valeur absolue ou pourcentage de la hauteur totale de l'élément via une fonction lambda.

De même, slideOutVertically suppose que le décalage initial est de 0. Ainsi, seul targetOffsetY doit être spécifié.

AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(
        // Enters by sliding down from offset -fullHeight to 0.
        initialOffsetY = { fullHeight -> -fullHeight }
    ),
    exit = slideOutVertically(
        // Exits by sliding up from offset 0 to -fullHeight.
        targetOffsetY = { fullHeight -> -fullHeight }
    )
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colorScheme.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}

En exécutant à nouveau l'application, nous constatons que l'animation est plus conforme à nos attentes :

Diapositive animée avec un décalage réussi

Nous pouvons personnaliser davantage notre animation avec le paramètre animationSpec. animationSpec est un paramètre courant pour de nombreuses API Animation, y compris EnterTransition et ExitTransition. Nous pouvons transmettre l'un des différents types d'AnimationSpec pour indiquer l'évolution de la valeur d'animation au fil du temps. Dans cet exemple, utilisons un élément AnimationSpec simple basé sur la durée. Vous pouvez le créer avec la fonction tween. La durée est de 150 ms et le lissage de vitesse est de type LinearOutSlowInEasing. Pour l'animation de sortie, nous allons utiliser la même fonction tween pour le paramètre animationSpec, mais avec une durée de 250 ms et un lissage de vitesse de type FastOutLinearInEasing.

Le code obtenu doit se présenter comme suit :

AnimatedVisibility(
    visible = shown,
    enter = slideInVertically(
        // Enters by sliding down from offset -fullHeight to 0.
        initialOffsetY = { fullHeight -> -fullHeight },
        animationSpec = tween(durationMillis = 150, easing = LinearOutSlowInEasing)
    ),
    exit = slideOutVertically(
        // Exits by sliding up from offset 0 to -fullHeight.
        targetOffsetY = { fullHeight -> -fullHeight },
        animationSpec = tween(durationMillis = 250, easing = FastOutLinearInEasing)
    )
) {
    Surface(
        modifier = Modifier.fillMaxWidth(),
        color = MaterialTheme.colorScheme.secondary,
        elevation = 4.dp
    ) {
        Text(
            text = stringResource(R.string.edit_message),
            modifier = Modifier.padding(16.dp)
        )
    }
}

Exécutez l'application et cliquez à nouveau sur le bouton d'action flottant. Comme vous pouvez le constater, le message apparaît et disparaît en glissant depuis le haut, avec différentes durées et fonctions de lissage de vitesse :

Animation montrant le message de modification qui glisse depuis le haut

5. Animation de la modification de la taille du contenu

L'application affiche plusieurs sujets. Cliquez sur l'un d'entre eux. Le corps du texte correspondant à ce sujet devrait s'afficher. La fiche contenant le texte se développe et se réduit lorsque le corps est affiché ou masqué.

Liste des sujets réduite

Liste des sujets développée

Vérifiez le code de TODO 3 dans le composable TopicRow.

Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)
) {
    // ... the title and the body
}

La taille du composable Column change à mesure que son contenu est modifié. Nous pouvons animer le changement de taille en ajoutant le modificateur animateContentSize.

Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp)
        .animateContentSize()
) {
    // ... the title and the body
}

Exécutez l'application, puis cliquez sur l'un des sujets. Vous pouvez constater qu'elle se développe et se réduit avec une animation.

Animation développant et réduisant la liste des sujets

animateContentSize peut aussi être personnalisé en personnalisant animationSpec. Nous pouvons vous proposer des solutions pour changer le type d'animation. Pour en savoir plus, consultez la documentation sur la personnalisation des animations.

6. Animation de plusieurs valeurs

Maintenant que nous connaissons quelques API Animation de base, examinons l'API Transition, qui nous permet de créer des animations plus complexes. L'API Transition nous permet de suivre le moment où toutes les animations d'une Transition sont terminées, ce qui n'est pas possible avec les API animate*AsState individuelles que nous avons vues précédemment. L'API Transition nous permet également de définir différentes transitionSpec lors du passage d'un état à un autre. Voyons comment l'utiliser :

Pour cet exemple, nous personnalisons l'indicateur d'onglet. Il s'agit d'un rectangle affiché dans l'onglet sélectionné.

Onglet "Home" (Accueil) sélectionné

Onglet "Work" sélectionné

Recherchez TODO 4 dans le composable HomeTabIndicator et découvrez comment l'indicateur d'onglet est implémenté.

val indicatorLeft = tabPositions[tabPage.ordinal].left
val indicatorRight = tabPositions[tabPage.ordinal].right
val color = if (tabPage == TabPage.Home) PaleDogwood else Green

Ici, indicatorLeft correspond à la position horizontale du bord gauche de l'indicateur dans la ligne de l'onglet. indicatorRight correspond à la position horizontale du bord droit de l'indicateur. La couleur passe également de la couleur pêche au vert.

Pour animer ces différentes valeurs simultanément, nous pouvons utiliser une Transition. Une Transition peut être créée avec la fonction updateTransition. Transmettez l'index de l'onglet actuellement sélectionné en tant que paramètre targetState.

Chaque valeur d'animation peut être déclarée à l'aide des fonctions d'extension animate* de Transition. Dans cet exemple, nous utilisons animateDp et animateColor. Ils prennent un bloc lambda et nous pouvons spécifier la valeur cible pour chacun des états. Nous connaissons déjà leurs valeurs cibles, ce qui nous permet d'encapsuler les valeurs comme indiqué ci-dessous. Notez que nous pouvons utiliser une déclaration by et la convertir en propriété déléguée locale, car les fonctions animate* renvoient un objet State.

val transition = updateTransition(tabPage, label = "Tab indicator")
val indicatorLeft by transition.animateDp(label = "Indicator left") { page ->
   tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(label = "Indicator right") { page ->
   tabPositions[page.ordinal].right
}
val color by transition.animateColor(label = "Border color") { page ->
   if (page == TabPage.Home) PaleDogwood else Green
}

Exécutez l'application et vous constaterez que le changement d'onglet est désormais beaucoup plus intéressant. Lorsque vous cliquez sur l'onglet, la valeur de l'état tabPage change. Par conséquent, toutes les valeurs d'animation associées à transition commencent à s'animer pour la valeur spécifiée pour l'état cible.

Animation entre les onglets Home et Work

Nous pouvons également spécifier le paramètre transitionSpec pour personnaliser le comportement de l'animation. Par exemple, nous pouvons obtenir un effet élastique pour l'indicateur en faisant en sorte que le bord le plus proche de la destination se déplace plus rapidement que l'autre bord. Nous pouvons utiliser la fonction infixe isTransitioningTo dans les lambdas transitionSpec pour déterminer le sens du changement d'état.

val transition = updateTransition(
    tabPage,
    label = "Tab indicator"
)
val indicatorLeft by transition.animateDp(
    transitionSpec = {
        if (TabPage.Home isTransitioningTo TabPage.Work) {
            // Indicator moves to the right.
            // The left edge moves slower than the right edge.
            spring(stiffness = Spring.StiffnessVeryLow)
        } else {
            // Indicator moves to the left.
            // The left edge moves faster than the right edge.
            spring(stiffness = Spring.StiffnessMedium)
        }
    },
    label = "Indicator left"
) { page ->
    tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp(
    transitionSpec = {
        if (TabPage.Home isTransitioningTo TabPage.Work) {
            // Indicator moves to the right
            // The right edge moves faster than the left edge.
            spring(stiffness = Spring.StiffnessMedium)
        } else {
            // Indicator moves to the left.
            // The right edge moves slower than the left edge.
            spring(stiffness = Spring.StiffnessVeryLow)
        }
    },
    label = "Indicator right"
) { page ->
    tabPositions[page.ordinal].right
}
val color by transition.animateColor(
    label = "Border color"
) { page ->
    if (page == TabPage.Home) PaleDogwood else Green
}

Exécutez à nouveau l'application et essayez de changer d'onglet.

Effet élastique personnalisé lors du changement d'onglet

Android Studio permet d'inspecter la transition dans l'aperçu de Compose. Pour utiliser l'aperçu de l'animation, démarrez le mode interactif en cliquant sur l'icône Start Animation Preview (Démarrer l'aperçu de l'animation) située dans l'angle supérieur droit d'un composable dans l'aperçu (icône Icône d'aperçu de l'animation). Essayez de cliquer sur l'icône du composable PreviewHomeTabBar. Un nouveau volet Animations s'ouvre.

Vous pouvez lancer l'animation en cliquant sur l'icône Play (Lecture). Vous pouvez également faire glisser la barre de recherche pour afficher chacune des images de l'animation. Pour une meilleure description des valeurs d'animation, vous pouvez spécifier le paramètre label dans updateTransition et les méthodes animate*.

Recherche des animations dans Android Studio

7. Animation répétée

Essayez de cliquer sur le bouton d'actualisation situé à côté de la température actuelle. L'application commence à charger les dernières informations météo (elle fait semblant). Tant que le chargement n'est pas terminé, un indicateur de chargement s'affiche (un cercle gris et une barre). Nous allons animer la valeur alpha de cet indicateur pour clarifier que le processus est en cours.

Image statique de la fiche info de l'espace réservé qui n'est pas encore animé.

Recherchez TODO 5 dans le composable LoadingRow.

val alpha = 1f

Nous souhaitons que cette valeur soit animée entre 0f et 1f à plusieurs reprises. Nous pouvons utiliser InfiniteTransition à cette fin. Cette API est semblable à l'API Transition de la section précédente. Elles permettent toutes les deux d'animer plusieurs valeurs, mais tandis que Transition anime les valeurs en fonction des changements d'état, InfiniteTransition anime les valeurs indéfiniment.

Pour créer une InfiniteTransition, utilisez la fonction rememberInfiniteTransition. Chaque modification de valeur d'animation peut ensuite être déclarée à l'aide de l'une des fonctions d'extension animate* de InfiniteTransition. Dans le cas présent, nous allons animer une valeur alpha. Nous allons donc utiliser animatedFloat. Le paramètre initialValue doit être 0f et le paramètre targetValue 1f. Nous pouvons également spécifier un AnimationSpec pour cette animation, mais cette API n'accepte qu'un InfiniteRepeatableSpec. Utilisez la fonction infiniteRepeatable pour en créer un. Ce AnimationSpec encapsule n'importe quelle AnimationSpec basée sur la durée et la rend reproductible. Par exemple, le code obtenu doit se présenter comme suit :

val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = keyframes {
            durationMillis = 1000
            0.7f at 500
        },
        repeatMode = RepeatMode.Reverse
    ),
    label = "alpha"
)

RepeatMode.Restart est le repeatMode par défaut qui passe de initialValue à targetValue et recommence à initialValue. Si vous définissez repeatMode sur RepeatMode.Reverse, l'animation passe de initialValue à targetValue, puis de targetValue à initialValue. L'animation progresse de 0 à 1, puis de 1 à 0.

L'animation keyFrames est un autre type d'animationSpec (telles que tween et spring), qui permet de modifier la valeur en cours à différentes millisecondes. Nous avons initialement défini durationMillis sur 1 000 ms. Nous pouvons ensuite définir des images clés dans l'animation. Par exemple, à 500 ms de l'animation, nous souhaitons que la valeur alpha soit de 0,7f. La progression de l'animation change : elle passe rapidement de 0 à 0,7 dans les premières 500 ms de l'animation, et de 0,7 à 1,0 entre 500 ms et 1 000 ms, ralentissant ainsi jusqu'à la fin.

Si vous souhaitez utiliser plusieurs images clés, vous pouvez définir plusieurs keyFrames comme suit :

animation = keyframes {
   durationMillis = 1000
   0.7f at 500
   0.9f at 800
}

Exécutez l'application et essayez de cliquer sur le bouton d'actualisation. L'indicateur de chargement s'anime.

Contenu de l'espace réservé animé en continu

8. Animation par gestes

Dans cette dernière section, vous allez découvrir comment exécuter des animations à partir de commandes tactiles. Nous allons créer un modificateur swipeToDismiss.

Recherchez TODO 6-1 dans le modificateur swipeToDismiss. Ici, nous essayons de créer un modificateur qui permet de balayer l'élément avec le toucher. Lorsque l'élément est déplacé vers le bord de l'écran, nous appelons le rappel onDismissed afin de le supprimer.

Pour créer un modificateur swipeToDismiss, vous devez comprendre certains concepts clés. Tout d'abord, l'utilisateur place son doigt sur l'écran, ce qui génère un événement tactile avec des coordonnées x et y. Il déplace ensuite son doigt vers la droite ou la gauche, déplaçant ainsi les coordonnées x et y en fonction de son mouvement. L'élément qu'il touche doit se déplacer avec le doigt. Nous allons donc modifier sa position en fonction de la vitesse et de la position de l'événement tactile.

Nous pouvons utiliser plusieurs des concepts décrits dans la documentation relative aux gestes dans Compose. Le modificateur pointerInput permet d'obtenir un accès de niveau inférieur aux événements tactiles entrants de type pointeur et de suivre la vitesse de déplacement de l'utilisateur à l'aide de ce pointeur. Si l'utilisateur enlève son doigt avant que l'élément ne dépasse la limite pour suppression, ce dernier revient à sa position initiale.

Plusieurs éléments sont à prendre en compte pour ce scénario. Tout d'abord, toute animation en cours peut être interceptée par un événement tactile. Ensuite, la valeur de l'animation n'est peut-être pas la seule source fiable. En d'autres termes, nous pouvons être amenés à synchroniser la valeur de l'animation avec des valeurs provenant d'événements tactiles.

Animatable est l'API de niveau le plus bas que nous ayons vue jusqu'à présent. Elle est dotée de plusieurs fonctionnalités utiles dans les scénarios de gestes, comme la possibilité d'ancrer instantanément la nouvelle valeur générée par un geste et d'arrêter toute animation en cours lorsqu'un nouvel événement tactile est déclenché. Nous allons maintenant créer une instance de Animatable et l'utiliser pour représenter le décalage horizontal de l'élément à faire glisser. Veillez à importer Animatable depuis androidx.compose.animation.core.Animatable, et non androidx.compose.animation.Animatable.

val offsetX = remember { Animatable(0f) } // Add this line
// used to receive user touch events
pointerInput {
    // Used to calculate a settling position of a fling animation.
    val decay = splineBasedDecay<Float>(this)
    // Wrap in a coroutine scope to use suspend functions for touch events and animation.
    coroutineScope {
        while (true) {
            // ...

TODO 6-2 est l'endroit où nous venons de recevoir un événement tactile. Nous devrions intercepter l'animation si elle est en cours d'exécution. Pour ce faire, appelez stop sur Animatable. Notez que l'appel est ignoré si l'animation n'est pas en cours d'exécution. VelocityTracker permet de calculer la vitesse de déplacement d'un utilisateur de gauche à droite. awaitPointerEventScope est une fonction de suspension qui peut attendre des événements d'entrée utilisateur et y répondre.

// Wait for a touch down event. Track the pointerId based on the touch
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // Add this line to cancel any on-going animations
// Prepare for drag events and record velocity of a fling gesture
val velocityTracker = VelocityTracker()
// Wait for drag events.
awaitPointerEventScope {

Nous recevons continuellement des événements de déplacement dans TODO 6-3. Nous devons synchroniser la position de l'événement tactile dans la valeur de l'animation. Pour ce faire, nous pouvons utiliser snapTo sur Animatable. snapTo doit être appelé dans un autre bloc launch, car awaitPointerEventScope et horizontalDrag sont des champs d'application de coroutine restreints. Cela signifie qu'ils ne peuvent suspend (suspendre) que awaitPointerEvents, snapTo n'est pas un événement de pointeur.

horizontalDrag(pointerId) { change ->
    // Add these 4 lines
    // Get the drag amount change to offset the item with
    val horizontalDragOffset = offsetX.value + change.positionChange().x
    // Need to call this in a launch block in order to run it separately outside of the awaitPointerEventScope
    launch {
        // Instantly set the Animable to the dragOffset to ensure its moving
        // as the user's finger moves
        offsetX.snapTo(horizontalDragOffset)
    }
    // Record the velocity of the drag.
    velocityTracker.addPosition(change.uptimeMillis, change.position)

    // Consume the gesture event, not passed to external
    if (change.positionChange() != Offset.Zero) change.consume()

}

TODO 6-4 désigne l'endroit où l'élément vient d'être balayé et déplacé. Nous devons calculer la position finale du déplacement afin de déterminer si nous devons faire glisser l'élément vers sa position d'origine, ou le faire glisser et appeler le rappel. Nous utilisons l'objet decay créé précédemment pour calculer targetOffsetX :

// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Add this line to calculate where it would end up with
// the current velocity and position
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)

C'est au niveau de TODO 6-5 que nous allons lancer l'animation. Avant cela, nous voulons définir les limites de valeur supérieure et inférieure sur Animatable pour que ce dernier s'arrête dès qu'il atteint ces limites (-size.width etsize.width, car nous ne voulons pas que offsetX ne dépasser ces deux valeurs). Le modificateur pointerInput nous permet d'accéder à la taille de l'élément par la propriété size. Utilisez cette valeur pour obtenir nos limites.

offsetX.updateBounds(
    lowerBound = -size.width.toFloat(),
    upperBound = size.width.toFloat()
)

C'est au niveau de TODO 6-6 que nous pouvons enfin lancer notre animation. Nous allons d'abord comparer la position finale du déplacement calculé ainsi que sa taille. Si cette position est inférieure à la taille, cela signifie que la vitesse de balayage n'était pas suffisante. Nous pouvons utiliser animateTo pour animer la valeur sur 0f. Sinon, nous utilisons animateDecay pour lancer l'animation de déplacement. Une fois l'animation terminée (généralement selon les limites définies précédemment), nous pouvons appeler le rappel.

launch {
    if (targetOffsetX.absoluteValue <= size.width) {
        // Not enough velocity; Slide back.
        offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
    } else {
        // Enough velocity to slide away the element to the edge.
        offsetX.animateDecay(velocity, decay)
        // The element was swiped away.
        onDismissed()
    }
}

Enfin, consultez TODO 6-7. Toutes les animations et tous les gestes sont configurés. N'oubliez pas d'appliquer le décalage à l'élément. L'élément se déplacera sur l'écran vers la valeur générée par notre geste ou notre animation :

.offset { IntOffset(offsetX.value.roundToInt(), 0) }

Après cette section, vous obtiendrez le code suivant :

private fun Modifier.swipeToDismiss(
    onDismissed: () -> Unit
): Modifier = composed {
    // This Animatable stores the horizontal offset for the element.
    val offsetX = remember { Animatable(0f) }
    pointerInput(Unit) {
        // Used to calculate a settling position of a fling animation.
        val decay = splineBasedDecay<Float>(this)
        // Wrap in a coroutine scope to use suspend functions for touch events and animation.
        coroutineScope {
            while (true) {
                // Wait for a touch down event.
                val pointerId = awaitPointerEventScope { awaitFirstDown().id }
                // Interrupt any ongoing animation.
                offsetX.stop()
                // Prepare for drag events and record velocity of a fling.
                val velocityTracker = VelocityTracker()
                // Wait for drag events.
                awaitPointerEventScope {
                    horizontalDrag(pointerId) { change ->
                        // Record the position after offset
                        val horizontalDragOffset = offsetX.value + change.positionChange().x
                        launch {
                            // Overwrite the Animatable value while the element is dragged.
                            offsetX.snapTo(horizontalDragOffset)
                        }
                        // Record the velocity of the drag.
                        velocityTracker.addPosition(change.uptimeMillis, change.position)
                        // Consume the gesture event, not passed to external
                        change.consumePositionChange()
                    }
                }
                // Dragging finished. Calculate the velocity of the fling.
                val velocity = velocityTracker.calculateVelocity().x
                // Calculate where the element eventually settles after the fling animation.
                val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
                // The animation should end as soon as it reaches these bounds.
                offsetX.updateBounds(
                    lowerBound = -size.width.toFloat(),
                    upperBound = size.width.toFloat()
                )
                launch {
                    if (targetOffsetX.absoluteValue <= size.width) {
                        // Not enough velocity; Slide back to the default position.
                        offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
                    } else {
                        // Enough velocity to slide away the element to the edge.
                        offsetX.animateDecay(velocity, decay)
                        // The element was swiped away.
                        onDismissed()
                    }
                }
            }
        }
    }
        // Apply the horizontal offset to the element.
        .offset { IntOffset(offsetX.value.roundToInt(), 0) }
}

Exécutez l'application et essayez de balayer l'un des éléments de la tâche. Vous pouvez constater que l'élément revient à sa position par défaut ou disparaît en fonction de la vitesse du déplacement. Vous pouvez également attraper l'élément pendant l'animation.

Animation de type Balayer pour ignorer les éléments

9. Félicitations !

Félicitations ! Vous connaissez maintenant les API Animation de base de Compose.

Dans cet atelier de programmation, nous avons appris à utiliser :

Des API Animation de niveau supérieur :

  • animatedContentSize
  • AnimatedVisibility

Des API Animation de niveau inférieur :

  • animate*AsState pour animer une seule valeur ;
  • updateTransition pour animer plusieurs valeurs ;
  • infiniteTransition pour animer des valeurs indéfiniment ;
  • Animatable pour créer des animations personnalisées en fonction des gestes tactiles.

Et maintenant ?

Consultez les autres ateliers de programmation du parcours Compose.

Pour en savoir plus, consultez la section Animations dans Compose et la documentation de référence suivante :