Build apps faster with our new App builder! Check it out →

AnimatedContent

Common

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 ") }
        }
    }
}
by @alexstyl