Jetpack Compose: Zaawansowane Animacje Krok po Kroku
Kompletny przewodnik po zaawansowanych animacjach Compose: przejścia, AnimatedVisibility, Animatable, gesty i wydajność płynnych interfejsów Android.

Animacje przekształcają funkcjonalną aplikację w niezapomniane doświadczenie użytkownika. Jetpack Compose oferuje potężne deklaratywne API animacji, które znacząco upraszcza tworzenie płynnych interfejsów. Ten przewodnik omawia zaawansowane techniki budowania wydajnych i łatwych w utrzymaniu animacji.
Ten tutorial zakłada znajomość podstaw Compose (rekompozycja, stan, modyfikatory). Aby przypomnieć sobie podstawy, warto najpierw zajrzeć do przewodnika z pytaniami rekrutacyjnymi o Jetpack Compose.
Podstawy API Animacji w Compose
Compose udostępnia kilka poziomów API dla animacji. Wybór zależy od pożądanego poziomu kontroli i złożoności animacji.
API dzieli się na trzy główne kategorie: animacje wysokiego poziomu (AnimatedVisibility, AnimatedContent), animacje oparte na stanie (animate*AsState) oraz animacje niskiego poziomu (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)
}
}Wybór odpowiedniego poziomu API jest kluczowy, aby zachować czytelny kod przy zachowaniu niezbędnej elastyczności.
AnimatedVisibility: Eleganckie Animacje Pojawiania i Znikania
AnimatedVisibility to idealny punkt wejścia do animowania pojawiania się i znikania elementów. To API automatycznie zarządza komponowaniem i dekomponowaniem treści.
Parametry enter i exit przyjmują kombinacje przejść definiujących zachowanie animacji. Przejścia te można łączyć operatorem +.
@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
)
}
}
}
}Treść wewnątrz AnimatedVisibility jest komponowana tylko gdy visible = true, co optymalizuje wydajność list z wieloma rozwijanymi elementami.
Dostępne przejścia obejmują fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut, scaleIn/scaleOut. Można je dowolnie łączyć, tworząc niestandardowe efekty.
animate*AsState: Animacje Sterowane Stanem
Rodzina funkcji animate*AsState automatycznie animuje zmiany wartości prymitywnych. To najbardziej idiomatyczne podejście do prostych animacji w Compose.
Każdy typ danych ma dedykowaną funkcję: animateColorAsState, animateFloatAsState, animateDpAsState, animateIntAsState itd.
@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
)
}
}Parametr animationSpec kontroluje czasowe zachowanie animacji. Dwie najczęściej używane specyfikacje to tween (ustalony czas trwania z easingiem) i spring (realistyczna fizyka z odbiciem).
Transition: Orkiestracja Wielu Animacji
Gdy kilka właściwości musi być animowanych w skoordynowany sposób, updateTransition zapewnia scentralizowaną kontrolę. To API gwarantuje, że wszystkie animacje pozostaną zsynchronizowane.
Wzorzec polega na zdefiniowaniu stanu enum, a następnie utworzeniu animacji dla każdej właściwości zależnej od tego stanu.
// 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
}
}Gotowy na rozmowy o Android?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Animatable: Pełna Kontrola nad Animacją
Animatable to API niskiego poziomu zapewniające pełną kontrolę programistyczną. Takie podejście jest niezbędne dla animacji przerywalnych, gestów lub złożonych scenariuszy.
W przeciwieństwie do animate*AsState, Animatable umożliwia zatrzymanie, odwrócenie lub zmodyfikowanie trwającej animacji bez czekania na jej zakończenie.
@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()
}
}Kluczowe metody Animatable to animateTo() (animacja do celu), snapTo() (natychmiastowa zmiana) i stop() (przerwanie).
AnimatedContent: Przejścia Treści
AnimatedContent animuje przejścia między różnymi treściami. To API jest idealne dla zmian stanu, które całkowicie modyfikują wyświetlany interfejs.
Klucz targetState określa, kiedy ma nastąpić przejście. transitionSpec definiuje sposób interakcji wychodzącej i przychodzącej treści.
@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
)
}
}Treść wewnątrz AnimatedContent jest rekomponowana przy każdej zmianie targetState. Dla złożonej treści warto memoizować kosztowne elementy lub używać odpowiedniej strategii kluczy.
Nieskończone Animacje z rememberInfiniteTransition
Dla animacji w pętli (wskaźniki ładowania, efekty pulsujące) rememberInfiniteTransition udostępnia dedykowane API, które nie wymaga ręcznego zarządzania cyklem.
@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)
)
}Animacje List z LazyColumn
Animacje elementów list wymagają szczególnej uwagi. Modyfikator animateItem() (wcześniej animateItemPlacement) automatycznie animuje zmiany kolejności.
@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)
}
}
}
}
}Bez stabilnego parametru key, animateItem nie potrafi śledzić elementów między rekompozycjami. Lepiej użyć unikalnego ID niż indeksu listy.
Animacje Canvas
Dla niestandardowych efektów wizualnych połączenie Canvas z animowanymi wartościami zapewnia pełną elastyczność.
@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)
)
}
}Gotowy na rozmowy o Android?
Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.
Optymalizacja Wydajności Animacji
Źle zoptymalizowane animacje mogą powodować jank i wyczerpywać baterię. Oto dobre praktyki, aby utrzymać 60 FPS.
Pierwsza zasada to unikanie alokacji podczas animacji. Lepiej użyć graphicsLayer zamiast modyfikatorów wywołujących rekompozycje.
@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()
}
}Drugi krytyczny punkt dotyczy animacji w listach. Należy ograniczyć liczbę jednoczesnych animacji i używać derivedStateOf dla obliczeń pochodnych.
@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 }
)
}
}
}Podsumowanie
Animacje w Jetpack Compose oferują równowagę między łatwością użycia a zaawansowaną kontrolą. Oto kluczowe punkty do zapamiętania:
- ✅ Wybierać odpowiedni poziom API w zależności od złożoności (AnimatedVisibility → animate*AsState → Animatable)
- ✅ Używać
updateTransitiondo koordynacji wielu powiązanych animacji - ✅ Preferować
springdla naturalnych animacji itweendla precyzyjnych czasów - ✅ Zawsze podawać stabilny parametr
keydla animacji list - ✅ Optymalizować z
graphicsLayer, aby unikać niepotrzebnych rekompozycji - ✅ Testować animacje na rzeczywistych urządzeniach, aby zweryfikować wydajność
Opanowanie animacji Compose wyróżnia profesjonalne aplikacje Android. Te techniki, połączone z dbałością o wydajność, pozwalają tworzyć płynne i angażujące doświadczenia użytkownika.
Zacznij ćwiczyć!
Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.
Tagi
Udostępnij
Powiązane artykuły

20 najczesciej zadawanych pytan rekrutacyjnych z Jetpack Compose w 2026
20 najczesciej zadawanych pytan na rozmowie kwalifikacyjnej z Jetpack Compose: rekompozycja, zarzadzanie stanem, nawigacja, wydajnosc i wzorce architektoniczne.

MVVM vs MVI na Androidzie: Którą Architekturę Wybrać w 2026?
Szczegółowe porównanie MVVM i MVI na Androidzie: zalety, ograniczenia, przypadki użycia i praktyczny przewodnik po wyborze właściwej architektury w 2026.

Kotlin Coroutines na Androida: Kompletny Przewodnik 2026
Kompleksowy przewodnik po korutynach Kotlin w programowaniu Android: funkcje suspend, zakresy, dispatchery, Flow i zaawansowane wzorce.