Animation simple avec Jetpack Compose

1. Avant de commencer

Dans cet atelier de programmation, vous allez apprendre à ajouter une animation simple à votre application Android. Les animations permettent de rendre votre application plus interactive, plus intéressante et plus facile à interpréter. Animer des changements spécifiques sur un écran rempli d'informations peut aider l'utilisateur à identifier ce qui change.

Vous pouvez utiliser de nombreux types d'animations dans l'interface utilisateur d'une application. Les éléments peuvent apparaître et disparaître en fondu, se déplacer sur l'écran ou en dehors, ou être transformés de façon intéressante. Cela rend l'interface utilisateur de l'application plus lisible et plus facile à utiliser.

Les animations permettent également d'améliorer l'aspect général de l'application, pour la rendre plus élégante tout en simplifiant son utilisation.

Conditions préalables

  • Vous maîtrisez Kotlin, y compris les fonctions, et composables sans état.
  • Vous disposez de connaissances de base en création de mises en page dans Jetpack Compose.
  • Vous disposez de connaissances de base en création de listes dans Jetpack Compose.
  • Vous disposez de connaissances de base en Material Design.

Points abordés

  • Créer une animation de rétroaction simple avec Jetpack Compose

Objectifs de l'atelier

Ce dont vous avez besoin

  • La dernière version stable d'Android Studio
  • Une connexion Internet pour télécharger le code de démarrage

2. Présentation de l'application

Dans l'atelier de programmation Thématisation Material avec Jetpack Compose, vous avez utilisé Material Design pour créer l'application Woof qui affiche une liste de chiens et les informations correspondantes.

36c6cabd93421a92.png

Dans cet atelier de programmation, vous allez ajouter une animation à l'application Woof. Vous ajouterez des informations sur le passe-temps, qui s'afficheront lorsque l'élément de liste est développé. Vous ajouterez également une animation de rétroaction pour animer l'élément de liste qui est développé.

c0d0a52463332875.gif

Télécharger le code de démarrage

Pour commencer, téléchargez le code de démarrage :

Vous pouvez également cloner le dépôt GitHub du code :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git
$ cd basic-android-kotlin-compose-training-woof
$ git checkout material

Vous pouvez parcourir le code dans le dépôt GitHub Woof app.

3. Ajouter une icône Développer

Dans cette section, vous allez ajouter les icônes Développer 30c384f00846e69b.png et Réduire f88173321938c003.png à votre application.

def59d71015c0fbe.png

Icônes

Les icônes sont des symboles qui aident les utilisateurs à comprendre l'interface utilisateur en communiquant visuellement la fonction souhaitée. Elles s'inspirent souvent d'objets du monde physique que l'utilisateur est supposé connaître. La conception des icônes réduit souvent le niveau de détail au minimum requis pour être reconnues par l'utilisateur. Par exemple, dans le monde physique, un crayon sert à écrire. Une icône de crayon symbolise généralement la création ou la modification d'un élément.

Crayon sur un carnet Photo par Angelina Litvin, publiée sur Unsplash

Icône de crayon en noir et blanc

Material Design propose un certain nombre d'icônes classées dans des catégories courantes répondant à la plupart des besoins.

Bibliothèque d'icônes Material

Ajouter une dépendance Gradle

Ajoutez la dépendance de bibliothèque material-icons-extended à votre projet. Vous utiliserez les icônes Icons.Filled.ExpandLess 30c384f00846e69b.png et Icons.Filled.ExpandMore f88173321938c003.png de cette bibliothèque.

  1. Dans le volet Project (Projet), ouvrez Gradle Scripts > build.gradle.kts (Module :app) (Scripts Gradle > build.gradle.kts (Module :app)).
  2. Faites défiler la page jusqu'à la fin du fichier build.gradle.kts (Module :app). Dans le bloc dependencies{}, ajoutez la ligne suivante :
implementation("androidx.compose.material:material-icons-extended")

Ajouter le composable d'icône

Ajoutez une fonction permettant d'afficher l'icône Développer à partir de la bibliothèque d'icônes Material et de l'utiliser comme bouton.

  1. Dans MainActivity.kt, après la fonction DogItem(), créez une fonction composable nommée DogItemButton().
  2. Transmettez Boolean pour l'état développé, une expression lambda pour le gestionnaire onClick du bouton et un Modifier facultatif, comme suit :
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
) {
 

}
  1. Dans la fonction DogItemButton(), ajoutez un composable IconButton() qui accepte un paramètre nommé onClick, un lambda utilisant la syntaxe lambda de fin, appelé lorsque l'utilisateur appuie sur cette icône, et un modifier facultatif. Définissez les valeurs IconButton's onClick et modifier value parameters sur celles transmises à DogItemButton.
@Composable
private fun DogItemButton(
   expanded: Boolean,
   onClick: () -> Unit,
   modifier: Modifier = Modifier
){
   IconButton(
       onClick = onClick,
       modifier = modifier
   ) {

   }
}
  1. Dans le bloc lambda IconButton(), ajoutez un composable Icon et définissez imageVector value-parameter sur Icons.Filled.ExpandMore. C'est ce qui s'affichera à la fin de l'élément de liste f88173321938c003.png. Android Studio affiche un avertissement pour les paramètres composables Icon() que vous corrigerez à l'étape suivante.
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.Icons
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton

IconButton(
   onClick = onClick,
   modifier = modifier
) {
   Icon(
       imageVector = Icons.Filled.ExpandMore
   )
}
  1. Ajoutez le paramètre de valeur tint et définissez la couleur de l'icône sur MaterialTheme.colorScheme.secondary. Ajoutez le paramètre nommé contentDescription et définissez-le sur la ressource de chaîne R.string.expand_button_content_description.
IconButton(
   onClick = onClick,
   modifier = modifier
){
   Icon(
       imageVector = Icons.Filled.ExpandMore,
       contentDescription = stringResource(R.string.expand_button_content_description),
       tint = MaterialTheme.colorScheme.secondary
   )
}

Afficher l'icône

Affichez le composable DogItemButton() en l'ajoutant à la mise en page.

  1. Au début de DogItem(), ajoutez une var pour enregistrer l'état développé de l'élément de liste. Définissez la valeur initiale sur false.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

var expanded by remember { mutableStateOf(false) }
  1. Affichez le bouton d'icône dans l'élément de liste. Dans le composable DogItem(), à la fin du blocRow, après l'appel à DogInformation(), ajoutez DogItemButton(). Transmettez l'état expanded et un lambda vide pour le rappel. Vous définirez l'action onClick ultérieurement.
Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. Consultez WoofPreview() dans le volet Design (Conception).

5bbf09cd2828b6.png

Notez que le bouton "Développer" n'est pas aligné à la fin des éléments de liste. Vous résoudrez ce problème à l'étape suivante.

Aligner les boutons "Développer"

Pour aligner les boutons de développement à la fin des éléments de liste, vous devez ajouter un espace vide dans la mise en page, avec l'attribut Modifier.weight().

Dans l'application Woof, chaque ligne de la liste contient une image et des informations sur le chien, ainsi qu'un bouton "Développer". Pour aligner correctement les icônes du bouton, vous devez ajouter un composable Spacer avant le bouton "Développer" et lui attribuer une pondération de 1f. Comme cet espace vide est le seul élément enfant pondéré de la ligne, il remplira l'espace restant après avoir mesuré la longueur des autres éléments enfants non pondérés.

733f6d9ef2939ab5.png

Ajouter l'espace à l'élément de liste

  1. Dans DogItem(), entre DogInformation() et DogItemButton(), ajoutez un Spacer. Transmettez un Modifier avec weight(1f). Modifier.weight() permet à l'espace vide de remplir l'espace disponible restant dans la ligne.
import androidx.compose.foundation.layout.Spacer

Row(
   modifier = Modifier
       .fillMaxWidth()
       .padding(dimensionResource(R.dimen.padding_small))
) {
   DogIcon(dog.imageResourceId)
   DogInformation(dog.name, dog.age)
   Spacer(modifier = Modifier.weight(1f))
   DogItemButton(
       expanded = expanded,
       onClick = { /*TODO*/ }
   )
}
  1. Consultez WoofPreview() dans le volet Design (Conception). Notez que le bouton "Développer" est désormais aligné au bout de l'élément de liste.

8df42b9d85a5dbaa.png

4. Ajouter un composable pour afficher le passe-temps

Dans cette tâche, vous allez ajouter des composables Text afin d'afficher des informations sur le passe-temps des chiens.

bba8146c6332cc37.png

  1. Créez une fonction composable appelée DogHobby(), qui récupère l'ID de ressource de chaîne du passe-temps d'un chien ainsi qu'un Modifier facultatif.
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
) {
}
  1. Dans la fonction DogHobby(), créez une Column et transmettez le modificateur transmis à DogHobby().
@Composable
fun DogHobby(
   @StringRes dogHobby: Int,
   modifier: Modifier = Modifier
){
   Column(
       modifier = modifier
   ) { 

   }
}
  1. Dans le bloc Column, ajoutez deux composables Text : un pour afficher le texte About (Passe-temps) au-dessus des informations sur le passe-temps, et l'autre pour afficher les informations en question.

