Jetpack Compose: Fortgeschrittene Animationen Schritt für Schritt
Vollständiger Leitfaden zu fortgeschrittenen Compose-Animationen: Übergänge, AnimatedVisibility, Animatable, Gesten und Performance für flüssige Android-Oberflächen.

Animationen verwandeln eine funktionale App in ein einprägsames Nutzererlebnis. Jetpack Compose bietet eine leistungsstarke deklarative Animations-API, die das Erstellen flüssiger Oberflächen erheblich vereinfacht. Dieser Leitfaden behandelt fortgeschrittene Techniken zum Aufbau performanter und wartbarer Animationen.
Dieses Tutorial setzt Vertrautheit mit den Compose-Grundlagen voraus (Recomposition, State, Modifiers). Für die Grundlagen empfiehlt es sich, zuerst den Leitfaden zu Jetpack-Compose-Interviewfragen zu konsultieren.
Grundlagen der Animations-API in Compose
Compose stellt mehrere API-Ebenen für Animationen bereit. Die Wahl hängt vom gewünschten Maß an Kontrolle und der Komplexität der Animation ab.
Die API gliedert sich in drei Hauptkategorien: High-Level-Animationen (AnimatedVisibility, AnimatedContent), zustandsbasierte Animationen (animate*AsState) und Low-Level-Animationen (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)
}
}Die Wahl der passenden API-Ebene ist entscheidend, um lesbaren Code zu erhalten und gleichzeitig die nötige Flexibilität zu bewahren.
AnimatedVisibility: Elegante Eintritts- und Austrittsanimationen
AnimatedVisibility ist der ideale Einstiegspunkt, um das Erscheinen und Verschwinden von Elementen zu animieren. Diese API verwaltet automatisch das Komponieren und Dekomponieren des Inhalts.
Die Parameter enter und exit akzeptieren Kombinationen von Übergängen, die das Animationsverhalten festlegen. Diese Übergänge lassen sich mit dem Operator + kombinieren.
@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
)
}
}
}
}Der Inhalt innerhalb von AnimatedVisibility wird nur komponiert, wenn visible = true gilt, was die Performance bei Listen mit vielen ausklappbaren Elementen optimiert.
Verfügbare Übergänge sind unter anderem fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut, scaleIn/scaleOut. Sie lassen sich frei kombinieren, um eigene Effekte zu erstellen.
animate*AsState: Zustandsbasierte Animationen
Die Funktionsfamilie animate*AsState animiert Änderungen primitiver Werte automatisch. Das ist der idiomatischste Ansatz für einfache Animationen in Compose.
Jeder Datentyp besitzt eine eigene Funktion: animateColorAsState, animateFloatAsState, animateDpAsState, animateIntAsState usw.
@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
)
}
}Der Parameter animationSpec steuert das zeitliche Verhalten der Animation. Die beiden gebräuchlichsten Specs sind tween (feste Dauer mit Easing) und spring (realistische Physik mit Federung).
Transition: Mehrere Animationen orchestrieren
Wenn mehrere Eigenschaften koordiniert animiert werden müssen, bietet updateTransition eine zentrale Steuerung. Diese API stellt sicher, dass alle Animationen synchron bleiben.
Das Muster besteht darin, einen Enum-Zustand zu definieren und dann Animationen für jede Eigenschaft zu erstellen, die von diesem Zustand abhängt.
// 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
}
}Bereit für deine Android-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Animatable: Volle Kontrolle über die Animation
Animatable ist die Low-Level-API, die vollständige programmatische Kontrolle bietet. Dieser Ansatz wird für unterbrechbare Animationen, Gesten oder komplexe Szenarien benötigt.
Im Gegensatz zu animate*AsState erlaubt Animatable das Anhalten, Umkehren oder Modifizieren einer laufenden Animation, ohne auf deren Ende zu warten.
@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()
}
}Die zentralen Methoden von Animatable sind animateTo() (zum Ziel animieren), snapTo() (sofortige Änderung) und stop() (Unterbrechung).
AnimatedContent: Inhaltsübergänge
AnimatedContent animiert Übergänge zwischen unterschiedlichen Inhalten. Diese API eignet sich perfekt für Zustandsänderungen, die die angezeigte Oberfläche vollständig verändern.
Der Schlüssel targetState bestimmt, wann ein Übergang stattfinden soll. transitionSpec definiert, wie ausgehende und eingehende Inhalte interagieren.
@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
)
}
}Der Inhalt innerhalb von AnimatedContent wird bei jeder Änderung von targetState neu komponiert. Bei komplexen Inhalten lohnt es sich, aufwendige Elemente zu memoisieren oder eine geeignete Key-Strategie einzusetzen.
Endlosanimationen mit rememberInfiniteTransition
Für Animationen in der Endlosschleife (Ladeindikatoren, Pulsiereffekte) bietet rememberInfiniteTransition eine eigene API, die keine manuelle Zyklusverwaltung erfordert.
@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)
)
}Listenanimationen mit LazyColumn
Animationen für Listenelemente erfordern besondere Aufmerksamkeit. Der Modifier animateItem() (früher animateItemPlacement) animiert Umsortierungen 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)
}
}
}
}
}Ohne stabilen key-Parameter kann animateItem Elemente zwischen Recompositions nicht verfolgen. Eine eindeutige ID statt des Listenindex ist die richtige Wahl.
Canvas-Animationen
Für eigene visuelle Effekte bietet die Kombination aus Canvas und animierten Werten maximale Flexibilität.
@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)
)
}
}Bereit für deine Android-Interviews?
Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.
Performance-Optimierung von Animationen
Schlecht optimierte Animationen können zu Jank führen und die Akkulaufzeit verkürzen. Hier sind die bewährten Praktiken, um 60 FPS zu halten.
Die erste Regel lautet, Allokationen während der Animation zu vermeiden. Statt Modifiers, die Recompositions auslösen, sollte graphicsLayer verwendet werden.
@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()
}
}Der zweite kritische Punkt betrifft Animationen in Listen. Die Anzahl gleichzeitiger Animationen sollte begrenzt und derivedStateOf für abgeleitete Berechnungen genutzt werden.
@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 }
)
}
}
}Fazit
Animationen in Jetpack Compose bieten ein Gleichgewicht zwischen Bedienkomfort und fortgeschrittener Kontrolle. Hier die wichtigsten Erkenntnisse:
- ✅ Die richtige API-Ebene je nach Komplexität wählen (AnimatedVisibility → animate*AsState → Animatable)
- ✅
updateTransitionverwenden, um mehrere zusammenhängende Animationen zu koordinieren - ✅
springfür natürliche Animationen undtweenfür präzise Dauer bevorzugen - ✅ Bei Listenanimationen stets einen stabilen
key-Parameter angeben - ✅ Mit
graphicsLayeroptimieren, um unnötige Recompositions zu vermeiden - ✅ Animationen auf realen Geräten testen, um die Performance zu überprüfen
Die Beherrschung von Compose-Animationen unterscheidet professionelle Android-Apps. Diese Techniken, kombiniert mit Aufmerksamkeit für Performance, ermöglichen flüssige und fesselnde Nutzererlebnisse.
Fang an zu üben!
Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.
Tags
Teilen
Verwandte Artikel

Die 20 wichtigsten Jetpack Compose Interviewfragen 2026
Die 20 am häufigsten gestellten Jetpack Compose Interviewfragen: Recomposition, State-Management, Navigation, Performance und Architekturmuster.

MVVM vs MVI in Android: Welche Architektur in 2026 wählen?
Ausführlicher Vergleich von MVVM und MVI in Android: Vorteile, Einschränkungen, Anwendungsfälle und ein praxisnaher Leitfaden zur Architekturwahl in 2026.

Kotlin Coroutines meistern: Vollständiger Guide 2026
Kotlin Coroutines für die Android-Entwicklung meistern: Suspend-Funktionen, Scopes, Dispatcher und fortgeschrittene Patterns.