1. Présentation
Dernière mise à jour : 25/01/2021
Dans cet atelier de programmation, vous allez apprendre à utiliser des API d'animation dans Jetpack Compose.
Jetpack Compose est un kit d'interface utilisateur moderne conçu pour simplifier le développement de l'interface utilisateur. Si vous n'avez jamais utilisé Jetpack Compose, nous vous recommandons 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
- Utilisation de plusieurs API Animation de base
- Quand utiliser une API ?
Prerequisites
- Connaissances de base du langage Kotlin
- Connaissances de base de la rédaction:
- Disposition simple (colonne, ligne, zone, etc.)
- Éléments d'interface utilisateur simples (bouton, texte, etc.)
- États et recomposition
Prérequis
2. Configuration
Téléchargez le code de l'atelier de programmation. Vous pouvez cloner le dépôt comme suit:
$ git clone git@github.com:googlecodelabs/android-compose-codelabs.git
Vous pouvez également télécharger le fichier ZIP.
Importez le projet AnimationCodelab
dans Android Studio.
Le projet comporte plusieurs modules:
start
est l'état initial de l'atelier de programmation.finished
correspond à l'état final de l'application une fois cet atelier de programmation terminé.
Assurez-vous que la règle start
est sélectionné dans le menu déroulant de la configuration d'exécution.
Nous commencerons à travailler sur plusieurs scénarios d'animation dans le chapitre suivant. Chaque extrait de code sur lequel nous travaillons dans cet atelier de programmation est marqué par un commentaire // TODO
. Une bonne astuce consiste à ouvrir la fenêtre de l'outil TODO dans Android Studio et à accéder à chacun des commentaires TODO du chapitre.
3. Animer un changement de valeur simple
Commençons par l'API Animation la plus simple dans Compose.
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 (TODO) : dans la fenêtre de l'outil TODO, découvrez comment cela fonctionne. Il est composable dans Home
.
val backgroundColor = if (tabPage == TabPage.Home) Purple100 else Green300
Ici, tabPage
est un Int
reposant sur un objet State
. Selon sa valeur, l'arrière-plan passe du violet au vert. Nous voulons animer ce changement de valeur.
Afin d'animer un tel changement de valeur, nous pouvons utiliser les API animate*AsState
. Vous pouvez créer une valeur d'animation en encapsulant simplement la valeur qui change avec la variante correspondante des animate*AsState
éléments composables (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(if (tabPage == TabPage.Home) Purple100 else Green300)
Exécutez à nouveau l'application et essayez de changer d'onglet. Le changement de couleur est 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 réduit en fonction de la direction de votre défilement.
Consultez la section TODO 2-1 (À FAIRE 2-1) et découvrez comment cela fonctionne. Il est composable dans 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 un expansion, et le masque en faisant un fondu et un rétrécissement. Ce comportement est particulièrement efficace dans cet exemple avec le bouton d'action flottant, mais nous pouvons également le personnaliser.
Cliquez sur le bouton d'action flottant pour afficher le message "La fonctionnalité de modification n'est pas disponible". Il utilise également AnimatedVisibility
pour animer son apparence et sa disparition. Voyons comment personnaliser l'animation de sorte que l'élément s'affiche en haut et qu'il glisse vers le haut.
Recherchez TODO 2-2 (À FAIRE 2-2) et consultez le code dans le composable EditMessage
.
AnimatedVisibility(
visible = shown
) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colors.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
aux éléments composables AnimatedVisibility
.
Le paramètre enter
doit être une instance de EnterTransition
. Dans cet exemple, nous pouvons utiliser la fonction slideInVertically
pour créer un EnterTransition
. Cette fonction permet de personnaliser davantage les paramètres initialOffsetY
et animationSpec
. Le initialOffsetY
doit être un lambda renvoyant la position initiale. Le lambda reçoit un argument, la hauteur de l'élément, que nous pouvons donc simplement renvoyer sa valeur négative. Lorsque vous utilisez slideInVertically
, le décalage cible pour une diapositive après la 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.
animationSpec
est un paramètre courant pour de nombreuses API d'animation, y compris EnterTransition
et ExitTransition
. Nous pouvons transmettre l'un des différents types AnimationSpec
pour indiquer l'évolution de la valeur d'animation au fil du temps. Dans cet exemple, nous allons utiliser 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 LinearOutSlowInEasing
.
De même, nous pouvons utiliser la fonction slideOutVertically
pour le paramètre exit
. slideOutVertically
suppose que le décalage initial est de 0. Il suffit donc de spécifier targetOffsetY
. Utilisons la même fonction tween
pour le paramètre animationSpec
, mais avec une durée de 250 ms et un lissage de vitesse de 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.colors.secondary,
elevation = 4.dp
) {
Text(
text = stringResource(R.string.edit_message),
modifier = Modifier.padding(16.dp)
)
}
}
Exécutez l'application et cliquez de nouveau sur le bouton d'action flottant. Comme vous pouvez le constater, le message s'affiche maintenant en haut et à droite.
5. Animation de la modification de la taille du contenu
L'application affiche plusieurs sujets dans le contenu. Cliquez sur l'une d'elles pour afficher le corps du texte correspondant à ce thème. La fiche qui contient le texte se développe et réduit lorsque le corps est affiché ou masqué.
Consultez le code de TODO 3 dans le composant TopicRow
.
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// ... the title and the body
}
L'élément Column
composable change de taille à 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. L'animation se réduit et se réduit sous forme d'animation.
6. Animation de plusieurs valeurs
Maintenant que nous connaissons quelques API de base pour l'animation, examinons l'API Transition
qui permet de créer des animations plus complexes. Dans 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 composant Possibilité de lire HomeTabIndicator
et observez 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) Purple700 else Green800
Ici, indicatorLeft
correspond à la position horizontale du bord gauche de l'indicateur sur la ligne d'onglet. indicatorRight
est la position horizontale du bord droit de l'indicateur. La couleur passe également du violet au vert.
Pour animer ces différentes valeurs simultanément, nous pouvons utiliser une Transition
. Vous pouvez créer un Transition
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
. Un bloc lambda est spécifié, et nous pouvons indiquer la valeur cible pour chacun des États. Nous savons déjà à quoi doivent ressembler les valeurs cibles. Il est donc possible d'encapsuler les valeurs comme indiqué ci-dessous. Notez que vous pouvez utiliser une déclaration by
pour la convertir en propriété locale, car les fonctions animate*
renvoient un objet State
.
val transition = updateTransition(tabPage)
val indicatorLeft by transition.animateDp { page ->
tabPositions[page.ordinal].left
}
val indicatorRight by transition.animateDp { page ->
tabPositions[page.ordinal].right
}
val color by transition.animateColor { page ->
if (page == TabPage.Home) Purple700 else Green800
}
Exécutez l'application et vous constaterez que le changement d'onglet est désormais beaucoup plus intéressant. Lorsque l'utilisateur clique sur l'onglet, il modifie la valeur de l'état tabPage
. Par conséquent, toutes les valeurs d'animation associées à transition
commencent à s'animer pour la valeur spécifiée pour l'état cible.
Vous pouvez également spécifier le paramètre transitionSpec
pour personnaliser le comportement de l'animation. Par exemple, pour obtenir un effet élastique pour l'indicateur, le bord se rapproche plus rapidement de l'autre bord. Nous pouvons utiliser la fonction infix 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) Purple700 else Green800
}
Exécutez à nouveau l'application et essayez de changer d'onglet.
Android Studio permet d'inspecter la transition dans la fonctionnalité Rédaction d'aperçu. Pour utiliser l'aperçu de l'animation, démarrez le mode interactif en cliquant sur l'icône "Démarrer le mode interactif" dans l'angle supérieur droit d'un aperçu composite. Activez-la dans les paramètres du test en suivant ces instructions si vous ne la trouvez pas. Essayez de cliquer sur l'icône de l'élément PreviewHomeTabBar
. Cliquez ensuite sur l'icône Démarrer l'inspection de l'animation en haut à droite du mode interactif. Une nouvelle fenêtre s'ouvre.
Pour lancer l'animation, cliquez sur l'icône de lecture. Vous pouvez également faire glisser la barre de recherche pour afficher chacune des images d'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 informations météo les plus récentes (elle se fait passer). 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 animate*
d'extension InfiniteTransition
. Dans le cas présent, nous animons une valeur alpha. Nous allons donc utiliser animatedFloat
. Le paramètre initialValue
doit être 0f
et 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 tous les AnimationSpec
basés sur la durée et les rend reproductibles. 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
)
)
Exécutez l'application et essayez de cliquer sur le bouton "Actualiser". 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. Il existe plusieurs éléments uniques à prendre en compte dans ce scénario. 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 d'informations fiables. En d'autres termes, nous devrons peut-être synchroniser la valeur de l'animation avec les valeurs provenant des événements tactiles.
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 jeté vers le bord de l'écran, nous appelons le rappel onDismissed
pour qu'il puisse être supprimé.
Animatable
est l'API de niveau inférieur que nous avons vue jusqu'à présent. Cet outil présente plusieurs fonctionnalités utiles qui permettent de créer des instances de Animatable
et de les utiliser pour représenter le décalage horizontal de l'élément à faire glisser.
val offsetX = remember { Animatable(0f) } // Add this line
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) {
// ...
C'est là que nous venons de recevoir un événement Retoucher 6-2. Nous devons 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.
// Wait for a touch down event.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // Add this line
// Prepare for drag events and record velocity of a fling.
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. Nous pouvons utiliser snapTo
sur Animatable
.
horizontalDrag(pointerId) { change ->
// Add these 4 lines
val horizontalDragOffset = offsetX.value + change.positionChange().x
launch {
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()
}
L'expression TODO 6-4 désigne l'endroit où l'élément vient d'être libéré et projeté. Nous devons calculer la position finale de l'inclinaison afin de déterminer si nous devons faire glisser l'élément vers la position d'origine, ou le faire glisser et appeler le rappel.
// Dragging finished. Calculate the velocity of the fling.
val velocity = velocityTracker.calculateVelocity().x
// Add this line
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
À TODO 6-5, nous allons lancer l'animation. Avant cela, nous voulons définir les limites de valeur supérieure et inférieure sur le Animatable
afin qu'il s'arrête dès qu'il atteint ces limites. Le modificateur pointerInput
nous permet d'accéder à la taille de l'élément par la propriété size
. Utilisez donc cette valeur pour obtenir nos limites.
offsetX.updateBounds(
lowerBound = -size.width.toFloat(),
upperBound = size.width.toFloat()
)
Enfin, c'est au niveau du commentaire TODO 6-6 que nous pouvons commencer notre animation. Nous allons d'abord comparer la position du terrain de l'élément calculé précédemment et la taille de l'élément. Si la position du règlement est inférieure à la taille, cela signifie que la vitesse de chargement n'était pas suffisante. Nous pouvons utiliser animateTo
pour animer la valeur sur 0f. Sinon, nous utilisons animateDecay
pour lancer l'animation de type Fling. 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.
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
Après cette section, vous vous retrouverez avec 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 glisse à nouveau vers la position par défaut ou disparaît en fonction de la vitesse de chargement. Vous pouvez également attraper l'élément pendant l'animation.
9. Félicitations !
Félicitations ! Vous connaissez maintenant les API Compose Animation de base.
Nous avons appris à créer plusieurs formats d'animation courants à l'aide d'API d'animation de haut niveau, telles que animateContentSize
et AnimatedVisibility
. Nous avons également appris que nous pouvons utiliser animate*AsState
pour animer une seule valeur, updateTransition
pour animer plusieurs valeurs et infiniteTransition
pour animer des valeurs indéfiniment. Nous avons également utilisé Animatable
pour créer une animation personnalisée en combinaison avec des gestes tactiles.
Étapes suivantes
Découvrez les autres ateliers de programmation dans le parcours de rédaction.
Pour en savoir plus, consultez la section Composer des animations et consultez ces documents de référence: