# Jetpack Compose : Animations avancées pas à pas > Guide complet des animations avancées en Jetpack Compose : transitions, AnimatedVisibility, Animatable, gestures et performance pour des interfaces fluides. - Published: 2026-02-08 - Updated: 2026-03-28 - Author: SharpSkill - Tags: jetpack compose, android, animations, kotlin, ui - Reading time: 12 min --- Les animations transforment une application fonctionnelle en une expérience utilisateur mémorable. Jetpack Compose propose une API d'animation déclarative puissante qui simplifie considérablement la création d'interfaces fluides. Ce guide explore les techniques avancées pour créer des animations performantes et maintenables. > **Prérequis** > > Ce tutoriel suppose une connaissance des bases de Compose (recomposition, state, modifiers). Pour les fondamentaux, consulter d'abord le guide des questions d'entretien Jetpack Compose. ## Les fondamentaux des animations Compose Compose offre plusieurs niveaux d'API pour les animations. Le choix dépend du niveau de contrôle souhaité et de la complexité de l'animation. L'API se divise en trois catégories principales : les animations de haut niveau (`AnimatedVisibility`, `AnimatedContent`), les animations d'état (`animate*AsState`), et les animations de bas niveau (`Animatable`, `Transition`). ```kotlin // AnimationLevels.kt // Vue d'ensemble des trois niveaux d'API animation @Composable fun AnimationApiOverview() { // Haut niveau : animations prédéfinies simples AnimatedVisibility(visible = isVisible) { Text("Contenu animé") } // Niveau intermédiaire : animation liée à un état val alpha by animateFloatAsState( targetValue = if (isSelected) 1f else 0.5f, label = "alpha" ) // Bas niveau : contrôle total sur l'animation val animatable = remember { Animatable(0f) } LaunchedEffect(targetValue) { animatable.animateTo(targetValue) } } ``` Le choix du bon niveau d'API est crucial pour maintenir un code lisible tout en conservant la flexibilité nécessaire. ## AnimatedVisibility : apparitions et disparitions élégantes `AnimatedVisibility` est le point d'entrée idéal pour animer l'entrée et la sortie d'éléments. Cette API gère automatiquement la composition et décomposition du contenu. Les paramètres `enter` et `exit` acceptent des combinaisons de transitions qui définissent le comportement de l'animation. Ces transitions peuvent être combinées avec l'opérateur `+`. ```kotlin // AnimatedVisibilityExample.kt @Composable fun ExpandableCard( title: String, content: String, modifier: Modifier = Modifier ) { var isExpanded by remember { mutableStateOf(false) } Card( modifier = modifier.clickable { isExpanded = !isExpanded } ) { Column(modifier = Modifier.padding(16.dp)) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text(text = title, style = MaterialTheme.typography.titleMedium) Icon( imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, contentDescription = null ) } // Animation d'expansion avec fade + slide AnimatedVisibility( visible = isExpanded, enter = fadeIn(animationSpec = tween(300)) + expandVertically(animationSpec = tween(300)), exit = fadeOut(animationSpec = tween(200)) + shrinkVertically(animationSpec = tween(200)) ) { Text( text = content, modifier = Modifier.padding(top = 12.dp), style = MaterialTheme.typography.bodyMedium ) } } } } ``` Le contenu à l'intérieur d'`AnimatedVisibility` n'est composé que lorsque `visible = true`, ce qui optimise les performances pour les listes contenant de nombreux éléments extensibles. > **Combinaison de transitions** > > Les transitions disponibles incluent `fadeIn/fadeOut`, `slideIn/slideOut`, `expandIn/shrinkOut`, `scaleIn/scaleOut`. Elles peuvent être combinées librement pour créer des effets personnalisés. ## animate*AsState : animations liées à l'état La famille de fonctions `animate*AsState` permet d'animer automatiquement les changements de valeurs primitives. C'est l'approche la plus idiomatique pour les animations simples en Compose. Chaque type de donnée possède sa fonction dédiée : `animateColorAsState`, `animateFloatAsState`, `animateDpAsState`, `animateIntAsState`, etc. ```kotlin // AnimateAsStateExample.kt @Composable fun InteractiveButton( isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier ) { // Couleur de fond animée val backgroundColor by animateColorAsState( targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant, animationSpec = tween(durationMillis = 250), label = "backgroundColor" ) // Élévation animée val elevation by animateDpAsState( targetValue = if (isSelected) 8.dp else 2.dp, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ), label = "elevation" ) // Taille du texte animée val textSize by animateFloatAsState( targetValue = if (isSelected) 18f else 14f, label = "textSize" ) Surface( modifier = modifier.clickable(onClick = onClick), color = backgroundColor, shadowElevation = elevation, shape = RoundedCornerShape(12.dp) ) { Text( text = if (isSelected) "Sélectionné" else "Sélectionner", modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp), fontSize = textSize.sp ) } } ``` Le paramètre `animationSpec` contrôle le comportement temporel de l'animation. Les deux specs les plus utilisées sont `tween` (durée fixe avec easing) et `spring` (physique réaliste avec rebond). ## Transition : orchestrer plusieurs animations Lorsque plusieurs propriétés doivent être animées de manière coordonnée, `updateTransition` offre un contrôle centralisé. Cette API garantit que toutes les animations restent synchronisées. Le pattern consiste à définir un état enum, puis à créer des animations pour chaque propriété qui dépend de cet état. ```kotlin // TransitionExample.kt // État de la carte : définit le comportement visuel enum class CardState { Collapsed, Expanded, Selected } @Composable fun AnimatedStateCard( cardState: CardState, modifier: Modifier = Modifier ) { // Transition centrale qui coordonne toutes les animations val transition = updateTransition( targetState = cardState, label = "cardTransition" ) // Hauteur de la carte selon l'état val cardHeight by transition.animateDp( transitionSpec = { spring(stiffness = Spring.StiffnessLow) }, label = "height" ) { state -> when (state) { CardState.Collapsed -> 80.dp CardState.Expanded -> 200.dp CardState.Selected -> 160.dp } } // Couleur de bordure selon l'état val borderColor by transition.animateColor( transitionSpec = { tween(300) }, label = "borderColor" ) { state -> when (state) { CardState.Collapsed -> Color.Transparent CardState.Expanded -> MaterialTheme.colorScheme.outline CardState.Selected -> MaterialTheme.colorScheme.primary } } // Rayon des coins selon l'état val cornerRadius by transition.animateDp( label = "cornerRadius" ) { state -> when (state) { CardState.Collapsed -> 8.dp CardState.Expanded -> 16.dp CardState.Selected -> 24.dp } } Card( modifier = modifier .height(cardHeight) .border(2.dp, borderColor, RoundedCornerShape(cornerRadius)), shape = RoundedCornerShape(cornerRadius) ) { // Contenu de la carte } } ``` ## Animatable : contrôle total sur l'animation `Animatable` est l'API de bas niveau qui offre un contrôle programmatique complet. Cette approche est nécessaire pour les animations interruptibles, les gestures, ou les scénarios complexes. Contrairement à `animate*AsState`, `Animatable` permet d'arrêter, inverser ou modifier une animation en cours sans attendre sa fin. ```kotlin // AnimatableExample.kt @Composable fun SwipeableCard( onDismiss: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit ) { // Offset horizontal contrôlé par Animatable val offsetX = remember { Animatable(0f) } val scope = rememberCoroutineScope() // Seuil de swipe pour déclencher la suppression val dismissThreshold = 300f Box( modifier = modifier .offset { IntOffset(offsetX.value.roundToInt(), 0) } .pointerInput(Unit) { detectHorizontalDragGestures( onDragEnd = { scope.launch { if (abs(offsetX.value) > dismissThreshold) { // Animation vers l'extérieur puis callback val target = if (offsetX.value > 0) 1000f else -1000f offsetX.animateTo( targetValue = target, animationSpec = tween(200) ) onDismiss() } else { // Retour à la position initiale avec spring offsetX.animateTo( targetValue = 0f, animationSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy ) ) } } }, onHorizontalDrag = { _, dragAmount -> scope.launch { // snapTo pour un suivi instantané du doigt offsetX.snapTo(offsetX.value + dragAmount) } } ) } ) { content() } } ``` Les méthodes clés d'`Animatable` sont `animateTo()` (animation vers une cible), `snapTo()` (changement instantané), et `stop()` (interruption). ## AnimatedContent : transitions de contenu `AnimatedContent` anime les transitions entre différents contenus. Cette API est parfaite pour les changements d'état qui modifient complètement l'UI affichée. La clé de `targetState` détermine quand une transition doit se produire. Le `transitionSpec` définit comment le contenu sortant et entrant interagissent. ```kotlin // AnimatedContentExample.kt @Composable fun CounterWithAnimation( count: Int, modifier: Modifier = Modifier ) { AnimatedContent( targetState = count, modifier = modifier, transitionSpec = { // Déterminer la direction de l'animation val direction = if (targetState > initialState) { // Nouveau nombre entre par le haut slideInVertically { height -> -height } + fadeIn() togetherWith slideOutVertically { height -> height } + fadeOut() } else { // Nouveau nombre entre par le bas slideInVertically { height -> height } + fadeIn() togetherWith slideOutVertically { height -> -height } + fadeOut() } direction.using(SizeTransform(clip = false)) }, label = "counter" ) { targetCount -> Text( text = "$targetCount", style = MaterialTheme.typography.displayLarge, fontWeight = FontWeight.Bold ) } } ``` > **Performance avec AnimatedContent** > > Le contenu à l'intérieur d'`AnimatedContent` est recomposé à chaque changement de `targetState`. Pour des contenus complexes, envisager de mémoriser les éléments coûteux ou d'utiliser une stratégie de clé appropriée. ## Animations infinies avec rememberInfiniteTransition Pour les animations en boucle (indicateurs de chargement, effets de pulsation), `rememberInfiniteTransition` fournit une API dédiée qui ne nécessite pas de gestion manuelle du cycle. ```kotlin // InfiniteTransitionExample.kt @Composable fun PulsingDot( color: Color = MaterialTheme.colorScheme.primary, modifier: Modifier = Modifier ) { val infiniteTransition = rememberInfiniteTransition(label = "pulse") // Animation d'échelle en boucle val scale by infiniteTransition.animateFloat( initialValue = 0.8f, targetValue = 1.2f, animationSpec = infiniteRepeatable( animation = tween(600, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Reverse ), label = "scale" ) // Animation d'opacité synchronisée val alpha by infiniteTransition.animateFloat( initialValue = 0.5f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(600, easing = FastOutSlowInEasing), repeatMode = RepeatMode.Reverse ), label = "alpha" ) Box( modifier = modifier .size(24.dp) .scale(scale) .alpha(alpha) .background(color = color, shape = CircleShape) ) } ``` ## Animations de liste avec LazyColumn Les animations d'éléments de liste nécessitent une attention particulière. Le modifier `animateItem()` (anciennement `animateItemPlacement`) permet d'animer automatiquement les réorganisations. ```kotlin // ListAnimationExample.kt @Composable fun AnimatedTaskList( tasks: List, onToggle: (Task) -> Unit, onDelete: (Task) -> Unit, modifier: Modifier = Modifier ) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp) ) { items( items = tasks, key = { it.id } // Clé stable obligatoire pour animateItem ) { task -> var isVisible by remember { mutableStateOf(true) } // Animation de sortie avant suppression AnimatedVisibility( visible = isVisible, exit = shrinkVertically() + fadeOut() ) { TaskItem( task = task, onToggle = { onToggle(task) }, onDelete = { isVisible = false // Délai pour laisser l'animation se terminer }, modifier = Modifier.animateItem( fadeInSpec = tween(300), fadeOutSpec = tween(300), placementSpec = spring( dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow ) ) ) } // Déclencher la suppression après l'animation LaunchedEffect(isVisible) { if (!isVisible) { delay(300) onDelete(task) } } } } } ``` > **Clés stables pour les animations de liste** > > Sans paramètre `key` stable, `animateItem` ne peut pas suivre les éléments entre les recompositions. Utiliser un ID unique plutôt que l'index de la liste. ## Animations avec Canvas Pour des effets visuels personnalisés, combiner `Canvas` avec des valeurs animées offre une flexibilité totale. ```kotlin // CanvasAnimationExample.kt @Composable fun AnimatedProgressRing( progress: Float, // 0f à 1f modifier: Modifier = Modifier ) { // Animation du progrès avec spring pour un effet naturel val animatedProgress by animateFloatAsState( targetValue = progress, animationSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessVeryLow ), label = "progress" ) // Animation de rotation continue val infiniteTransition = rememberInfiniteTransition(label = "rotation") val rotation by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 360f, animationSpec = infiniteRepeatable( animation = tween(2000, easing = LinearEasing) ), label = "rotation" ) val primaryColor = MaterialTheme.colorScheme.primary val trackColor = MaterialTheme.colorScheme.surfaceVariant Canvas( modifier = modifier .size(120.dp) .rotate(rotation) ) { val strokeWidth = 12.dp.toPx() val radius = (size.minDimension - strokeWidth) / 2 // Cercle de fond (track) drawCircle( color = trackColor, radius = radius, style = Stroke(width = strokeWidth, cap = StrokeCap.Round) ) // Arc de progression animé drawArc( color = primaryColor, startAngle = -90f, sweepAngle = animatedProgress * 360f, useCenter = false, style = Stroke(width = strokeWidth, cap = StrokeCap.Round), topLeft = Offset(strokeWidth / 2, strokeWidth / 2), size = Size(radius * 2, radius * 2) ) } } ``` ## Optimisation des performances d'animation Les animations mal optimisées peuvent causer des saccades et drainer la batterie. Voici les bonnes pratiques pour maintenir 60 FPS. La première règle est d'éviter les allocations pendant l'animation. Utiliser `graphicsLayer` plutôt que des modifiers qui déclenchent des recompositions. ```kotlin // PerformanceOptimization.kt @Composable fun OptimizedAnimatedCard( isExpanded: Boolean, modifier: Modifier = Modifier ) { val scale by animateFloatAsState( targetValue = if (isExpanded) 1.1f else 1f, label = "scale" ) val alpha by animateFloatAsState( targetValue = if (isExpanded) 1f else 0.8f, label = "alpha" ) Card( modifier = modifier // ✅ graphicsLayer : modifications GPU sans recomposition .graphicsLayer { scaleX = scale scaleY = scale this.alpha = alpha } // ❌ Éviter : .scale(scale).alpha(alpha) // Ces modifiers déclenchent des recompositions ) { Text("Contenu de la carte") } } // Exemple avec lambda remember pour éviter les allocations @Composable fun OptimizedClickableItem( onClick: () -> Unit, content: @Composable () -> Unit ) { // ✅ Lambda stable mémorisée val interactionSource = remember { MutableInteractionSource() } Box( modifier = Modifier .clickable( interactionSource = interactionSource, indication = ripple(), onClick = onClick ) ) { content() } } ``` Le deuxième point critique concerne les animations dans les listes. Limiter le nombre d'animations simultanées et utiliser `derivedStateOf` pour les calculs dérivés. ```kotlin // ListPerformance.kt @Composable fun PerformantAnimatedList( items: List, modifier: Modifier = Modifier ) { // Calculer une seule fois si la liste est vide val isEmpty by remember { derivedStateOf { items.isEmpty() } } LazyColumn(modifier = modifier) { items( items = items, key = { it.id } ) { item -> // Animation légère uniquement sur l'apparition initiale var hasAppeared by remember { mutableStateOf(false) } LaunchedEffect(Unit) { hasAppeared = true } val alpha by animateFloatAsState( targetValue = if (hasAppeared) 1f else 0f, animationSpec = tween(200), label = "itemAlpha" ) ItemCard( item = item, modifier = Modifier.graphicsLayer { this.alpha = alpha } ) } } } ``` ## Conclusion Les animations en Jetpack Compose offrent un équilibre entre simplicité d'utilisation et contrôle avancé. Voici les points clés à retenir : - ✅ Choisir le bon niveau d'API selon la complexité (AnimatedVisibility → animate*AsState → Animatable) - ✅ Utiliser `updateTransition` pour coordonner plusieurs animations liées - ✅ Préférer `spring` pour des animations naturelles et `tween` pour des durées précises - ✅ Toujours fournir un paramètre `key` stable pour les animations de liste - ✅ Optimiser avec `graphicsLayer` pour éviter les recompositions inutiles - ✅ Tester les animations sur des appareils réels pour valider les performances La maîtrise des animations Compose distingue les applications Android professionnelles. Ces techniques, combinées à une attention portée aux performances, permettent de créer des expériences utilisateur fluides et engageantes. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/android/jetpack-compose-animations-avancees