Jetpack Compose: Geavanceerde Animaties Stap voor Stap

Volledige gids voor geavanceerde Compose-animaties: transities, AnimatedVisibility, Animatable, gestures en performance voor vloeiende Android-interfaces.

Geavanceerde Jetpack Compose-animaties voor Android-ontwikkelaars

Animaties veranderen een functionele app in een memorabele gebruikerservaring. Jetpack Compose biedt een krachtige declaratieve animatie-API die het maken van vloeiende interfaces aanzienlijk vereenvoudigt. Deze gids verkent geavanceerde technieken voor performante en onderhoudbare animaties.

Vereisten

Deze tutorial gaat uit van bekendheid met de Compose-basis (recomposition, state, modifiers). Voor de fundamenten loont het om eerst de gids met Jetpack Compose-sollicitatievragen te raadplegen.

Fundamenten van de Animatie-API in Compose

Compose biedt verschillende API-niveaus voor animaties. De keuze hangt af van de gewenste mate van controle en de complexiteit van de animatie.

De API valt uiteen in drie hoofdcategorieën: high-level animaties (AnimatedVisibility, AnimatedContent), op state gebaseerde animaties (animate*AsState) en low-level animaties (Animatable, Transition).

AnimationLevels.ktkotlin
// Overview of the three animation API levels
@Composable
fun AnimationApiOverview() {
    // High level: simple predefined animations
    AnimatedVisibility(visible = isVisible) {
        Text("Animated content")
    }

    // Intermediate level: state-driven animation
    val alpha by animateFloatAsState(
        targetValue = if (isSelected) 1f else 0.5f,
        label = "alpha"
    )

    // Low level: full control over animation
    val animatable = remember { Animatable(0f) }
    LaunchedEffect(targetValue) {
        animatable.animateTo(targetValue)
    }
}

Het juiste API-niveau kiezen is cruciaal om leesbare code te behouden zonder de benodigde flexibiliteit te verliezen.

AnimatedVisibility: Elegante Enter- en Exit-animaties

AnimatedVisibility is het ideale beginpunt om het verschijnen en verdwijnen van elementen te animeren. Deze API regelt automatisch de compositie en decompositie van de inhoud.

De parameters enter en exit accepteren combinaties van transities die het animatiegedrag bepalen. Deze transities kunnen worden gecombineerd met de +-operator.

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

            // Expansion animation with 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
                )
            }
        }
    }
}

De inhoud binnen AnimatedVisibility wordt alleen samengesteld wanneer visible = true, wat de prestaties optimaliseert in lijsten met veel uitklapbare items.

Transities combineren

Beschikbare transities zijn onder andere fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut, scaleIn/scaleOut. Ze kunnen vrijelijk worden gecombineerd om eigen effecten te creëren.

animate*AsState: Op State Gebaseerde Animaties

De functiefamilie animate*AsState animeert wijzigingen van primitieve waarden automatisch. Dit is de meest idiomatische aanpak voor eenvoudige animaties in Compose.

Elk datatype heeft een eigen functie: animateColorAsState, animateFloatAsState, animateDpAsState, animateIntAsState, enzovoort.

AnimateAsStateExample.ktkotlin
@Composable
fun InteractiveButton(
    isSelected: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    // Animated background color
    val backgroundColor by animateColorAsState(
        targetValue = if (isSelected) MaterialTheme.colorScheme.primary
                      else MaterialTheme.colorScheme.surfaceVariant,
        animationSpec = tween(durationMillis = 250),
        label = "backgroundColor"
    )

    // Animated elevation
    val elevation by animateDpAsState(
        targetValue = if (isSelected) 8.dp else 2.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        ),
        label = "elevation"
    )

    // Animated text size
    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) "Selected" else "Select",
            modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
            fontSize = textSize.sp
        )
    }
}

De parameter animationSpec regelt het tijdsverloop van de animatie. De twee meestgebruikte specs zijn tween (vaste duur met easing) en spring (realistische fysica met bounce).

Transition: Meerdere Animaties Orkestreren

Wanneer meerdere eigenschappen gecoördineerd moeten worden geanimeerd, biedt updateTransition centrale controle. Deze API zorgt ervoor dat alle animaties gesynchroniseerd blijven.

Het patroon bestaat uit het definiëren van een enum-state en vervolgens animaties maken voor elke eigenschap die van die state afhangt.

TransitionExample.ktkotlin
// Card state: defines visual behavior
enum class CardState { Collapsed, Expanded, Selected }

