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.

Fortgeschrittene Jetpack-Compose-Animationen für Android-Entwickler

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.

Voraussetzungen

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

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

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.

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

Der Inhalt innerhalb von AnimatedVisibility wird nur komponiert, wenn visible = true gilt, was die Performance bei Listen mit vielen ausklappbaren Elementen optimiert.

Übergänge kombinieren

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.

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

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.

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

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.

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

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.

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 bei AnimatedContent

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.

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

Listenanimationen mit LazyColumn

Animationen für Listenelemente erfordern besondere Aufmerksamkeit. Der Modifier animateItem() (früher animateItemPlacement) animiert Umsortierungen 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)
                }
            }
        }
    }
}
Stabile Keys für Listenanimationen

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.

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

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.

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

Der zweite kritische Punkt betrifft Animationen in Listen. Die Anzahl gleichzeitiger Animationen sollte begrenzt und derivedStateOf für abgeleitete Berechnungen genutzt werden.

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

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)
  • updateTransition verwenden, um mehrere zusammenhängende Animationen zu koordinieren
  • spring für natürliche Animationen und tween für präzise Dauer bevorzugen
  • ✅ Bei Listenanimationen stets einen stabilen key-Parameter angeben
  • ✅ Mit graphicsLayer optimieren, 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

#jetpack compose
#android
#animations
#kotlin
#ui

Teilen

Verwandte Artikel