DeferredAnimatedContent

Functions summary

Unit
@ExperimentalDeferredTransitionApi
@Composable
<S : Any?> DeferredTransition<S>.DeferredAnimatedContent(
    modifier: Modifier,
    transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform,
    contentAlignment: Alignment,
    contentKey: (targetState) -> Any?,
    mutableTransformSpec: AnimatedContentTransitionScope<S>.() -> MutableContentTransform?,
    content: @Composable AnimatedContentScope.(targetState) -> Unit
)

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

Cmn

Functions

DeferredTransition.DeferredAnimatedContent

@ExperimentalDeferredTransitionApi
@Composable
fun <S : Any?> DeferredTransition<S>.DeferredAnimatedContent(
    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) -> Any? = { it },
    mutableTransformSpec: AnimatedContentTransitionScope<S>.() -> MutableContentTransform? = { null },
    content: @Composable AnimatedContentScope.(targetState) -> Unit
): 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.

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.DeferredAnimatedContent
import androidx.compose.animation.MutableContentTransform
import androidx.compose.animation.core.DeferredTransitionState
import androidx.compose.animation.core.rememberTransition
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp

// 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")
    }
}
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp

@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()
        }
    }
}
Parameters
modifier: Modifier = Modifier

is applied to the Layout created by AnimatedContent, which houses all the animating contents.

transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = { (fadeIn(animationSpec = tween(220, delayMillis = 90)) + scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90))) .togetherWith(fadeOut(animationSpec = tween(90))) }

specifies the enter/exit animations, as well as size animation (if applicable). By default, a fadeIn and scaleIn will be used for the entering content, and fadeOut for the exiting content. The ContentTransform returned by transitionSpec can be created using togetherWith with EnterTransition and ExitTransition.

contentAlignment: Alignment = Alignment.TopStart

specifies the alignment of the animated content. By default, it'll be aligned to the top start of the container.

contentKey: (targetState) -> Any? = { it }

A key to identify the content.

mutableTransformSpec: AnimatedContentTransitionScope<S>.() -> MutableContentTransform? = { null }

A specification to control an optional manual transformation during the deferred phase (e.g., for predictive back gestures) before the main transition begins. This is only active if the Transition was created using rememberTransition with DeferredTransitionState. By default, this returns null, meaning no manual transformations are applied.

Lifecycle: The deferred phase starts when DeferredTransitionState.defer is called. It ends and the automatic transition begins when DeferredTransitionState.animateTo is called.

Transformations: During this phase, you can manually manipulate the entering and exiting content's transformations (via MutableContentTransform). These transformations are applied on top of the transition's initial state. For example, if the enter transition starts at an alpha of 0.5, applying a manual alpha of 0.5 will result in a combined alpha of 0.25.

Handoff: Once the transition starts, the manually applied transformations are seamlessly handed off to the configured transitionSpec. For exiting content, a "sustain unless specified" policy is applied: if an exit transition (e.g. fadeOut) is specified, the hand-off will animate towards the target value of that transition. However, if no exit transition is specified for a given property (e.g. slideOut is missing), that property will sustain its last manual value until the entire transition completes.

Note: While in the deferred phase, entering content remains in the EnterExitState.PreEnter state, and exiting content remains in the EnterExitState.Visible state.

content: @Composable AnimatedContentScope.(targetState) -> Unit

The composable function to render the content for a given state.