A Transition that supports a deferred phase, created via rememberTransition.

DeferredTransition extends the standard Transition to allow manual manipulation of transformation properties before the automatic transition begins. This is particularly useful for coordinating multi-stage animations like predictive back gestures.

Summary

Extension functions

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
Unit
@ExperimentalDeferredTransitionApi
@Composable
<T : Any?> DeferredTransition<T>.DeferredAnimatedVisibility(
    visible: (T) -> Boolean,
    modifier: Modifier,
    enter: EnterTransition,
    exit: ExitTransition,
    mutableTransform: MutableTransform?,
    content: @Composable AnimatedVisibilityScope.() -> Unit
)

AnimatedVisibility can be used to animate the appearance and disappearance of its content as the Transition state changes.

Cmn

Inherited functions

From androidx.compose.animation.core.Transition
open String
Cmn

Inherited properties

From androidx.compose.animation.core.Transition
List<Transition.TransitionAnimationState<*, *>>

List of TransitionAnimationStates that are in a Transition.

Cmn
S

Current state of the transition.

Cmn
Boolean

Used internally to know when a SeekableTransitionState is animating initial values after SeekableTransitionState.animateTo or SeekableTransitionState.seekTo has redirected a transition prior to it completing.

Cmn
Boolean

Indicates whether there is any animation running in the transition.

Cmn
String?
Cmn
Transition.Segment<S>

segment contains the initial state and the target state of the currently on-going transition.

Cmn
S

Target state of the transition.

Cmn
Long

Total duration of the Transition, accounting for all the animations and child transitions defined on the Transition.

Cmn
List<Transition<*>>

List of child transitions in a Transition.

Cmn

Extension 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.

DeferredTransition.DeferredAnimatedVisibility

@ExperimentalDeferredTransitionApi
@Composable
fun <T : Any?> DeferredTransition<T>.DeferredAnimatedVisibility(
    visible: (T) -> Boolean,
    modifier: Modifier = Modifier,
    enter: EnterTransition = fadeIn() + expandIn(),
    exit: ExitTransition = shrinkOut() + fadeOut(),
    mutableTransform: MutableTransform? = null,
    content: @Composable AnimatedVisibilityScope.() -> Unit
): Unit

AnimatedVisibility can be used to animate the appearance and disappearance of its content as the Transition state changes.

visible defines whether the content should be visible based on transition state T.

modifier modifier for the Layout created to contain the content

enter EnterTransition(s) used for the appearing animation, fading in while expanding vertically by default

exit ExitTransition(s) used for the disappearing animation, fading out while shrinking vertically by default

mutableTransform A block to control the visual transformations 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 is null, meaning no manual transformations are applied. This phase starts when DeferredTransitionState.defer is called and ends when DeferredTransitionState.animateTo is called to start the automatic transition. During this phase, you can manually manipulate the content's transformations (like TransformScope.alpha and TransformScope.scale). These transformations are applied on top of the transition's initial state. Once the transition starts, the manually applied transformations are seamlessly handed off to the configured enter and exit transitions. 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. While in the deferred phase, entering content remains in the EnterExitState.PreEnter state, and exiting content remains in the EnterExitState.Visible state.

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.DeferredAnimatedVisibility
import androidx.compose.animation.MutableTransform
import androidx.compose.animation.core.DeferredTransitionState
import androidx.compose.animation.core.rememberTransition
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.LaunchedEffect
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
var visible by remember { mutableStateOf(true) }
var isBackGestureInProgress by remember { mutableStateOf(false) }
var swipeOffset by remember { mutableStateOf(IntOffset.Zero) }

val transitionState = remember { DeferredTransitionState(visible) }
val transition = rememberTransition(transitionState)
LaunchedEffect(isBackGestureInProgress, visible) {
    if (isBackGestureInProgress) {
        transitionState.defer(visible)
    } else {
        transitionState.animateTo(visible)
    }
}

transition.DeferredAnimatedVisibility(
    visible = { it },
    mutableTransform =
        MutableTransform { fullSize ->
            if (isBackGestureInProgress) {
                val progressX = (swipeOffset.x.toFloat() / fullSize.width).coerceIn(0f, 1f)
                // Shrink the content down to 80% as the user swipes
                scale = 1f - (progressX * 0.2f)
                // Slide the content along the swipe
                offset = swipeOffset
            }
        },
) {
    Box(Modifier.size(200.dp).background(Color.Red))
}

content Content to appear or disappear based on the visibility derived from the Transition.targetState and the provided visible lambda

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun ItemMainContent() {
    Row(Modifier.height(100.dp).fillMaxWidth(), Arrangement.SpaceEvenly) {
        Box(
            Modifier.size(60.dp)
                .align(Alignment.CenterVertically)
                .background(Color(0xffcdb7f6), CircleShape)
        )
        Column(Modifier.align(Alignment.CenterVertically)) {
            Box(Modifier.height(30.dp).width(300.dp).padding(5.dp).background(Color.LightGray))
            Box(Modifier.height(30.dp).width(300.dp).padding(5.dp).background(Color.LightGray))
        }
    }
}

@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SelectableItem() {
    // This sample animates a number of properties, including AnimatedVisibility, as a part of
    // the Transition going between selected and unselected.
    Box(Modifier.padding(15.dp)) {
        var selected by remember { mutableStateOf(false) }
        // Creates a transition to animate visual changes when `selected` is changed.
        val selectionTransition = updateTransition(selected)
        // Animates the border color as a part of the transition
        val borderColor by
            selectionTransition.animateColor { isSelected ->
                if (isSelected) Color(0xff03a9f4) else Color.White
            }
        // Animates the background color when selected state changes
        val contentBackground by
            selectionTransition.animateColor { isSelected ->
                if (isSelected) Color(0xffdbf0fe) else Color.White
            }
        // Animates elevation as a part of the transition
        val elevation by
            selectionTransition.animateDp { isSelected -> if (isSelected) 10.dp else 2.dp }
        Surface(
            shape = RoundedCornerShape(10.dp),
            border = BorderStroke(2.dp, borderColor),
            modifier = Modifier.clickable { selected = !selected },
            color = contentBackground,
            elevation = elevation,
        ) {
            Column(Modifier.fillMaxWidth()) {
                ItemMainContent()
                // Creates an AnimatedVisibility as a part of the transition, so that when
                // selected it's visible. This will hoist all the animations that are internal
                // to AnimatedVisibility (i.e. fade, slide, etc) to the transition. As a result,
                // `selectionTransition` will not finish until all the animations in
                // AnimatedVisibility as well as animations added directly to it have finished.
                selectionTransition.AnimatedVisibility(
                    visible = { it },
                    enter = expandVertically(),
                    exit = shrinkVertically(),
                ) {
                    Box(Modifier.fillMaxWidth().padding(10.dp)) {
                        Text(
                            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed" +
                                " eiusmod tempor incididunt labore et dolore magna aliqua. " +
                                "Ut enim ad minim veniam, quis nostrud exercitation ullamco " +
                                "laboris nisi ut aliquip ex ea commodo consequat. Duis aute " +
                                "irure dolor."
                        )
                    }
                }
            }
        }
    }
}