@Composable
fun AnimatedStateCard(
    cardState: CardState,
    modifier: Modifier = Modifier
) {
    // Central transition coordinating all animations
    val transition = updateTransition(
        targetState = cardState,
        label = "cardTransition"
    )

    // Card height based on state
    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
        }
    }

    // Border color based on state
    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
        }
    }

    // Corner radius based on state
    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)
    ) {
        // Card content
    }
}

Klaar om je Android gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Animatable: Volledige Controle over de Animatie

Animatable is de low-level API die volledige programmatische controle biedt. Deze aanpak is nodig voor onderbreekbare animaties, gestures of complexe scenario's.

In tegenstelling tot animate*AsState maakt Animatable het mogelijk om een lopende animatie te stoppen, om te keren of aan te passen zonder te wachten tot deze is voltooid.

AnimatableExample.ktkotlin
@Composable
fun SwipeableCard(
    onDismiss: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    // Horizontal offset controlled by Animatable
    val offsetX = remember { Animatable(0f) }
    val scope = rememberCoroutineScope()

    // Swipe threshold to trigger dismissal
    val dismissThreshold = 300f

    Box(
        modifier = modifier
            .offset { IntOffset(offsetX.value.roundToInt(), 0) }
            .pointerInput(Unit) {
                detectHorizontalDragGestures(
                    onDragEnd = {
                        scope.launch {
                            if (abs(offsetX.value) > dismissThreshold) {
                                // Animate out then callback
                                val target = if (offsetX.value > 0) 1000f else -1000f
                                offsetX.animateTo(
                                    targetValue = target,
                                    animationSpec = tween(200)
                                )
                                onDismiss()
                            } else {
                                // Return to initial position with spring
                                offsetX.animateTo(
                                    targetValue = 0f,
                                    animationSpec = spring(
                                        dampingRatio = Spring.DampingRatioMediumBouncy
                                    )
                                )
                            }
                        }
                    },
                    onHorizontalDrag = { _, dragAmount ->
                        scope.launch {
                            // snapTo for instant finger tracking
                            offsetX.snapTo(offsetX.value + dragAmount)
                        }
                    }
                )
            }
    ) {
        content()
    }
}

De belangrijkste methoden van Animatable zijn animateTo() (naar een doel animeren), snapTo() (directe wijziging) en stop() (onderbreking).

AnimatedContent: Inhoudstransities

AnimatedContent animeert overgangen tussen verschillende inhoud. Deze API is ideaal voor state-wijzigingen die de getoonde UI volledig veranderen.

De sleutel targetState bepaalt wanneer een transitie moet plaatsvinden. transitionSpec definieert hoe de uitgaande en inkomende inhoud op elkaar inwerken.

AnimatedContentExample.ktkotlin
@Composable
fun CounterWithAnimation(
    count: Int,
    modifier: Modifier = Modifier
) {
    AnimatedContent(
        targetState = count,
        modifier = modifier,
        transitionSpec = {
            // Determine animation direction
            val direction = if (targetState > initialState) {
                // New number enters from top
                slideInVertically { height -> -height } + fadeIn() togetherWith
                slideOutVertically { height -> height } + fadeOut()
            } else {
                // New number enters from bottom
                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 bij AnimatedContent

De inhoud binnen AnimatedContent wordt opnieuw samengesteld bij elke wijziging van targetState. Voor complexe inhoud loont het om dure elementen te memoiseren of een passende key-strategie te gebruiken.

Oneindige Animaties met rememberInfiniteTransition

Voor herhalende animaties (laadindicators, pulserende effecten) biedt rememberInfiniteTransition een toegewijde API die geen handmatig cyclusbeheer vereist.

InfiniteTransitionExample.ktkotlin
@Composable
fun PulsingDot(
    color: Color = MaterialTheme.colorScheme.primary,
    modifier: Modifier = Modifier
) {
    val infiniteTransition = rememberInfiniteTransition(label = "pulse")

    // Looping scale animation
    val scale by infiniteTransition.animateFloat(
        initialValue = 0.8f,
        targetValue = 1.2f,
        animationSpec = infiniteRepeatable(
            animation = tween(600, easing = FastOutSlowInEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "scale"
    )

    // Synchronized opacity animation
    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)
    )
}

Lijstanimaties met LazyColumn

Animaties van lijstitems vragen om bijzondere aandacht. De modifier animateItem() (voorheen animateItemPlacement) animeert herordeningen automatisch.

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 }  // Stable key required for animateItem
        ) { task ->
            var isVisible by remember { mutableStateOf(true) }

            // Exit animation before deletion
            AnimatedVisibility(
                visible = isVisible,
                exit = shrinkVertically() + fadeOut()
            ) {
                TaskItem(
                    task = task,
                    onToggle = { onToggle(task) },
                    onDelete = {
                        isVisible = false
                        // Delay to let animation complete
                    },
                    modifier = Modifier.animateItem(
                        fadeInSpec = tween(300),
                        fadeOutSpec = tween(300),
                        placementSpec = spring(
                            dampingRatio = Spring.DampingRatioMediumBouncy,
                            stiffness = Spring.StiffnessLow
                        )
                    )
                )
            }

            // Trigger deletion after animation
            LaunchedEffect(isVisible) {
                if (!isVisible) {
                    delay(300)
                    onDelete(task)
                }
            }
        }
    }
}
Stabiele keys voor lijstanimaties

