AnimatedContent
Component in Compose Animation
[AnimatedContent] is a container that automatically animates its content when [targetState] changes. Its [content] for different target states is defined in a mapping between a target state and a composable function.
IMPORTANT: The targetState parameter for the [content] lambda should always be taken into account in deciding what composable function to return as the content for that state. This is critical to ensure a successful lookup of all the incoming and outgoing content during content transform.
When [targetState] changes, content for both new and previous targetState will be looked up
through the [content] lambda. They will go through a [ContentTransform] so that the new target
content can be animated in while the initial content animates out. Meanwhile the container will
animate its size as needed to accommodate the new content, unless [SizeTransform] is set to
null
. Once the [ContentTransform] is finished, the outgoing content will be disposed.
If [targetState] is expected to mutate frequently and not all mutations should be treated as target state change, consider defining a mapping between [targetState] and a key in [contentKey]. As a result, transitions will be triggered when the resulting key changes. In other words, there will be no animation when switching between [targetState]s that share the same key. By default, the key will be the same as the targetState object.
By default, the [ContentTransform] will be a delayed [fadeIn] of the target content and a delayed [scaleIn] [togetherWith] a [fadeOut] of the initial content, using a [SizeTransform] to animate any size change of the content. This behavior can be customized using [transitionSpec]. If desired, different [ContentTransform]s can be defined for different pairs of initial content and target content.
[AnimatedContent] displays only the content for [targetState] when not animating. However, during
the transient content transform, there will be more than one set of content present in the
[AnimatedContent] container. It may be sometimes desired to define the positional relationship
among the different content and the overlap. This can be achieved by defining [contentAlignment]
and [zOrder][ContentTransform.targetContentZIndex]. By default, [contentAlignment] aligns all
content to [Alignment.TopStart], and the zIndex
for all the content is 0f. Note: The target
content will always be placed last, therefore it will be on top of all the other content unless
zIndex is specified.
Different content in [AnimatedContent] will have access to their own [AnimatedContentScope]. This allows content to define more local enter/exit transitions via [AnimatedContentScope.animateEnterExit] and [AnimatedContentScope.transition]. These custom enter/exit animations will be triggered as the content enters/leaves the container.
[label] is an optional parameter to differentiate from other animations in Android Studio.
Last updated:
Installation
dependencies {
implementation("androidx.compose.animation:animation:1.8.0-alpha04")
}
Overloads
@Composable
fun <S> AnimatedContent(
targetState: S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
(fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
.togetherWith(fadeOut(animationSpec = tween(90)))
},
contentAlignment: Alignment = Alignment.TopStart,
label: String = "AnimatedContent",
contentKey: (targetState: S) -> Any? = { it },
content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
)
Code Examples
SimpleAnimatedContentSample
@Composable
fun SimpleAnimatedContentSample() {
// enum class ContentState { Foo, Bar, Baz }
@Composable
fun Foo() {
Box(Modifier.size(200.dp).background(Color(0xffffdb00)))
}
@Composable
fun Bar() {
Box(Modifier.size(40.dp).background(Color(0xffff8100)))
}
@Composable
fun Baz() {
Box(Modifier.size(80.dp, 20.dp).background(Color(0xffff4400)))
}
var contentState: ContentState by remember { mutableStateOf(ContentState.Foo) }
AnimatedContent(contentState) {
when (it) {
// Specifies the mapping between a given ContentState and a composable function.
ContentState.Foo -> Foo()
ContentState.Bar -> Bar()
ContentState.Baz -> Baz()
}
}
}
AnimateIncrementDecrementSample
@Composable
fun AnimateIncrementDecrementSample() {
Column(Modifier.padding(20.dp), horizontalAlignment = Alignment.CenterHorizontally) {
var count by remember { mutableStateOf(0) }
// The `AnimatedContent` below uses an integer count as its target state. So when the
// count changes, it will animate out the content associated with the previous count, and
// animate in the content associated with the target state.
AnimatedContent(
targetState = count,
transitionSpec = {
// We can define how the new target content comes in and how initial content
// leaves in the ContentTransform. Here we want to create the impression that the
// different numbers have a spatial relationship - larger numbers are
// positioned (vertically) below smaller numbers.
if (targetState > initialState) {
// If the incoming number is larger, new number slides up and fades in while
// the previous (smaller) number slides up to make room and fades out.
slideInVertically { it } + fadeIn() togetherWith
slideOutVertically { -it } + fadeOut()
} else {
// If the incoming number is smaller, new number slides down and fades in
// while
// the previous number slides down and fades out.
slideInVertically { -it } + fadeIn() togetherWith
slideOutVertically { it } + fadeOut()
// Disable clipping since the faded slide-out is desired out of bounds, but
// the size transform is still needed from number getting longer
}
.using(SizeTransform(clip = false)) // Using default spring for the size change.
}
) { targetCount ->
// This establishes a mapping between the target state and the content in the form of a
// Composable function. IMPORTANT: The parameter of this content lambda should
// *always* be used. During the content transform, the old content will be looked up
// using this lambda with the old state, until it's fully animated out.
// Since AnimatedContent differentiates the contents using their target states as the
// key, the same composable function returned by the content lambda like below will be
// invoked under different keys and therefore treated as different entities.
Text("$targetCount", fontSize = 20.sp)
}
Spacer(Modifier.size(20.dp))
Row(horizontalArrangement = Arrangement.SpaceAround) {
Button(onClick = { count-- }) { Text("Minus") }
Spacer(Modifier.size(60.dp))
Button(onClick = { count++ }) { Text("Plus ") }
}
}
}