Jetpack Compose: Просунуті анімації крок за кроком
Повний гід з просунутих анімацій Compose: переходи, AnimatedVisibility, Animatable, жести та продуктивність для плавних інтерфейсів Android.

Анімації перетворюють функціональний застосунок на запам'ятовуваний користувацький досвід. Jetpack Compose пропонує потужний декларативний API анімацій, який значно спрощує створення плавних інтерфейсів. Цей гід розглядає просунуті техніки створення продуктивних і підтримуваних анімацій.
Цей урок передбачає знайомство з основами Compose (рекомпозиція, стан, модифікатори). Для опанування основ варто спершу ознайомитися з гідом запитань на співбесіді з Jetpack Compose.
Основи API анімацій у Compose
Compose надає кілька рівнів API для анімацій. Вибір залежить від бажаного рівня контролю та складності анімації.
API поділяється на три основні категорії: високорівневі анімації (AnimatedVisibility, AnimatedContent), анімації на основі стану (animate*AsState) і низькорівневі анімації (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)
}
}Вибір правильного рівня API критично важливий для збереження читабельного коду без втрати потрібної гнучкості.
AnimatedVisibility: Витончені анімації появи та зникнення
AnimatedVisibility — ідеальна точка входу для анімації появи та зникнення елементів. Цей API автоматично керує композицією та декомпозицією контенту.
Параметри enter і exit приймають комбінації переходів, що визначають поведінку анімації. Ці переходи можна поєднувати оператором +.
@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
)
}
}
}
}Контент усередині AnimatedVisibility компонується лише коли visible = true, що оптимізує продуктивність списків з багатьма розгортуваними елементами.
Доступні переходи включають fadeIn/fadeOut, slideIn/slideOut, expandIn/shrinkOut, scaleIn/scaleOut. Їх можна вільно комбінувати для створення власних ефектів.
animate*AsState: Анімації, керовані станом
Родина функцій animate*AsState автоматично анімує зміни примітивних значень. Це найідіоматичніший підхід для простих анімацій у Compose.
Кожен тип даних має свою функцію: animateColorAsState, animateFloatAsState, animateDpAsState, animateIntAsState тощо.
@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
)
}
}Параметр animationSpec керує часовою поведінкою анімації. Дві найвживаніші специфікації: tween (фіксована тривалість з easing) і spring (реалістична фізика з підстрибуванням).
Transition: Оркестрування кількох анімацій
Коли кілька властивостей мають анімуватися узгоджено, updateTransition забезпечує централізований контроль. Цей API гарантує, що всі анімації залишаться синхронізованими.
Патерн полягає у визначенні enum-стану та подальшому створенні анімацій для кожної властивості, яка залежить від цього стану.
// 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
}
}Готовий до співбесід з Android?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Animatable: Повний контроль над анімацією
Animatable — це низькорівневий API, що забезпечує повний програмний контроль. Цей підхід необхідний для перериваних анімацій, жестів або складних сценаріїв.
На відміну від animate*AsState, Animatable дає змогу зупинити, обернути або змінити поточну анімацію без очікування її завершення.
@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()
}
}Ключові методи Animatable: animateTo() (анімувати до цілі), snapTo() (миттєва зміна) і stop() (переривання).
AnimatedContent: Переходи контенту
AnimatedContent анімує переходи між різним контентом. Цей API ідеально підходить для змін стану, які повністю змінюють відображуваний UI.
Ключ targetState визначає, коли має відбутися перехід. transitionSpec визначає, як взаємодіють вихідний і вхідний контент.
@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
)
}
}Контент усередині AnimatedContent рекомпонується при кожній зміні targetState. Для складного контенту варто мемоїзувати дорогі елементи або використовувати відповідну стратегію ключів.
Нескінченні анімації з rememberInfiniteTransition
Для циклічних анімацій (індикатори завантаження, пульсуючі ефекти) rememberInfiniteTransition надає окремий API, який не вимагає ручного керування циклом.
@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)
)
}Анімації списків із LazyColumn
Анімації елементів списку потребують особливої уваги. Модифікатор animateItem() (раніше animateItemPlacement) автоматично анімує перевпорядкування.
@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)
}
}
}
}
}Без стабільного параметра key animateItem не може відстежувати елементи між рекомпозиціями. Краще використати унікальний ID, а не індекс списку.
Анімації Canvas
Для власних візуальних ефектів поєднання Canvas з анімованими значеннями забезпечує повну гнучкість.
@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)
)
}
}Готовий до співбесід з Android?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Оптимізація продуктивності анімацій
Погано оптимізовані анімації можуть викликати jank і виснажувати батарею. Ось найкращі практики для підтримання 60 FPS.
Перше правило — уникати алокацій під час анімації. Замість модифікаторів, які запускають рекомпозиції, варто використовувати graphicsLayer.
@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()
}
}Друга важлива точка стосується анімацій у списках. Слід обмежувати кількість одночасних анімацій і використовувати derivedStateOf для похідних обчислень.
@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 }
)
}
}
}Висновок
Анімації в Jetpack Compose забезпечують баланс між простотою використання та просунутим контролем. Ключові висновки:
- ✅ Обирати рівень API відповідно до складності (AnimatedVisibility → animate*AsState → Animatable)
- ✅ Використовувати
updateTransitionдля координації кількох пов'язаних анімацій - ✅ Віддавати перевагу
springдля природних анімацій іtweenдля точних тривалостей - ✅ Завжди надавати стабільний параметр
keyдля анімацій списків - ✅ Оптимізувати з
graphicsLayer, щоб уникнути зайвих рекомпозицій - ✅ Тестувати анімації на реальних пристроях для підтвердження продуктивності
Володіння анімаціями Compose вирізняє професійні Android-застосунки. Ці техніки в поєднанні з увагою до продуктивності дають змогу створювати плавний і захопливий користувацький досвід.
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

20 найпоширеніших питань на співбесіді з Jetpack Compose у 2026 році
20 найчастіших питань на співбесіді з Jetpack Compose: рекомпозиція, управління станом, навігація, продуктивність та архітектурні патерни.

MVVM vs MVI на Android: Яку Архітектуру Обрати у 2026?
Детальне порівняння MVVM і MVI на Android: переваги, обмеження, випадки використання та практичний посібник з вибору правильної архітектури у 2026 році.

Kotlin Coroutines: Повний посібник 2026 для Android
Опануйте Kotlin coroutines для Android-розробки: suspend-функції, скоупи, диспатчери та просунуті патерни.