Définissez le text du premier composable sur about à partir du fichier strings.xml et définissez le style sur labelSmall. Définissez le text du second composable sur dogHobby, qui est transmis, et le style sur bodyLarge.

Column(
   modifier = modifier
) {
   Text(
       text = stringResource(R.string.about),
       style = MaterialTheme.typography.labelSmall
   )
   Text(
       text = stringResource(dogHobby),
       style = MaterialTheme.typography.bodyLarge
   )
}
  1. Dans DogItem(), le composable DogHobby() ira en dessous de la Row contenant les éléments DogIcon(), DogInformation(), Spacer() et DogItemButton(). Pour ce faire, encapsulez la Row avec une Column afin que le passe-temps puisse être ajouté sous la Row.
Column() {
   Row(
       modifier = Modifier
           .fillMaxWidth()
           .padding(dimensionResource(R.dimen.padding_small))
   ) {
       DogIcon(dog.imageResourceId)
       DogInformation(dog.name, dog.age)
       Spacer(modifier = Modifier.weight(1f))
       DogItemButton(
           expanded = expanded,
           onClick = { /*TODO*/ }
       )
   }
}
  1. Ajoutez DogHobby() après la Row en tant que deuxième enfant de la Column. Transmettez dog.hobbies, qui contient le passe-temps unique du chien transmis, et un modifier avec la marge intérieure du composable DogHobby().
Column() {
   Row() {
      ...
   }
   DogHobby(
       dog.hobbies,
       modifier = Modifier.padding(
           start = dimensionResource(R.dimen.padding_medium),
           top = dimensionResource(R.dimen.padding_small),
           end = dimensionResource(R.dimen.padding_medium),
           bottom = dimensionResource(R.dimen.padding_medium)
       )
   )
}

Une fois terminée, la fonction DogItem() doit se présenter comme suit :

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       modifier = modifier
   ) {
       Column() {
           Row(
               modifier = Modifier
                   .fillMaxWidth()
                   .padding(dimensionResource(R.dimen.padding_small))
           ) {
               DogIcon(dog.imageResourceId)
               DogInformation(dog.name, dog.age)
               Spacer(Modifier.weight(1f))
               DogItemButton(
                   expanded = expanded,
                   onClick = { /*TODO*/ },
               )
           }
           DogHobby(
               dog.hobbies, 
               modifier = Modifier.padding(
                   start = dimensionResource(R.dimen.padding_medium),
                   top = dimensionResource(R.dimen.padding_small),
                   end = dimensionResource(R.dimen.padding_medium),
                   bottom = dimensionResource(R.dimen.padding_medium)
               )
           )
       }
   }
}
  1. Consultez WoofPreview() dans le volet Design (Conception). Le passe-temps des chiens s'affiche.

Aperçu de Woof avec éléments de liste développés

5. Afficher ou masquer le passe-temps lorsque l'utilisateur appuie sur le bouton

Votre application dispose d'un bouton Développer pour chaque élément de liste, mais ce bouton n'a aucun effet pour l'instant. Dans cette section, vous ajouterez la possibilité d'afficher ou masquer les informations concernant le passe-temps lorsque l'utilisateur appuie sur le bouton "Développer".

  1. Accédez à la fonction composable DogItem(). Dans l'appel de fonction DogItemButton(), définissez l'expression lambda onClick() et appliquez la valeur d'état booléenne true pour expanded lorsque l'utilisateur appuie sur le bouton, et rétablissez false lorsque l'utilisateur appuie de nouveau.
DogItemButton(
   expanded = expanded,
   onClick = { expanded = !expanded }
)
  1. Dans la fonction DogItem(), encapsulez l'appel de fonction DogHobby() avec une vérification if sur la valeur booléenne expanded.
@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   Card(
       ...
   ) {
       Column(
           ...
       ) {
           Row(
               ...
           ) {
               ...
           }
           if (expanded) {
               DogHobby(
                   dog.hobbies, modifier = Modifier.padding(
                       start = dimensionResource(R.dimen.padding_medium),
                       top = dimensionResource(R.dimen.padding_small),
                       end = dimensionResource(R.dimen.padding_medium),
                       bottom = dimensionResource(R.dimen.padding_medium)
                   )
               )
           }
       }
   }
}

Désormais, les informations sur le passe-temps du chien ne s'affichent que si la valeur de expanded correspond à true.

  1. L'aperçu vous permet de voir à quoi ressemble l'interface utilisateur et d'interagir avec. Pour interagir avec l'aperçu de l'interface utilisateur, pointez sur le texte WoofPreview dans le volet Design (Conception), puis cliquez sur le bouton Mode interactif 42379dbe94a7a497.png en haut à droite de ce même volet. L'aperçu passe en mode interactif.

