1. Introduction
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.
- Principes de base de Jetpack Compose
- Mises en page dans Jetpack Compose
- Utiliser l'état dans Jetpack Compose
Points abordés
- Comment utiliser plusieurs API Animation de base ?
Conditions préalables
- Connaissances de base en Kotlin
- Connaissances de base de Compose, dont :
- la mise en page simple (colonne, ligne, case, etc.) ;
- les éléments d'interface utilisateur simples (bouton, texte, etc.) ;
- les états et la recomposition.
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.
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.
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.
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.
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é.
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.
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.
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.
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 est correcte, mais pas tout à fait parfaite. En effet, slideInVertically
et slideOutVertically
utilisent par défaut la moitié de la hauteur de l'élément.
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 :
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 :
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é.
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.
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é.
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.
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.
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 ). 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*
.
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.
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.
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.
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 :