# Jetpack Compose: Geavanceerde Animaties Stap voor Stap > Volledige gids voor geavanceerde Compose-animaties: transities, AnimatedVisibility, Animatable, gestures en performance voor vloeiende Android-interfaces. - Published: 2026-02-08 - Updated: 2026-04-28 - Author: SharpSkill - Tags: jetpack compose, android, animations, kotlin, ui - Reading time: 12 min --- 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`). ```kotlin // AnimationLevels.kt // 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. ```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 ) } // 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. ```kotlin // AnimateAsStateExample.kt @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. ```kotlin // TransitionExample.kt // 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 } } ``` ## 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. ```kotlin // AnimatableExample.kt @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. ```kotlin // AnimatedContentExample.kt @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. ```kotlin // InfiniteTransitionExample.kt @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. ```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 } // 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. ```kotlin // CanvasAnimationExample.kt @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) ) } } ``` ## 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. ```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: 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. ```kotlin // ListPerformance.kt @Composable fun PerformantAnimatedList( items: List, 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/android/jetpack-compose-advanced-animations