74e1624d68fb4131.png

  1. Interagissez avec l'aperçu en cliquant sur le bouton "Développer". Notez que les informations du passe-temps du chien sont affichées ou masquées lorsque vous cliquez sur le bouton.

Animation de développement et de réduction des éléments de liste Woof

Notez que l'icône du bouton "Développer" reste la même lorsque l'élément de liste est développé. Pour offrir une meilleure expérience utilisateur, vous devez modifier l'icône afin que ExpandMore affiche la flèche vers le bas c761ef298c2aea5a.png, et ExpandLess affiche la flèche vers le haut b380f933be0b6ff4.png.

  1. Dans la fonction DogItemButton(), ajoutez une instruction if qui met à jour la valeur imageVector en fonction de l'état expanded, comme suit :
import androidx.compose.material.icons.filled.ExpandLess

@Composable
private fun DogItemButton(
   ...
) {
   IconButton(onClick = onClick) {
       Icon(
           imageVector = if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
           ...
       )
   }
}

Notez comment vous avez écrit if-else dans l'extrait de code précédent.

if (expanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore

Cela revient à utiliser les accolades { } dans le code suivant :

if (expanded) {

`Icons.Filled.ExpandLess`

} else {

`Icons.Filled.ExpandMore`

}

Les accolades sont facultatives si l'instruction if-else ne comporte qu'une seule ligne de code.

  1. Exécutez l'application sur un appareil ou un émulateur, ou passez à nouveau en mode interactif dans l'aperçu. Notez que l'icône alterne entre ExpandMore  c761ef298c2aea5a.png et ExpandLess  b380f933be0b6ff4.png.

de5dc4a953f11e65.gif

Bravo ! Vous avez mis à jour votre icône.

Avez-vous noté le changement abrupt de hauteur lorsque vous développez un élément de la liste ? Dans une application, ce type de changements brusques ne reflète pas une finition soignée. Pour résoudre ce problème, vous allez ajouter une animation à votre application.

6. Ajouter une animation

Les animations peuvent ajouter des repères visuels pour informer les utilisateurs de ce qui se passe dans votre application. Ils sont particulièrement utiles lorsque l'interface utilisateur change d'état (par exemple lorsque de nouveaux contenus se chargent ou que de nouvelles actions deviennent disponibles). Les animations permettent également d'améliorer l'aspect général de l'application.

Dans cette section, vous ajouterez une animation de rétroaction au changement de hauteur de l'élément de liste.

Animation de rétroaction

Une animation de rétroaction s'inspire des lois de la physique, et plus spécifiquement de la force de tension. Avec une animation de rétroaction, la valeur et la vitesse du mouvement sont calculées en fonction de la tension appliquée.

Par exemple, si vous faites glisser une icône d'application sur l'écran, puis la relâchez en relevant votre doigt, l'icône revient à son emplacement d'origine sous l'effet d'une force invisible.

L'animation suivante illustre l'effet de rétroaction. Lorsque l'icône est relâchée, elle revient à sa place, comme poussée par un ressort.

Animation illustrant l'effet de rétroaction lorsque l'icône est relâchée

Effet de rétroaction

La force de tension ("effet ressort") est déterminée par les deux propriétés suivantes :

  • Taux d'amortissement : la résistance à la déformation, qui limite les rebonds du ressort.
  • Niveau de raideur : la résistance à l'étirement, qui définit la vitesse du ressort en fin de course.

Vous trouverez ci-dessous quelques exemples d'animations avec différents paramètres d'amortissement et de raideur.

Effet de rétroactionRebond élevé

Effet de rétroactionAucun rebond

Raideur élevée

Raideur faible Raideur très faible

Examinez l'appel de fonction DogHobby() dans la fonction composable DogItem(). Les informations concernant le passe-temps du chien sont incluses dans la composition, en fonction de la valeur booléenne expanded. La hauteur de l'élément de liste change, selon que les informations de passe-temps sont affichées ou masquées. Pour le moment, la transition est brusque. Dans cette section, vous allez utiliser le modificateur animateContentSize pour ajouter une transition plus fluide entre les états développé et non développé.

// No need to copy over
@Composable
fun DogItem(...) {
  ...
    if (expanded) {
       DogHobby(
          dog.hobbies, 
          modifier = Modifier.padding(
              start = dimensionResource(R.dimen.padding_medium),
              top = dimensionResource(R.dimen.padding_small),
              end = dimensionResource(R.dimen.padding_medium),
              bottom = dimensionResource(R.dimen.padding_medium)
          )
      )
   }
}
  1. Accédez à MainActivity.kt. Dans DogItem(), ajoutez un paramètre modifier à la mise en page Column.
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
          modifier = Modifier
       ){
           ...
       }
   }
}
  1. Enchaînez ce modificateur au modificateur animateContentSize pour animer le changement de dimensions (hauteur de l'élément de liste).
import androidx.compose.animation.animateContentSize

Column(
   modifier = Modifier
       .animateContentSize()
)

L'implémentation actuelle anime la hauteur de l'élément de liste dans votre application. Cependant, l'animation est si subtile qu'il est difficile de la discerner lorsque vous exécutez l'application. Pour résoudre ce problème, utilisez le paramètre facultatif animationSpec, qui permet de personnaliser l'animation.

  1. Pour Woof, l'animation ne comporte aucun rebond. Ajoutez le paramètre animationSpec à l'appel de fonction animateContentSize(). Définissez-le sur une animation de rétroaction avec DampingRatioNoBouncy pour qu'il n'y ait pas de rebond et définissez un paramètre StiffnessMedium afin de rendre le rebond un peu plus rigide.
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

Column(
   modifier = Modifier
       .animateContentSize(
           animationSpec = spring(
               dampingRatio = Spring.DampingRatioNoBouncy,
               stiffness = Spring.StiffnessMedium
           )
       )
)
  1. Consultez WoofPreview() dans le volet Design (Conception) et utilisez le mode interactif, ou exécutez votre application sur un émulateur ou un appareil pour tester votre animation de rétroaction.

c0d0a52463332875.gif

Bravo ! Profitez de votre superbe application avec des animations.

7. Tester d'autres animations (facultatif)

animate*AsState

Les fonctions animate*AsState() comptent parmi les API d'animation les plus simples dans Compose. Elles permettent d'animer une seule valeur. Vous fournissez uniquement la valeur finale (ou la valeur cible), et l'API lance une animation menant de la valeur actuelle à la valeur spécifiée.

Compose fournit des fonctions animate*AsState() pour Float, Color, Dp, Size, Offset et Int, entre autres. Vous pouvez facilement prendre en charge d'autres types de données en utilisant animateValueAsState(), qui utilise un type générique.

Essayez d'utiliser la fonction animateColorAsState() pour modifier la couleur lorsqu'un élément de liste est développé.

  1. Dans DogItem(), déclarez une couleur et déléguez son initialisation à la fonction animateColorAsState().
import androidx.compose.animation.animateColorAsState

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState()
   ...
}
  1. Définissez le paramètre nommé targetValue en fonction de la valeur booléenne expanded. Si l'élément de liste est développé, définissez sa couleur sur tertiaryContainer. Sinon, définissez-la sur primaryContainer.