Zonder een stabiele key-parameter kan animateItem items niet volgen tussen recomposities. Een unieke ID werkt beter dan de lijstindex.

Canvas-animaties

Voor aangepaste visuele effecten biedt het combineren van Canvas met geanimeerde waarden volledige flexibiliteit.

CanvasAnimationExample.ktkotlin
@Composable
fun AnimatedProgressRing(
    progress: Float,  // 0f to 1f
    modifier: Modifier = Modifier
) {
    // Progress animation with spring for natural feel
    val animatedProgress by animateFloatAsState(
        targetValue = progress,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioLowBouncy,
            stiffness = Spring.StiffnessVeryLow
        ),
        label = "progress"
    )

    // Continuous rotation animation
    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

        // Background circle (track)
        drawCircle(
            color = trackColor,
            radius = radius,
            style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
        )

        // Animated progress arc
        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)
        )
    }
}

Klaar om je Android gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Performance-optimalisatie van Animaties

Slecht geoptimaliseerde animaties kunnen jank veroorzaken en de batterij leegtrekken. Hier volgen de best practices om 60 FPS te halen.

De eerste regel is allocaties tijdens de animatie vermijden. Beter graphicsLayer gebruiken dan modifiers die recomposities triggeren.

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: GPU modifications without recomposition
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                this.alpha = alpha
            }
        // ❌ Avoid: .scale(scale).alpha(alpha)
        // These modifiers trigger recompositions
    ) {
        Text("Card content")
    }
}

// Example with remembered lambda to avoid allocations
@Composable
fun OptimizedClickableItem(
    onClick: () -> Unit,
    content: @Composable () -> Unit
) {
    // ✅ Stable remembered lambda
    val interactionSource = remember { MutableInteractionSource() }

    Box(
        modifier = Modifier
            .clickable(
                interactionSource = interactionSource,
                indication = ripple(),
                onClick = onClick
            )
    ) {
        content()
    }
}

Het tweede kritische punt betreft animaties in lijsten. Het aantal gelijktijdige animaties moet beperkt worden en derivedStateOf is geschikt voor afgeleide berekeningen.

ListPerformance.ktkotlin
@Composable
fun PerformantAnimatedList(
    items: List<Item>,
    modifier: Modifier = Modifier
) {
    // Calculate once whether the list is empty
    val isEmpty by remember {
        derivedStateOf { items.isEmpty() }
    }

    LazyColumn(modifier = modifier) {
        items(
            items = items,
            key = { it.id }
        ) { item ->
            // Lightweight animation only on initial appearance
            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 }
            )
        }
    }
}

Conclusie

Animaties in Jetpack Compose bieden een evenwicht tussen gebruiksgemak en geavanceerde controle. Dit zijn de belangrijkste punten om te onthouden:

  • ✅ Het juiste API-niveau kiezen op basis van complexiteit (AnimatedVisibility → animate*AsState → Animatable)
  • updateTransition gebruiken om meerdere gerelateerde animaties te coördineren
  • spring verkiezen voor natuurlijke animaties en tween voor precieze duur
  • ✅ Altijd een stabiele key-parameter opgeven bij lijstanimaties
  • ✅ Optimaliseren met graphicsLayer om onnodige recomposities te vermijden
  • ✅ Animaties testen op echte apparaten om de prestaties te valideren

Het beheersen van Compose-animaties onderscheidt professionele Android-apps. Deze technieken, gecombineerd met aandacht voor performance, maken vloeiende en boeiende gebruikerservaringen mogelijk.

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

#jetpack compose
#android
#animations
#kotlin
#ui

Delen

Gerelateerde artikelen