Mastering Animations in Compose: From Simple to Complex
AnimationMotionUI PolishUser Experience
Animation Philosophy in Compose
Compose animations are state-driven. You change state, and the framework animates between old and new values. This declarative approach eliminates the imperative animation code that was error-prone in Views.
The key insight: you do not tell Compose HOW to animate, you tell it WHAT values to animate between, and it figures out the rest.
animate*AsState: The Simplest Animation
For animating single values, animate*AsState is your go-to. It creates a smooth transition whenever the target value changes:
@Composable
fun ExpandableCard(isExpanded: Boolean, content: @Composable () -> Unit) {
// Animates smoothly between 100dp and 300dp
val height by animateDpAsState(
targetValue = if (isExpanded) 300.dp else 100.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "card height"
)
Card(modifier = Modifier.height(height)) {
content()
}
}AnimatedVisibility: Enter and Exit
AnimatedVisibility handles appearing and disappearing content with customizable transitions:
@Composable
fun NotificationBanner(message: String?, onDismiss: () -> Unit) {
AnimatedVisibility(
visible = message != null,
enter = slideInVertically(initialOffsetY = { -it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { -it }) + fadeOut()
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(message ?: "")
IconButton(onClick = onDismiss) {
Icon(Icons.Default.Close, contentDescription = "Dismiss")
}
}
}
}
}Transition: Coordinating Multiple Animations
When multiple values need to animate together, use updateTransition. It ensures all animations start and end at the same time:
enum class CardState { Collapsed, Expanded }
@Composable
fun CoordinatedCard(state: CardState) {
val transition = updateTransition(state, label = "card transition")
val height by transition.animateDp(label = "height") { cardState ->
when (cardState) {
CardState.Collapsed -> 100.dp
CardState.Expanded -> 300.dp
}
}
val cornerRadius by transition.animateDp(label = "corner") { cardState ->
when (cardState) {
CardState.Collapsed -> 16.dp
CardState.Expanded -> 8.dp
}
}
val backgroundColor by transition.animateColor(label = "color") { cardState ->
when (cardState) {
CardState.Collapsed -> Color.LightGray
CardState.Expanded -> Color.White
}
}
Card(
modifier = Modifier.height(height),
shape = RoundedCornerShape(cornerRadius),
colors = CardDefaults.cardColors(containerColor = backgroundColor)
) {
// Content
}
}Performance Tips
Animations can be performance-intensive. Follow these guidelines:
- Prefer graphicsLayer modifiers for translation, rotation, scale, and alpha — they avoid recomposition
- Use derivedStateOf to debounce rapid state changes
- Provide meaningful labels to all animations for debugging in Layout Inspector
- Test on low-end devices to catch performance issues early
Note: Always respect user preferences. Check LocalAccessibilityManager for users who have enabled "reduce motion" in system settings.