import androidx.compose.animation.animateColorAsState

@Composable
fun DogItem(
   dog: Dog,
   modifier: Modifier = Modifier
) {
   var expanded by remember { mutableStateOf(false) }
   val color by animateColorAsState(
       targetValue = if (expanded) MaterialTheme.colorScheme.tertiaryContainer
       else MaterialTheme.colorScheme.primaryContainer,
   )
   ...
}
  1. Définissez color comme modificateur d'arrière-plan sur Column.
@Composable
fun DogItem(
   dog: Dog, 
   modifier: Modifier = Modifier
) {
   ...
   Card(
       ...
   ) {
       Column(
           modifier = Modifier
               .animateContentSize(
                   ...
                   )
               )
               .background(color = color)
       ) {...}
}
  1. Découvrez comment la couleur change lorsque l'élément de liste est développé. Les éléments de liste non développés sont de couleur primaryContainer, tandis que les éléments de liste développés sont de couleur tertiaryContainer.

Animation animateAsState

8. Télécharger le code de solution

Pour télécharger le code de l'atelier de programmation terminé, utilisez la commande Git suivante :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-woof.git

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous souhaitez voir le code de solution, affichez-le sur GitHub.

9. Conclusion

Félicitations ! Vous avez ajouté un bouton pour afficher et masquer les informations concernant les chiens. Vous avez amélioré l'expérience utilisateur grâce aux animations de rétroaction. Vous avez également appris à utiliser le mode interactif dans le volet Design (Conception).

Vous pouvez également explorer un autre type d'animation Jetpack Compose. N'oubliez pas de partager le fruit de vos efforts sur les réseaux sociaux avec le hashtag #AndroidBasics.

En savoir plus