animate*AsState
Most animations you'll write in Compose aren't complicated timelines — they're a single value that
needs to glide from its old value to a new one whenever some piece of state changes. A boolean flips,
and a color should ease from grey to red; a selection changes, and a size should grow. The
animate*AsState family of functions — animateFloatAsState,
animateColorAsState, animateDpAsState, and several more for other types —
exists exactly for this: hand it a target value, and it returns a State that
automatically animates toward that target on every recomposition where the target changed.
The Problem: Animating One Value By Hand
Without help, animating a single property means manually driving an Animatable inside a
LaunchedEffect every time the target changes — tracking the old value, launching a
coroutine, cancelling it if the target changes again mid-flight. animate*AsState hides
all of that bookkeeping behind one function call:
Without animate*AsState (manual, more code than you need)
val tint = remember { Animatable(Color.Gray) }
LaunchedEffect(isFavorite) {
tint.animateTo(if (isFavorite) Color.Red else Color.Gray)
}
With animateColorAsState (one line)
val tint by animateColorAsState(
targetValue = if (isFavorite) Color.Red else Color.Gray,
label = "favoriteTint"
)
Both approaches end up calling the same underlying animation machinery, but animateColorAsState
handles interruption (retargeting mid-animation) and cleanup for you. Reach for a raw
Animatable only when you need something animate*AsState doesn't offer, such
as driving the animation from a gesture instead of from a state change — that's covered later in
Gesture-Driven Animations.
animateFloatAsState, animateColorAsState & animateDpAsState
Compose ships a typed animate*AsState function for each common animatable type, so you
never have to cast or convert:
| Function | Animates |
|---|---|
animateFloatAsState | A plain Float — scale factors, alpha, progress fractions |
animateColorAsState | A Color — tints, backgrounds, borders |
animateDpAsState | A Dp — sizes, padding, elevation |
animateIntAsState, animateOffsetAsState, ... | Ints, Offsets, Rects and other common types |
Every one of them follows the same shape: pass a targetValue, get back a
State<T> you read with by. Whenever the composable recomposes with a
different targetValue, the returned state smoothly animates from wherever it currently is
to the new target — even if the previous animation hadn't finished yet.
Controlling the Motion with animationSpec
Every animate*AsState call accepts an optional animationSpec parameter that
controls the timing curve. The two you'll reach for most often are tween (a fixed-duration
animation with an easing curve) and spring (a physics-based animation described by
damping and stiffness, which naturally handles being interrupted mid-flight):
tween: fixed duration and easing
val alpha by animateFloatAsState(
targetValue = if (isVisible) 1f else 0f,
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
label = "alpha"
)
spring: physics-based, naturally interruptible
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "scale"
)
spring is generally the better default for anything that responds to user interaction
(taps, drags, toggles) because a spring that's interrupted mid-motion re-targets smoothly from its
current velocity, whereas a tween that gets retargeted restarts its easing curve from
scratch, which can look like a small stutter.
Worked Example: RecipeBox's Favorite Icon
RecipeBox lets a user tap a heart icon on a recipe card to mark it as a favorite. Two things should
change together when that happens: the icon's tint animates from grey to red, and the icon briefly
scales up to give the tap some weight. Both are single-value animations, so both are perfect
animate*AsState candidates:
RecipeBox: the animated favorite icon
@Composable
fun FavoriteIcon(
isFavorite: Boolean,
onToggle: () -> Unit,
modifier: Modifier = Modifier
) {
val tint by animateColorAsState(
targetValue = if (isFavorite) Color(0xFFE53935) else Color(0xFF9E9E9E),
animationSpec = tween(durationMillis = 250),
label = "favoriteTint"
)
val scale by animateFloatAsState(
targetValue = if (isFavorite) 1.2f else 1f,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "favoriteScale"
)
Icon(
imageVector = Icons.Filled.Favorite,
contentDescription = "Toggle favorite",
tint = tint,
modifier = modifier
.scale(scale)
.clickable { onToggle() }
)
}
Both tint and scale are ordinary Color and Float
state values as far as the rest of FavoriteIcon is concerned — there's no manual
coroutine, no cleanup, and tapping the heart repeatedly before an animation finishes just smoothly
retargets both values instead of glitching.
Common Pitfalls
| Mistake | Why it hurts |
|---|---|
Forgetting the label parameter | Not a compile error, but it makes the animation show up unnamed in the Android Studio animation inspector, which is painful to debug |
Using animate*AsState for a value driven by a live drag gesture | It only animates toward a target on recomposition — a raw Animatable updated in pointerInput is the right tool there |
| Recomputing the target with an unstable expression every recomposition | Causes the "animation" to restart every frame instead of running once toward a stable target |