AnimatedVisibility
animate*AsState animates a value that always exists. But sometimes a whole composable
should exist one moment and be gone the next — an error message that only appears while a form
is invalid, a "loading more" row at the bottom of a list. Simply wrapping that composable in an
if makes it pop in and out instantly. AnimatedVisibility replaces that
if with a container that keeps the composable around just long enough to animate its
entrance and exit.
Basic Usage
AnimatedVisibility takes a boolean visible parameter and a content lambda,
much like an if block would, except the content composable is kept alive during its exit
animation even after visible flips to false:
The simplest form
AnimatedVisibility(visible = showBanner) {
Text("Recipe saved!")
}
With no other arguments, Compose uses sensible default enter/exit transitions (a fade combined with an
expand/shrink). Most of the time you'll want to be explicit about the motion, which is what the
enter and exit parameters are for.
fadeIn/fadeOut, slideInVertically/slideOutVertically & expandVertically/shrinkVertically
Compose ships a small library of EnterTransition and ExitTransition builders
that each describe one visual effect. The three most commonly reached for:
| Function | Effect |
|---|---|
fadeIn() / fadeOut() | Animates alpha from/to 0 |
slideInVertically() / slideOutVertically() | Animates a vertical offset, controlled by an initialOffsetY/targetOffsetY lambda |
expandVertically() / shrinkVertically() | Animates the composable's own height from/to 0, pushing surrounding content out of the way as it grows |
Each of these accepts its own animationSpec, exactly like animate*AsState
did in the previous lesson:
One transition at a time
AnimatedVisibility(
visible = showError,
enter = slideInVertically(animationSpec = tween(200)),
exit = slideOutVertically(animationSpec = tween(150))
) {
Text("Something went wrong")
}
Combining Transitions with the + Operator
Real UI motion is rarely just one effect. EnterTransition and ExitTransition
values can be combined with +, running every combined effect together:
Fade, slide and expand together
val enter = fadeIn(animationSpec = tween(200)) +
slideInVertically(animationSpec = tween(200)) +
expandVertically(animationSpec = tween(200))
Order doesn't matter for how the effects run — Compose runs every combined transition on the
same clock — but keeping the same animationSpec duration across all of them avoids
one effect visibly finishing before another.
Worked Example: RecipeBox's Title Validation Error
When saving a new recipe, RecipeBox validates that the title field isn't blank. Rather than the error text just appearing (or worse, always reserving space below the field even when there's nothing wrong), it slides and fades in when validation fails, and slides back out once the user fixes it:
RecipeBox: the title field with an animated error
@Composable
fun RecipeTitleField(
title: String,
onTitleChange: (String) -> Unit,
showError: Boolean
) {
Column {
OutlinedTextField(
value = title,
onValueChange = onTitleChange,
label = { Text("Recipe title") },
isError = showError,
modifier = Modifier.fillMaxWidth()
)
AnimatedVisibility(
visible = showError,
enter = fadeIn(animationSpec = tween(200)) +
slideInVertically(
animationSpec = tween(200),
initialOffsetY = { fullHeight -> -fullHeight / 2 }
) +
expandVertically(animationSpec = tween(200)),
exit = fadeOut(animationSpec = tween(150)) +
slideOutVertically(
animationSpec = tween(150),
targetOffsetY = { fullHeight -> -fullHeight / 2 }
) +
shrinkVertically(animationSpec = tween(150))
) {
Text(
text = "Title can't be empty",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 4.dp)
)
}
}
}
The negative initialOffsetY/targetOffsetY makes the message slide in and out
from slightly above its resting position rather than from off-screen, which reads as "unfolding from
the field above it" instead of an unrelated element sliding up from below.
AnimatedVisibility vs. a Plain if
if (visible) { Content() } removes the composable from
composition the instant visible becomes false — there's no exit animation
to play because there's nothing left to animate. AnimatedVisibility keeps Content()
composed for the duration of the exit transition and only removes it once that transition finishes.