Compose Unstyled 2.0 is out! Check the official announcement blog ->
Composable Function

DeferredAnimatedContent

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

DeferredAnimatedContentSample

@OptIn(ExperimentalDeferredTransitionApi::class)
@Composable
fun DeferredAnimatedContentSample() {
    // In a real app, these states would be driven by a gesture handler like PredictiveBackHandler
    val targetScreen by remember { mutableIntStateOf(0) }
    val isBackGestureInProgress by remember { mutableStateOf(false) }
    val swipeOffset by remember { mutableStateOf(IntOffset.Zero) }
    val transitionState = remember { DeferredTransitionState(targetScreen) }
    val transition = rememberTransition(transitionState)
    LaunchedEffect(isBackGestureInProgress, targetScreen) {
        if (isBackGestureInProgress) {
            transitionState.defer(targetScreen)
        } else {
            transitionState.animateTo(targetScreen)
        }
    }
    transition.DeferredAnimatedContent(
        transitionSpec = { slideInHorizontally { it } togetherWith slideOutHorizontally { -it } },
        mutableTransformSpec = {
            MutableContentTransform {
                if (isBackGestureInProgress && targetScreen < transitionState.targetState) {
                    // Shift the entering and exiting screens based on swipe offset
                    targetContentTransform { offset = swipeOffset }
                    initialContentTransform {
                        offset = swipeOffset.copy(swipeOffset.x / 2, swipeOffset.y / 2)
                    }
                }
            }
        },
    ) { screen ->
        Box(Modifier.size(200.dp).background(if (screen % 2 == 0) Color.Blue else Color.Green)) {
            Text("Screen $screen")
        }
    }
}

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

Last updated: