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.

Animations avancées Jetpack Compose pour développeurs Android

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).

AnimationLevels.ktkotlin
// 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 +.

AnimatedVisibilityExample.ktkotlin
@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.

AnimateAsStateExample.ktkotlin
@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.

TransitionExample.ktkotlin
// É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.

AnimatableExample.ktkotlin
@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.

AnimatedContentExample.ktkotlin
@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.

InfiniteTransitionExample.ktkotlin
@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.

ListAnimationExample.ktkotlin
@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)
                }
            }
        }
    }
}
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.

CanvasAnimationExample.ktkotlin
@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.

PerformanceOptimization.ktkotlin
@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.

ListPerformance.ktkotlin
@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 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.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#jetpack compose
#android
#animations
#kotlin
#ui

Partager

Articles similaires