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.

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.
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).
// 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 +.
@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.
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.
@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.
// É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
}
}Prêt à réussir tes entretiens Android ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
@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.
@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
)
}
}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.
@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.
@Composable
fun AnimatedTaskList(
tasks: List<Task>,
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)
}
}
}
}
}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.
@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)
)
}
}Prêt à réussir tes entretiens Android ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
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.
@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.
@Composable
fun PerformantAnimatedList(
items: List<Item>,
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
updateTransitionpour coordonner plusieurs animations liées - ✅ Préférer
springpour des animations naturelles ettweenpour des durées précises - ✅ Toujours fournir un paramètre
keystable pour les animations de liste - ✅ Optimiser avec
graphicsLayerpour é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.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Top 20 questions d'entretien Jetpack Compose en 2026
Les 20 questions les plus posées en entretien sur Jetpack Compose : recomposition, state, navigation, performance et architecture.

Kotlin 2.3 pour Android : Destructuration par Nom, KMP et Questions d'Entretien 2026
Questions d'entretien Kotlin 2.3 pour développeurs Android en 2026. Destructuration par nom, KMP, paramètres de contexte, Flow et coroutines avec exemples de code.

MVVM vs MVI : Quelle architecture choisir en 2026 ?
Comparaison détaillée entre MVVM et MVI sur Android : avantages, inconvénients, cas d'usage et guide pour choisir la bonne architecture en 2026.