AnimatedContent

Composable Function

Common
@Composable
public 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,
)

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 targetStates 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 ContentTransforms 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. 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.

Below is an example of customizing transitionSpec to imply a spatial relationship between the content for different states:

Common
@Composable
public fun <S> Transition<S>.AnimatedContent(
    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,
    contentKey: (targetState: S) -> Any? = { it },
    content: @Composable() AnimatedContentScope.(targetState: S) -> Unit,
)

AnimatedContent is a container that automatically animates its content when Transition.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 Transition.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 Transition.targetState is expected to mutate frequently and not all mutations should be treated as target state change, consider defining a mapping between Transition.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 Transition.targetStates 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 ContentTransforms can be defined for different pairs of initial content and target content.

AnimatedContent displays only the content for Transition.targetState when not animating. However, during the transient content transform, there will be more than one sets of content present in the AnimatedContent container. It may be sometimes desired to define the positional relationship among different content and the style of overlap. This can be achieved by defining contentAlignment and zOrder. 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.

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 ") }
        }
    }
}

TransitionExtensionAnimatedContentSample

@Composable
fun TransitionExtensionAnimatedContentSample() {
    @Composable
    fun CollapsedCart() {
        /* Some content here */
    }
    @Composable
    fun ExpandedCart() {
        /* Some content here */
    }
    // enum class CartState { Expanded, Collapsed }
    var cartState by remember { mutableStateOf(CartState.Collapsed) }
    // Creates a transition here to animate the corner shape and content.
    val cartOpenTransition = updateTransition(cartState, "CartOpenTransition")
    val cornerSize by
        cartOpenTransition.animateDp(
            label = "cartCornerSize",
            transitionSpec = {
                when {
                    CartState.Expanded isTransitioningTo CartState.Collapsed ->
                        tween(durationMillis = 433, delayMillis = 67)
                    else -> tween(durationMillis = 150)
                }
            },
        ) {
            if (it == CartState.Expanded) 0.dp else 24.dp
        }
    Surface(
        Modifier.shadow(8.dp, CutCornerShape(topStart = cornerSize))
            .clip(CutCornerShape(topStart = cornerSize)),
        color = Color(0xfffff0ea),
    ) {
        // Creates an AnimatedContent using the transition. This AnimatedContent will
        // derive its target state from cartOpenTransition.targetState. All the animations
        // created inside of AnimatedContent for size change, enter/exit will be added to the
        // Transition.
        cartOpenTransition.AnimatedContent(
            transitionSpec = {
                fadeIn(animationSpec = tween(150, delayMillis = 150))
                    .togetherWith(fadeOut(animationSpec = tween(150)))
                    .using(
                        SizeTransform { initialSize, targetSize ->
                            // Using different SizeTransform for different state change
                            if (CartState.Collapsed isTransitioningTo CartState.Expanded) {
                                keyframes {
                                    durationMillis = 500
                                    // Animate to full target width and by 200px in height at 150ms
                                    IntSize(targetSize.width, initialSize.height + 200) at 150
                                }
                            } else {
                                keyframes {
                                    durationMillis = 500
                                    // Animate 1/2 the height without changing the width at 150ms.
                                    // The width and rest of the height will be animated in the
                                    // timeframe between 150ms and duration (i.e. 500ms)
                                    IntSize(
                                        initialSize.width,
                                        (initialSize.height + targetSize.height) / 2,
                                    ) at 150
                                }
                            }
                        }
                    )
                    .apply {
                        targetContentZIndex =
                            when (targetState) {
                                // This defines a relationship along z-axis during the momentary
                                // overlap as both incoming and outgoing content is on screen. This
                                // fixed zOrder will ensure that collapsed content will always be on
                                // top of the expanded content - it will come in on top, and
                                // disappear over the expanded content as well.
                                CartState.Expanded -> 1f
                                CartState.Collapsed -> 2f
                            }
                    }
            }
        ) {
            // This defines the mapping from state to composable. It's critical to use the state
            // parameter (i.e. `it`) that is passed into this block of code to ensure correct
            // content lookup.
            when (it) {
                CartState.Expanded -> ExpandedCart()
                CartState.Collapsed -> CollapsedCart()
            }
        }
    }
}