Composable Function

AnimatedContent

[AnimatedContent] is a container that automatically animates its content when [targetState] changes.

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

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

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