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

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.
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).
// 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.
@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.
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.
@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.
// 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.
@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.
@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
)
}
}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.
@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.
@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)
}
}
}
}
}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.
@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.
@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.
@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)
- ✅
updateTransitiongebruiken om meerdere gerelateerde animaties te coördineren - ✅
springverkiezen voor natuurlijke animaties entweenvoor precieze duur - ✅ Altijd een stabiele
key-parameter opgeven bij lijstanimaties - ✅ Optimaliseren met
graphicsLayerom 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
Delen
Gerelateerde artikelen

De 20 meest gestelde Jetpack Compose interviewvragen in 2026
De 20 meest gestelde Jetpack Compose interviewvragen: recomposition, state management, navigatie, performance en architectuurpatronen.

MVVM vs MVI op Android: Welke Architectuur Kiezen in 2026?
Uitgebreide vergelijking tussen MVVM en MVI op Android: voordelen, beperkingen, gebruiksscenario's en een praktische gids voor de juiste architectuurkeuze in 2026.

Kotlin Coroutines voor Android: Complete Gids 2026
Uitgebreide gids over Kotlin coroutines voor Android-ontwikkeling: suspend-functies, scopes, dispatchers, Flow en geavanceerde patronen.