SharedTransitionScope


SharedTransitionScope provides a coordinator space in which shared elements/ shared bounds (when matched) will transform their bounds from one to another. Their position animation is always relative to the origin defined by where SharedTransitionScope is in the tree.

SharedTransitionScope also creates an overlay, in which all shared elements and shared bounds are rendered by default, so that they are not subject to their parent's fading or clipping, and can therefore transform the bounds without alpha jumps or being unintentionally clipped.

It is also SharedTransitionScope's responsibility to do the SharedContentState key match for all the sharedElement or sharedBounds defined in this scope. Note: key match will not work for SharedContentState created in different SharedTransitionScopes.

SharedTransitionScope oversees all the animations in its scope. When any of the animations is active, isTransitionActive will be true. Once a bounds transform starts, by default the shared element or shared bounds will render the content in the overlay. The rendering will remain in the overlay until all other animations in the SharedTransitionScope are finished (i.e. when isTransitionActive == false).

Summary

Nested types

OverlayClip defines a specific clipping that should be applied to a sharedBounds or sharedElement in the overlay.

PlaceHolderSize defines the size of the space that was or will be occupied by the exiting or entering sharedElement/sharedBounds.

There are two different modes to resize child layout of sharedBounds during bounds transform: 1) scaleToBounds and 2) RemeasureToBounds.

SharedContentConfig allows a shared element to be disabled or enabled dynamically through isEnabled property.

SharedContentState is designed to allow access of the properties of sharedBounds/sharedElement, such as whether a match of the same key has been found in the SharedTransitionScope, its clipPathInOverlay and parentSharedContentState if there is a parent sharedBounds in the layout tree.

Public functions

SharedTransitionScope.OverlayClip
OverlayClip(clipShape: Shape)

Creates an OverlayClip based on a specific clipShape.

Cmn
open SharedTransitionScope.SharedContentConfig

SharedContentConfig is a factory method that returns an SharedContentConfig object with default implementations for all the functions and properties defined in the SharedContentConfig interface.

Cmn
open SharedTransitionScope.SharedContentConfig

SharedContentConfig is a factory method that takes a lambda that can dynamically toggle a shared element between enabled and disabled state, and returns a SharedContentConfig object.

Cmn
open SharedTransitionScope.SharedContentState

Creates and remembers a SharedContentState with a given key and a given SharedContentConfig.

Cmn
Modifier
Modifier.renderInSharedTransitionScopeOverlay(
    zIndexInOverlay: Float,
    renderInOverlay: (SharedTransitionScope) -> Boolean
)

Renders the content in the SharedTransitionScope's overlay, where shared content (i.e. shared elements and shared bounds) is rendered by default.

Cmn
Modifier
Modifier.sharedBounds(
    sharedContentState: SharedTransitionScope.SharedContentState,
    animatedVisibilityScope: AnimatedVisibilityScope,
    enter: EnterTransition,
    exit: ExitTransition,
    boundsTransform: BoundsTransform,
    resizeMode: SharedTransitionScope.ResizeMode,
    placeHolderSize: SharedTransitionScope.PlaceHolderSize,
    renderInOverlayDuringTransition: Boolean,
    zIndexInOverlay: Float,
    clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip
)

sharedBounds is a modifier that tags a layout with a SharedContentState.key, such that entering and exiting shared bounds of the same key share the animated and continuously changing bounds during the layout change.

Cmn
Modifier
Modifier.sharedElement(
    sharedContentState: SharedTransitionScope.SharedContentState,
    animatedVisibilityScope: AnimatedVisibilityScope,
    boundsTransform: BoundsTransform,
    placeHolderSize: SharedTransitionScope.PlaceHolderSize,
    renderInOverlayDuringTransition: Boolean,
    zIndexInOverlay: Float,
    clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip
)

sharedElement is a modifier that tags a layout with a SharedContentState.key, such that entering and exiting shared elements of the same key share the animated and continuously changing bounds during the layout change.

Cmn
Modifier
Modifier.sharedElementWithCallerManagedVisibility(
    sharedContentState: SharedTransitionScope.SharedContentState,
    visible: Boolean,
    boundsTransform: BoundsTransform,
    placeHolderSize: SharedTransitionScope.PlaceHolderSize,
    renderInOverlayDuringTransition: Boolean,
    zIndexInOverlay: Float,
    clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip
)

sharedElementWithCallerManagedVisibility is a modifier that tags a layout with a SharedContentState.key, such that entering and exiting shared elements of the same key share the animated and continuously changing bounds during the layout change.

Cmn
open Modifier

A modifier that anchors a layout at the target position obtained from the lookahead pass during shared element transitions.

Cmn
Modifier

skipToLookaheadSize enables a layout to measure its child with the lookahead constraints, therefore laying out the child as if the transition has finished.

Cmn

Public properties

Boolean

Indicates whether there is any ongoing transition between matched sharedElement or sharedBounds.

Cmn

Inherited functions

From androidx.compose.ui.layout.LookaheadScope
open Offset
LayoutCoordinates.localLookaheadPositionOf(
    sourceCoordinates: LayoutCoordinates,
    relativeToSource: Offset,
    includeMotionFrameOfReference: Boolean
)

Converts relativeToSource in sourceCoordinates's lookahead coordinate space into local lookahead coordinates.

Cmn
LayoutCoordinates

Converts a LayoutCoordinates into a LayoutCoordinates in the Lookahead coordinate space.

Cmn

Public functions

OverlayClip

fun OverlayClip(clipShape: Shape): SharedTransitionScope.OverlayClip

Creates an OverlayClip based on a specific clipShape.

SharedContentConfig

open fun SharedContentConfig(): SharedTransitionScope.SharedContentConfig

SharedContentConfig is a factory method that returns an SharedContentConfig object with default implementations for all the functions and properties defined in the SharedContentConfig interface. More specifically, the returned SharedTransitionScope.SharedContentConfig enables shared elements and bounds, and keeps them enabled while the animation is in-flight. It also sets the SharedContentConfig.alternativeTargetBoundsInTransitionScopeAfterRemoval to null, ensuring the shared element transition is canceled immediately if the incoming shared element is removed during the animation.

SharedContentConfig

open fun SharedContentConfig(isEnabled: SharedTransitionScope.SharedContentState.() -> Boolean): SharedTransitionScope.SharedContentConfig

SharedContentConfig is a factory method that takes a lambda that can dynamically toggle a shared element between enabled and disabled state, and returns a SharedContentConfig object.

Important: If the shared element is already in-flight for the layout that this SharedContentConfig applies to, the on-going animation will be honored even if isEnabled returns false. This is to ensure a continuous experience out-of-the-box by avoiding accidentally removing in-flight animations. If, however, it is desired to disable the shared element while the animation is running, consider implementing interface SharedContentConfig and overriding SharedContentConfig#shouldKeepEnabledForOngoingAnimation.

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.PagerSnapDistance
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.lookaheadScopeCoordinates
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// In this example, we will dynamically enable/disable shared elements for the items in the
// Pager. Specifically, we will only enable shared element transition for items that are
// completely visible in the viewport.
val colors = remember {
    listOf(
        Color(0xFFffd7d7.toInt()),
        Color(0xFFffe9d6.toInt()),
        Color(0xFFfffbd0.toInt()),
        Color(0xFFe3ffd9.toInt()),
        Color(0xFFd0fff8.toInt()),
    )
}
val TwoPagesPerViewport = remember {
    object : PageSize {
        override fun Density.calculateMainAxisPageSize(
            availableSpace: Int,
            pageSpacing: Int,
        ): Int {
            return (availableSpace - 2 * pageSpacing) / 2
        }
    }
}
var selectedColor by remember { mutableStateOf<Color?>(null) }
val pagerState = rememberPagerState { colors.size }
SharedTransitionLayout(modifier = Modifier.fillMaxSize()) {
    AnimatedContent(selectedColor) { colorSelected ->
        if (colorSelected == null) {
            HorizontalPager(
                modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center),
                state = pagerState,
                pageSize = TwoPagesPerViewport,
                pageSpacing = 8.dp,
                snapPosition = SnapPosition.Center,
                flingBehavior =
                    PagerDefaults.flingBehavior(
                        state = pagerState,
                        pagerSnapDistance = PagerSnapDistance.atMost(3),
                    ),
            ) {
                val color = colors[it]
                var coordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
                Box(
                    Modifier.clickable { selectedColor = color }
                        .onPlaced { coordinates = it }
                        .sharedElement(
                            rememberSharedContentState(
                                color,
                                SharedContentConfig {
                                    // This is a lambda that returns a Boolean indicating
                                    // whether shared element should be enabled.
                                    val nonNullCoordinates =
                                        // If the item has never been placed, we will consider
                                        // it enabled.
                                        coordinates ?: return@SharedContentConfig true

                                    // In this specific case, we will use the
                                    // SharedTransitionLayout to approximate viewport.
                                    val scopeCoords =
                                        // Obtain the coordinates of the SharedTransitionLayout/
                                        // SharedTransitionScope.
                                        // Since SharedTransitionScope is a LookaheadScope, we
                                        // can use `lookaheadScopeCoordinates` to acquire the
                                        // coordinates of the scope.
                                        nonNullCoordinates.lookaheadScopeCoordinates(
                                            this@SharedTransitionLayout
                                        )
                                    val (w, h) = scopeCoords.size
                                    // Calculate the relative position of the item within
                                    // SharedTransitionLayout.
                                    val positionInScope =
                                        scopeCoords.localPositionOf(nonNullCoordinates)
                                    // Check the left, top, right, bottom of the relative
                                    // bounds of the item to see if it is within
                                    // SharedTransitionLayout. This result will inform
                                    // whether shared element transition should be enabled
                                    // for this item.
                                    positionInScope.x >= 0 &&
                                        positionInScope.y >= 0 &&
                                        positionInScope.x + nonNullCoordinates.size.width <=
                                            w &&
                                        positionInScope.y + nonNullCoordinates.size.height <= h
                                },
                            ),
                            this@AnimatedContent,
                        )
                        .background(color)
                        .size(150.dp)
                )
            }
        } else {
            Box(
                Modifier.sharedElement(
                        rememberSharedContentState(colorSelected),
                        this@AnimatedContent,
                    )
                    .background(colorSelected)
                    .aspectRatio(1f)
                    .fillMaxWidth()
                    .clickable { selectedColor = null }
            )
        }
    }
}
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
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.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.Absolute.SpaceEvenly
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// In this example, we will enable shared element transition for transitioning from A to B,
// from A to C, but not the other directions such as B -> A, or B -> C
@Composable
fun ScreenA(modifier: Modifier = Modifier) {
    Box(modifier.size(200.dp).background(Color.Red), contentAlignment = Alignment.Center) {
        Text(text = "A", fontSize = 50.sp)
    }
}

@Composable
fun ScreenB(modifier: Modifier = Modifier) {
    Box(modifier.size(400.dp).background(Color.Yellow), contentAlignment = Alignment.Center) {
        Text(text = "B", fontSize = 50.sp)
    }
}

@Composable
fun ScreenC(modifier: Modifier = Modifier) {
    Box(
        modifier.size(100.dp, 300.dp).background(Color.Blue),
        contentAlignment = Alignment.Center,
    ) {
        Text(text = "C", fontSize = 50.sp)
    }
}

var targetState by remember { mutableStateOf("A") }
val listOfEnabledStatePairs = remember { mutableStateListOf("A" to "B", "A" to "C") }

Column {
    Row(Modifier.fillMaxWidth(), horizontalArrangement = SpaceEvenly) {
        Button(onClick = { targetState = "A" }) { Text("To A") }
        Button(onClick = { targetState = "B" }) { Text("To B") }
        Button(onClick = { targetState = "C" }) { Text("To C") }
    }
    SharedTransitionLayout {
        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            val animatedContentTransition = updateTransition(targetState)

            val config = remember {
                // Creates a SharedContentConfig to dynamically enable/disable shared elements
                SharedContentConfig {
                    // Returns whether a shared element should be enabled based on
                    // the current state of the target state of the AnimatedContent.
                    listOfEnabledStatePairs.contains(
                        animatedContentTransition.currentState to
                            animatedContentTransition.targetState
                    )
                }
            }
            animatedContentTransition.AnimatedContent(
                transitionSpec = { fadeIn() togetherWith fadeOut() using null }
            ) {
                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    when (it) {
                        "A" ->
                            ScreenA(
                                Modifier.sharedElement(
                                    rememberSharedContentState("square", config),
                                    this@AnimatedContent,
                                )
                            )

                        "B" ->
                            ScreenB(
                                Modifier.sharedElement(
                                    rememberSharedContentState("square", config),
                                    this@AnimatedContent,
                                )
                            )

                        "C" ->
                            ScreenC(
                                Modifier.sharedElement(
                                    rememberSharedContentState("square", config),
                                    this@AnimatedContent,
                                )
                            )
                    }
                }
            }
        }
    }
}
Parameters
isEnabled: SharedTransitionScope.SharedContentState.() -> Boolean

A lambda that returns a boolean indicating whether the shared element is enabled.

rememberSharedContentState

@Composable
open fun rememberSharedContentState(
    key: Any,
    config: SharedTransitionScope.SharedContentConfig = SharedTransitionDefaults.SharedContentConfig
): SharedTransitionScope.SharedContentState

Creates and remembers a SharedContentState with a given key and a given SharedContentConfig.

key will be used to match a shared element against others in the same SharedTransitionScope.

config defines whether the shared element is enabled or disabled, and the alternative target bounds if the shared element is disposed amid animation (e.g., scrolled out of the viewport and subsequently disposed). By default, the shared element is enabled and the alternative target bounds are not defined. Hence the default behavior is to stop the animation when the target shared element (i.e. shared element in the incoming/target content) is removed.

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.snapping.SnapPosition
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PageSize
import androidx.compose.foundation.pager.PagerDefaults
import androidx.compose.foundation.pager.PagerSnapDistance
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.lookaheadScopeCoordinates
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// In this example, we will dynamically enable/disable shared elements for the items in the
// Pager. Specifically, we will only enable shared element transition for items that are
// completely visible in the viewport.
val colors = remember {
    listOf(
        Color(0xFFffd7d7.toInt()),
        Color(0xFFffe9d6.toInt()),
        Color(0xFFfffbd0.toInt()),
        Color(0xFFe3ffd9.toInt()),
        Color(0xFFd0fff8.toInt()),
    )
}
val TwoPagesPerViewport = remember {
    object : PageSize {
        override fun Density.calculateMainAxisPageSize(
            availableSpace: Int,
            pageSpacing: Int,
        ): Int {
            return (availableSpace - 2 * pageSpacing) / 2
        }
    }
}
var selectedColor by remember { mutableStateOf<Color?>(null) }
val pagerState = rememberPagerState { colors.size }
SharedTransitionLayout(modifier = Modifier.fillMaxSize()) {
    AnimatedContent(selectedColor) { colorSelected ->
        if (colorSelected == null) {
            HorizontalPager(
                modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.Center),
                state = pagerState,
                pageSize = TwoPagesPerViewport,
                pageSpacing = 8.dp,
                snapPosition = SnapPosition.Center,
                flingBehavior =
                    PagerDefaults.flingBehavior(
                        state = pagerState,
                        pagerSnapDistance = PagerSnapDistance.atMost(3),
                    ),
            ) {
                val color = colors[it]
                var coordinates: LayoutCoordinates? by remember { mutableStateOf(null) }
                Box(
                    Modifier.clickable { selectedColor = color }
                        .onPlaced { coordinates = it }
                        .sharedElement(
                            rememberSharedContentState(
                                color,
                                SharedContentConfig {
                                    // This is a lambda that returns a Boolean indicating
                                    // whether shared element should be enabled.
                                    val nonNullCoordinates =
                                        // If the item has never been placed, we will consider
                                        // it enabled.
                                        coordinates ?: return@SharedContentConfig true

                                    // In this specific case, we will use the
                                    // SharedTransitionLayout to approximate viewport.
                                    val scopeCoords =
                                        // Obtain the coordinates of the SharedTransitionLayout/
                                        // SharedTransitionScope.
                                        // Since SharedTransitionScope is a LookaheadScope, we
                                        // can use `lookaheadScopeCoordinates` to acquire the
                                        // coordinates of the scope.
                                        nonNullCoordinates.lookaheadScopeCoordinates(
                                            this@SharedTransitionLayout
                                        )
                                    val (w, h) = scopeCoords.size
                                    // Calculate the relative position of the item within
                                    // SharedTransitionLayout.
                                    val positionInScope =
                                        scopeCoords.localPositionOf(nonNullCoordinates)
                                    // Check the left, top, right, bottom of the relative
                                    // bounds of the item to see if it is within
                                    // SharedTransitionLayout. This result will inform
                                    // whether shared element transition should be enabled
                                    // for this item.
                                    positionInScope.x >= 0 &&
                                        positionInScope.y >= 0 &&
                                        positionInScope.x + nonNullCoordinates.size.width <=
                                            w &&
                                        positionInScope.y + nonNullCoordinates.size.height <= h
                                },
                            ),
                            this@AnimatedContent,
                        )
                        .background(color)
                        .size(150.dp)
                )
            }
        } else {
            Box(
                Modifier.sharedElement(
                        rememberSharedContentState(colorSelected),
                        this@AnimatedContent,
                    )
                    .background(colorSelected)
                    .aspectRatio(1f)
                    .fillMaxWidth()
                    .clickable { selectedColor = null }
            )
        }
    }
}
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
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.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.Absolute.SpaceEvenly
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// In this example, we will enable shared element transition for transitioning from A to B,
// from A to C, but not the other directions such as B -> A, or B -> C
@Composable
fun ScreenA(modifier: Modifier = Modifier) {
    Box(modifier.size(200.dp).background(Color.Red), contentAlignment = Alignment.Center) {
        Text(text = "A", fontSize = 50.sp)
    }
}

@Composable
fun ScreenB(modifier: Modifier = Modifier) {
    Box(modifier.size(400.dp).background(Color.Yellow), contentAlignment = Alignment.Center) {
        Text(text = "B", fontSize = 50.sp)
    }
}

@Composable
fun ScreenC(modifier: Modifier = Modifier) {
    Box(
        modifier.size(100.dp, 300.dp).background(Color.Blue),
        contentAlignment = Alignment.Center,
    ) {
        Text(text = "C", fontSize = 50.sp)
    }
}

var targetState by remember { mutableStateOf("A") }
val listOfEnabledStatePairs = remember { mutableStateListOf("A" to "B", "A" to "C") }

Column {
    Row(Modifier.fillMaxWidth(), horizontalArrangement = SpaceEvenly) {
        Button(onClick = { targetState = "A" }) { Text("To A") }
        Button(onClick = { targetState = "B" }) { Text("To B") }
        Button(onClick = { targetState = "C" }) { Text("To C") }
    }
    SharedTransitionLayout {
        Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
            val animatedContentTransition = updateTransition(targetState)

            val config = remember {
                // Creates a SharedContentConfig to dynamically enable/disable shared elements
                SharedContentConfig {
                    // Returns whether a shared element should be enabled based on
                    // the current state of the target state of the AnimatedContent.
                    listOfEnabledStatePairs.contains(
                        animatedContentTransition.currentState to
                            animatedContentTransition.targetState
                    )
                }
            }
            animatedContentTransition.AnimatedContent(
                transitionSpec = { fadeIn() togetherWith fadeOut() using null }
            ) {
                Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
                    when (it) {
                        "A" ->
                            ScreenA(
                                Modifier.sharedElement(
                                    rememberSharedContentState("square", config),
                                    this@AnimatedContent,
                                )
                            )

                        "B" ->
                            ScreenB(
                                Modifier.sharedElement(
                                    rememberSharedContentState("square", config),
                                    this@AnimatedContent,
                                )
                            )

                        "C" ->
                            ScreenC(
                                Modifier.sharedElement(
                                    rememberSharedContentState("square", config),
                                    this@AnimatedContent,
                                )
                            )
                    }
                }
            }
        }
    }
}

renderInSharedTransitionScopeOverlay

fun Modifier.renderInSharedTransitionScopeOverlay(
    zIndexInOverlay: Float = 0.0f,
    renderInOverlay: (SharedTransitionScope) -> Boolean = SharedTransitionDefaults.RenderInOverlay
): Modifier

Renders the content in the SharedTransitionScope's overlay, where shared content (i.e. shared elements and shared bounds) is rendered by default. This is useful for rendering content that is not shared on top of shared content to preserve a specific spatial relationship.

renderInOverlay dynamically controls whether the content should be rendered in the SharedTransitionScope's overlay. By default, it returns the same value as SharedTransitionScope.isTransitionActive. This means the default behavior is to render the child layout of this modifier in the overlay only when the transition is active.

IMPORTANT: When elevating layouts into the overlay, the layout is no longer subjected to 1) its parent's clipping, and 2) parent's layer transform (e.g. alpha, scale, etc). Therefore, it is recommended to create an enter/exit animation (e.g. using AnimatedVisibilityScope.animateEnterExit) for the child layout to avoid any abrupt visual changes.

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
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.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.Share
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.draw.clip
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// Create an Image that will be shared between the two shared elements.
@Composable
fun Cat(modifier: Modifier = Modifier) {
    Image(
        painterResource(id = R.drawable.yt_profile),
        contentDescription = "cute cat",
        contentScale = ContentScale.FillHeight,
        modifier = modifier.clip(shape = RoundedCornerShape(10)),
    )
}

var showThumbnail by remember { mutableStateOf(true) }
SharedTransitionLayout(
    Modifier.clickable { showThumbnail = !showThumbnail }.fillMaxSize().padding(10.dp)
) {
    Column(Modifier.padding(10.dp)) {
        // Create an AnimatedVisibility for the shared element, so that the layout siblings
        // (i.e. the two boxes below) will move in to fill the space during the exit transition.
        AnimatedVisibility(visible = showThumbnail) {
            Cat(
                Modifier.size(100.dp)
                    // Create a shared element, using string as the key
                    .sharedElement(
                        rememberSharedContentState(key = "YT"),
                        this@AnimatedVisibility,
                    )
            )
        }
        Box(
            Modifier.fillMaxWidth()
                .height(100.dp)
                .background(Color(0xffffcc5c), RoundedCornerShape(5.dp))
        )
        Box(
            Modifier.fillMaxWidth()
                .height(100.dp)
                .background(Color(0xff2a9d84), RoundedCornerShape(5.dp))
        )
    }
    Box(modifier = Modifier.fillMaxSize()) {
        AnimatedVisibility(!showThumbnail) {
            Cat(
                Modifier.fillMaxSize()
                    // Create another shared element, and make sure the string key matches
                    // the other shared element.
                    .sharedElement(
                        rememberSharedContentState(key = "YT"),
                        this@AnimatedVisibility,
                    )
            )
        }
        FloatingActionButton(
            modifier =
                Modifier.padding(20.dp)
                    .align(Alignment.BottomEnd)
                    // During shared element transition, shared elements will be rendered in
                    // overlay to escape any clipping or layer transform from parents. It also
                    // means they will render over on top of UI elements such as Floating Action
                    // Button. Once the transition is finished, they will be dropped from the
                    // overlay to their own DrawScopes. To help support keeping specific UI
                    // elements always on top, Modifier.renderInSharedTransitionScopeOverlay
                    // will temporarily elevate them into the overlay as well. By default,
                    // this modifier keeps content in overlay during the time when the
                    // shared transition is active (i.e.
                    // SharedTransitionScope#isTransitionActive).
                    // The duration can be customize via `renderInOverlay` parameter.
                    .renderInSharedTransitionScopeOverlay(
                        // zIndexInOverlay by default is 0f for this modifier and for shared
                        // elements. By overwriting zIndexInOverlay to 1f, we can ensure this
                        // FAB is rendered on top of the shared elements.
                        zIndexInOverlay = 1f
                    ),
            onClick = {},
        ) {
            Icon(Icons.Default.Favorite, contentDescription = "favorite")
        }
    }
}
Parameters
zIndexInOverlay: Float = 0.0f

The zIndex of the content in the overlay. Defaults to 0f.

renderInOverlay: (SharedTransitionScope) -> Boolean = SharedTransitionDefaults.RenderInOverlay

renderInOverlay determines when the content should be rendered in the overlay. Defaults to SharedTransitionDefaults.RenderInOverlay, which renders the content in the overlay only when the transition is active.

sharedBounds

fun Modifier.sharedBounds(
    sharedContentState: SharedTransitionScope.SharedContentState,
    animatedVisibilityScope: AnimatedVisibilityScope,
    enter: EnterTransition = fadeIn(),
    exit: ExitTransition = fadeOut(),
    boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform,
    resizeMode: SharedTransitionScope.ResizeMode = scaleToBounds(ContentScale.FillWidth, Center),
    placeHolderSize: SharedTransitionScope.PlaceHolderSize = contentSize,
    renderInOverlayDuringTransition: Boolean = true,
    zIndexInOverlay: Float = 0.0f,
    clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip
): Modifier

sharedBounds is a modifier that tags a layout with a SharedContentState.key, such that entering and exiting shared bounds of the same key share the animated and continuously changing bounds during the layout change. The bounds will be animated from the initial bounds defined by the exiting shared bounds to the target bounds calculated based on the incoming shared shared bounds. The animation for the bounds can be customized using boundsTransform. The target bounds for sharedBounds are determined by the bounds of the sharedBounds becoming visible based on the target state of animatedVisibilityScope.

In contrast to sharedElement, sharedBounds is designed for shared content that has the visually different content. While the sharedBounds keeps the continuity of the bounds, the incoming and outgoing content within the sharedBounds will enter and exit in an enter/exit transition using enter/exit. By default, fadeIn and fadeOut are used to fade the content in or out.

resizeMode defines how the child layout of sharedBounds should be resized during boundsTransform. By default, scaleToBounds will be used to measure the child content with lookahead constraints to arrive at the stable layout. Then the stable layout will be scaled to fit or fill (depending on the content scale used) the transforming bounds of sharedBounds. If there's a need to relayout content (rather than scaling) based on the animated bounds size (e.g. dynamically resizing a Row), it's recommended to use RemeasureToBounds as the resizeMode.

Important: When a shared bounds finds its match and starts a transition, it will be rendered into the overlay of the SharedTransitionScope in order to avoid being faded in/out along with its parents or clipped by its parent as it transforms to the target size and position. This also means that any clipping or fading for the shared elements will need to be applied explicitly as the child of sharedBounds (i.e. after sharedBounds modifier in the modifier chain). For example: Modifier.sharedBounds(...).clip(shape = RoundedCornerShape(20.dp))

By default, the sharedBounds is clipped by the clipInOverlayDuringTransition of its parent sharedBounds in the layout tree. If the sharedBounds has no parent sharedBounds or if the parent sharedBounds has no clipping defined, it'll not be clipped. If additional clipping is desired to ensure child sharedBounds or child sharedElement don't move outside of the this sharedBounds's visual bounds in the overlay, clipInOverlayDuringTransition can be used to specify the clipping.

While the shared bounds are rendered in overlay during the transition, its zIndexInOverlay can be specified to allow them to render in a different order than their placement/zOrder when not in the overlay. For example, the title of a page is typically placed and rendered before the content below. During the transition, it may be desired to animate the title over on top of the other shared elements on that page to indicate significance or a point of interest. zIndexInOverlay can be used to facilitate such use cases. zIndexInOverlay is 0f by default.

renderInOverlayDuringTransition is true by default. In some rare use cases, there may be no clipping or layer transform (fade, scale, etc) in the application that prevents shared elements from transitioning from one bounds to another without any clipping or sudden alpha change. In such cases, renderInOverlayDuringTransition could be specified to false.

During a shared bounds transition, the space that was occupied by the exiting shared bounds and the space that the entering shared bounds will take up are considered place holders. Their sizes during the shared element transition can be configured through placeHolderSize. By default, it will be the same as the content size of the respective shared bounds. It can also be set to animatedSize or any other PlaceHolderSize to report to their parent layout an animated size to create a visual effect where the parent layout dynamically adjusts the layout to accommodate the animated size of the shared elements.

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

var showText by remember { mutableStateOf(true) }
val rememberSharedKey = remember { Any() }
SharedTransitionLayout(Modifier.clickable { showText = !showText }) {
    AnimatedContent(
        targetState = showText,
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center,
    ) { showText ->
        if (showText) {
            Text(
                text =
                    "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent fringilla" +
                        " mollis efficitur. Maecenas sit amet urna eu urna blandit suscipit efficitur" +
                        " eget mauris. Nullam eget aliquet ligula. Nunc id euismod elit. Morbi aliquam" +
                        " enim eros, eget consequat dolor consequat id. Quisque elementum faucibus" +
                        " congue. Curabitur mollis aliquet turpis, ut pellentesque justo eleifend nec.\n",
                fontSize = 20.sp,
                modifier =
                    Modifier.padding(20.dp)
                        // Creates a shared bounds for the text so that we can animate from the
                        // bounds of the text to the bounds of the image below.
                        .sharedBounds(
                            // Here we use an object created above as the key for the shared
                            // bounds transition.
                            sharedContentState =
                                rememberSharedContentState(key = rememberSharedKey),
                            animatedVisibilityScope = this,
                            // As the bounds transition from the text from/to the image, the
                            // text
                            // will be fading within in the bounds. This is also the default
                            // behavior.
                            enter = fadeIn(),
                            exit = fadeOut(),
                            // Since the text and the image have different aspect ratios, as
                            // the bounds transition from one to the other, we need to define
                            // how to fit the content in the changing bounds. Here we will
                            // be using crop to fit the content.
                            resizeMode = scaleToBounds(contentScale = ContentScale.Crop),
                        ),
            )
        } else {
            Image(
                painterResource(id = R.drawable.yt_profile),
                contentDescription = "cute cat",
                modifier =
                    Modifier.wrapContentSize()
                        // Creates a shared bounds for the image so that we can animate from the
                        // bounds of the text to the bounds of this image, and vice versa.
                        .sharedBounds(
                            sharedContentState =
                                rememberSharedContentState(key = rememberSharedKey),
                            animatedVisibilityScope = this,
                            resizeMode = scaleToBounds(contentScale = ContentScale.Crop),
                        )
                        .requiredSize(200.dp)
                        .clip(shape = RoundedCornerShape(10)),
            )
        }
    }
}

Since sharedBounds show both incoming and outgoing content in its bounds, it affords opportunities to do interesting transitions where additional sharedElement and sharedBounds can be nested in a parent sharedBounds. See the sample code below for a more complex example with nested shared bounds/elements.

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

// Nested shared bounds sample.
val selectionColor = Color(0xff3367ba)
var expanded by remember { mutableStateOf(true) }
SharedTransitionLayout(
    Modifier.fillMaxSize().clickable { expanded = !expanded }.background(Color(0x88000000))
) {
    AnimatedVisibility(
        visible = expanded,
        enter = EnterTransition.None,
        exit = ExitTransition.None,
    ) {
        Box(modifier = Modifier.fillMaxSize()) {
            Surface(
                Modifier.align(Alignment.BottomCenter)
                    .padding(20.dp)
                    .sharedBounds(
                        rememberSharedContentState(key = "container"),
                        this@AnimatedVisibility,
                    )
                    .requiredHeightIn(max = 60.dp),
                shape = RoundedCornerShape(50),
            ) {
                Row(
                    Modifier.padding(10.dp)
                        // By using Modifier.skipToLookaheadSize(), we are telling the layout
                        // system to layout the children of this node as if the animations had
                        // all finished. This avoid re-laying out the Row with animated width,
                        // which is _sometimes_ desirable. Try removing this modifier and
                        // observe the effect.
                        .skipToLookaheadSize()
                ) {
                    Icon(
                        Icons.Outlined.Share,
                        contentDescription = "Share",
                        modifier =
                            Modifier.padding(
                                top = 10.dp,
                                bottom = 10.dp,
                                start = 10.dp,
                                end = 20.dp,
                            ),
                    )
                    Icon(
                        Icons.Outlined.Favorite,
                        contentDescription = "Favorite",
                        modifier =
                            Modifier.padding(
                                top = 10.dp,
                                bottom = 10.dp,
                                start = 10.dp,
                                end = 20.dp,
                            ),
                    )
                    Icon(
                        Icons.Outlined.Create,
                        contentDescription = "Create",
                        tint = Color.White,
                        modifier =
                            Modifier.sharedBounds(
                                    rememberSharedContentState(key = "icon_background"),
                                    this@AnimatedVisibility,
                                )
                                .background(selectionColor, RoundedCornerShape(50))
                                .padding(
                                    top = 10.dp,
                                    bottom = 10.dp,
                                    start = 20.dp,
                                    end = 20.dp,
                                )
                                .sharedElement(
                                    rememberSharedContentState(key = "icon"),
                                    this@AnimatedVisibility,
                                ),
                    )
                }
            }
        }
    }
    AnimatedVisibility(
        visible = !expanded,
        enter = EnterTransition.None,
        exit = ExitTransition.None,
    ) {
        Box(modifier = Modifier.fillMaxSize()) {
            Surface(
                Modifier.align(Alignment.BottomEnd)
                    .padding(30.dp)
                    .sharedBounds(
                        rememberSharedContentState(key = "container"),
                        this@AnimatedVisibility,
                        enter = EnterTransition.None,
                    )
                    .sharedBounds(
                        rememberSharedContentState(key = "icon_background"),
                        this@AnimatedVisibility,
                        resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
                        enter = EnterTransition.None,
                        exit = ExitTransition.None,
                    ),
                shape = RoundedCornerShape(30.dp),
                color = selectionColor,
            ) {
                Icon(
                    Icons.Outlined.Create,
                    contentDescription = "Create",
                    tint = Color.White,
                    modifier =
                        Modifier.padding(30.dp)
                            .size(40.dp)
                            .sharedElement(
                                rememberSharedContentState(key = "icon"),
                                this@AnimatedVisibility,
                            ),
                )
            }
        }
    }
}
Parameters
sharedContentState: SharedTransitionScope.SharedContentState

The SharedContentState of the shared element. This defines the key used for matching shared elements.

animatedVisibilityScope: AnimatedVisibilityScope

The AnimatedVisibilityScope in which the shared element is declared. This helps the system determine if the shared element is incoming or outgoing.

enter: EnterTransition = fadeIn()

The enter transition used for incoming content while it's displayed within the transforming bounds. This defaults to a fade-in.

exit: ExitTransition = fadeOut()

The exit transition used for outgoing content while it's displayed within the transforming bounds. This defaults to a fade-out.

boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform

A BoundsTransform to customize the animation specification based on the shared element's initial and target bounds for the transition.

resizeMode: SharedTransitionScope.ResizeMode = scaleToBounds(ContentScale.FillWidth, Center)

A ResizeMode that defines how the child layout of sharedBounds should be resized during boundsTransform. By default, scaleToBounds is used to scale the child content to fit the transforming bounds.

placeHolderSize: SharedTransitionScope.PlaceHolderSize = contentSize

A PlaceHolderSize that defines the size the transforming layout reports to the layout system during the transition. By default, this is the shared bounds' content size (without any scaling or transformation).

renderInOverlayDuringTransition: Boolean = true

Whether the shared bounds should be rendered in the overlay during the transition. Defaults to true.

zIndexInOverlay: Float = 0.0f

The zIndex of the shared bounds within the overlay, enabling custom z-ordering for multiple shared bounds or elements.

clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip

The clipping path of the shared bounds in the overlay. By default, it uses the resolved clip path from its parent sharedBounds (if applicable).

See also
sharedBounds

sharedElement

fun Modifier.sharedElement(
    sharedContentState: SharedTransitionScope.SharedContentState,
    animatedVisibilityScope: AnimatedVisibilityScope,
    boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform,
    placeHolderSize: SharedTransitionScope.PlaceHolderSize = contentSize,
    renderInOverlayDuringTransition: Boolean = true,
    zIndexInOverlay: Float = 0.0f,
    clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip
): Modifier

sharedElement is a modifier that tags a layout with a SharedContentState.key, such that entering and exiting shared elements of the same key share the animated and continuously changing bounds during the layout change. The bounds will be animated from the initial bounds defined by the exiting shared element to the target bounds calculated based on the incoming shared element. The animation for the bounds can be customized using boundsTransform. During the bounds transform, sharedElement will re-measure and relayout its child layout using fixed constraints derived from its animated size, similar to RemeasureToBounds resizeMode in sharedBounds.

In contrast to sharedBounds, sharedElement is designed for shared content that has the exact match in terms of visual content and layout when the measure constraints are the same. Such examples include image assets, icons, MovableContent etc. Only the shared element that is becoming visible will be rendered during the transition. The bounds for shared element are determined by the bounds of the shared element becoming visible based on the target state of animatedVisibilityScope.

Important: When a shared element finds its match and starts a transition, it will be rendered into the overlay of the SharedTransitionScope in order to avoid being faded in/out along with its parents or clipped by its parent as it transforms to the target size and position. This also means that any clipping or fading for the shared elements will need to be applied explicitly as the child of sharedElement (i.e. after sharedElement modifier in the modifier chain). For example: Modifier.sharedElement(...).clip(shape = RoundedCornerShape(20.dp)).animateEnterExit(...)

By default, the sharedElement is clipped by the clipInOverlayDuringTransition of its parent sharedBounds. If the sharedElement has no parent sharedBounds or if the parent sharedBounds has no clipping defined, it'll not be clipped. If additional clipping is desired to ensure sharedElement doesn't move outside of a visual bounds, clipInOverlayDuringTransition can be used to specify the clipping for when the shared element is going through an active transition towards a new target bounds.

While the shared elements are rendered in overlay during the transition, its zIndexInOverlay can be specified to allow shared elements to render in a different order than their placement/zOrder when not in the overlay. For example, the title of a page is typically placed and rendered before the content below. During the transition, it may be desired to animate the title over on top of the other shared elements on that page to indicate significance or a point of interest. zIndexInOverlay can be used to facilitate such use cases. zIndexInOverlay is 0f by default.

renderInOverlayDuringTransition is true by default. In some rare use cases, there may be no clipping or layer transform (fade, scale, etc) in the application that prevents shared elements from transitioning from one bounds to another without any clipping or sudden alpha change. In such cases, renderInOverlayDuringTransition could be specified to false.

During a shared element transition, the space that was occupied by the exiting shared element and the space that the entering shared element will take up are considered place holders. Their sizes during the shared element transition can be configured through placeHolderSize. By default, it will be the same as the content size of the respective shared element. It can also be set to animatedSize or any other PlaceHolderSize to report to their parent layout an animated size to create a visual effect where the parent layout dynamically adjusts the layout to accommodate the animated size of the shared elements.

Below is an example of using shared elements in a transition from a list to a details page. For a more complex example using sharedElement along with sharedBounds, see the API documentation for SharedTransitionLayout.

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Text
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

var itemSelected by remember { mutableStateOf<Int?>(null) }
SharedTransitionLayout(modifier = Modifier.clipToBounds().fillMaxSize()) {
    val listState = rememberLazyListState()
    AnimatedContent(itemSelected) { selected ->
        when (selected) {
            null -> { // No item selected, show list
                LazyColumn(state = listState) {
                    items(50) { item ->
                        Row(
                            modifier = Modifier.clickable { itemSelected = item }.fillMaxWidth()
                        ) {
                            Image(
                                painter = painterResource(R.drawable.yt_profile),
                                modifier =
                                    Modifier.size(100.dp)
                                        .sharedElement(
                                            rememberSharedContentState(key = "item-image$item"),
                                            animatedVisibilityScope = this@AnimatedContent,
                                        ),
                                contentScale = ContentScale.Crop,
                                contentDescription = null,
                            )
                            Spacer(Modifier.size(15.dp))
                            Text(
                                "Item $item",
                                modifier = Modifier.align(Alignment.CenterVertically),
                            )
                        }
                    }
                }
            }
            else -> { // show detail for item selected
                Column(modifier = Modifier.fillMaxSize().clickable { itemSelected = null }) {
                    Image(
                        painter = painterResource(R.drawable.yt_profile),
                        modifier =
                            Modifier.sharedElement(
                                    rememberSharedContentState(key = "item-image$selected"),
                                    animatedVisibilityScope = this@AnimatedContent,
                                )
                                .fillMaxWidth(),
                        contentScale = ContentScale.Crop,
                        contentDescription = null,
                    )
                    Text("Item $itemSelected", fontSize = 23.sp)
                }
            }
        }
    }
}
Parameters
sharedContentState: SharedTransitionScope.SharedContentState

The SharedContentState of the shared element. This defines the key used for matching shared elements.

animatedVisibilityScope: AnimatedVisibilityScope

The AnimatedVisibilityScope in which the shared element is declared. This helps the system determine if the shared element is incoming or outgoing.

boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform

A BoundsTransform to customize the animation specification based on the shared element's initial and target bounds during the transition.

placeHolderSize: SharedTransitionScope.PlaceHolderSize = contentSize

A PlaceHolderSize that defines the size the transforming layout reports to the layout system during the transition. By default, this is the shared element's content size (without any scaling or transformation).

renderInOverlayDuringTransition: Boolean = true

Whether the shared element should be rendered in the overlay during the transition. Defaults to true.

zIndexInOverlay: Float = 0.0f

The zIndex of the shared element within the overlay, enabling custom z-ordering for multiple shared elements.

clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip

The clipping path of the shared element in the overlay. By default, it uses the resolved clip path from its parent sharedBounds (if applicable).

sharedElementWithCallerManagedVisibility

fun Modifier.sharedElementWithCallerManagedVisibility(
    sharedContentState: SharedTransitionScope.SharedContentState,
    visible: Boolean,
    boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform,
    placeHolderSize: SharedTransitionScope.PlaceHolderSize = contentSize,
    renderInOverlayDuringTransition: Boolean = true,
    zIndexInOverlay: Float = 0.0f,
    clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip
): Modifier

sharedElementWithCallerManagedVisibility is a modifier that tags a layout with a SharedContentState.key, such that entering and exiting shared elements of the same key share the animated and continuously changing bounds during the layout change. The bounds will be animated from the initial bounds defined by the exiting shared element to the target bounds calculated based on the incoming shared element. The animation for the bounds can be customized using boundsTransform.

Compared to sharedElement, sharedElementWithCallerManagedVisibility is designed for shared element transitions where the shared element is not a part of the content that is being animated out by AnimatedVisibility. Therefore, it is the caller's responsibility to explicitly remove the exiting shared element (i.e. shared elements where visible == false) from the tree as appropriate. Typically this is when the transition is finished (i.e. SharedTransitionScope.isTransitionActive == false). The target bounds is derived from the sharedElementWithCallerManagedVisibility with visible being true.

In contrast to sharedBounds, this modifier is intended for shared content that has the exact match in terms of visual content and layout when the measure constraints are the same. Such examples include image assets, icons, MovableContent etc. Only the shared element that is becoming visible will be rendered during the transition.

Important: When a shared element finds its match and starts a transition, it will be rendered into the overlay of the SharedTransitionScope in order to avoid being faded in/out along with its parents or clipped by its parent as it transforms to the target size and position. This also means that any clipping or fading for the shared elements will need to be applied explicitly as the child of sharedElementWithCallerManagedVisibility (i.e. after sharedElementWithCallerManagedVisibility modifier in the modifier chain). For example:

Modifier.sharedElementWithCallerManagedVisibility(...)
.clip(shape = RoundedCornerShape(20.dp))

By default, the sharedElementWithCallerManagedVisibility is clipped by the clipInOverlayDuringTransition of its parent sharedBounds. If the sharedElementWithCallerManagedVisibility has no parent sharedBounds or if the parent sharedBounds has no clipping defined, it'll not be clipped. If additional clipping is desired to ensure sharedElementWithCallerManagedVisibility doesn't move outside of a visual bounds, clipInOverlayDuringTransition can be used to specify the clipping for when the shared element is going through an active transition towards a new target bounds.

While the shared elements are rendered in overlay during the transition, its zIndexInOverlay can be specified to allow shared elements to render in a different order than their placement/zOrder when not in the overlay. For example, the title of a page is typically placed and rendered before the content below. During the transition, it may be desired to animate the title over on top of the other shared elements on that page to indicate significance or a point of interest. zIndexInOverlay can be used to facilitate such use cases. zIndexInOverlay is 0f by default.

renderInOverlayDuringTransition is true by default. In some rare use cases, there may be no clipping or layer transform (fade, scale, etc) in the application that prevents shared elements from transitioning from one bounds to another without any clipping or sudden alpha change. In such cases, renderInOverlayDuringTransition could be specified to false.

During a shared element transition, the space that was occupied by the exiting shared element and the space that the entering shared element will take up are considered place holders. Their sizes during the shared element transition can be configured through placeHolderSize. By default, it will be the same as the content size of the respective shared element. It can also be set to animatedSize or any other PlaceHolderSize to report to their parent layout an animated size to create a visual effect where the parent layout dynamically adjusts the layout to accommodate the animated size of the shared elements.

import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
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.shape.RoundedCornerShape
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
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.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

var showThumbnail by remember { mutableStateOf(true) }
val movableContent = remember {
    movableContentOf {
        val cornerRadius = animateDpAsState(targetValue = if (!showThumbnail) 20.dp else 5.dp)
        Image(
            painterResource(id = R.drawable.yt_profile),
            contentDescription = "cute cat",
            contentScale = ContentScale.FillHeight,
            modifier = Modifier.clip(shape = RoundedCornerShape(cornerRadius.value)),
        )
    }
}
SharedTransitionLayout(
    Modifier.clickable { showThumbnail = !showThumbnail }.fillMaxSize().padding(10.dp)
) {
    Column {
        Box(
            // When using Modifier.sharedElementWithCallerManagedVisibility(), even when
            // visible == false, the layout will continue to occupy space in its parent layout.
            // The content will continue to be composed, unless the content is [MovableContent]
            // like in this example below.
            Modifier.sharedElementWithCallerManagedVisibility(
                    rememberSharedContentState(key = "YT"),
                    showThumbnail,
                )
                .size(100.dp)
        ) {
            if (showThumbnail) {
                movableContent()
            }
        }
        Box(
            Modifier.fillMaxWidth()
                .height(100.dp)
                .background(Color(0xffffcc5c), RoundedCornerShape(5.dp))
        )
        Box(
            Modifier.fillMaxWidth()
                .height(100.dp)
                .background(Color(0xff2a9d84), RoundedCornerShape(5.dp))
        )
    }
    Box(
        Modifier.fillMaxSize()
            .aspectRatio(1f)
            .sharedElementWithCallerManagedVisibility(
                rememberSharedContentState(key = "YT"),
                !showThumbnail,
            )
    ) {
        if (!showThumbnail) {
            movableContent()
        }
    }
}
Parameters
sharedContentState: SharedTransitionScope.SharedContentState

The SharedContentState of the shared element. This defines the key used for matching shared elements.

visible: Boolean

Whether the shared element is visible.

boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform

A BoundsTransform to customize the animation specification based on the shared element's initial and target bounds during the transition.

placeHolderSize: SharedTransitionScope.PlaceHolderSize = contentSize

A PlaceHolderSize that defines the size the transforming layout reports to the layout system during the transition. By default, this is the shared element's content size (without any scaling or transformation).

renderInOverlayDuringTransition: Boolean = true

Whether the shared element should be rendered in the overlay during the transition. Defaults to true.

zIndexInOverlay: Float = 0.0f

The zIndex of the shared element within the overlay, enabling custom z-ordering for multiple shared elements.

clipInOverlayDuringTransition: SharedTransitionScope.OverlayClip = ParentClip

The clipping path of the shared element in the overlay. By default, it uses the resolved clip path from its parent sharedBounds (if applicable).

skipToLookaheadPosition

open fun Modifier.skipToLookaheadPosition(
    isEnabled: () -> Boolean = { isTransitionActive }
): Modifier

A modifier that anchors a layout at the target position obtained from the lookahead pass during shared element transitions.

This modifier ensures that a layout maintains its target position determined by the lookahead layout pass, preventing it from being influenced by layout changes in the tree during shared element transitions. This is particularly useful for preventing unwanted movement or repositioning of elements during animated transitions.

Important: skipToLookaheadPosition anchors the layout at the lookahead position relative to the SharedTransitionLayout. It does NOT necessarily anchor the layout within the window. More specifically, if a SharedTransitionLayout re-positions itself, any child layout using skipToLookaheadPosition will move along with it. If it is desirable to anchor a layout relative to a window, it's recommended to set up SharedTransitionLayout in a way that it does not change position in the window.

Note: skipToLookaheadPosition by default is only enabled via isEnabled lambda during a shared transition. It is recommended to enable it only when necessary. When active, it counteracts its ancestor layout's movement, which can incur extra placement pass costs if the parent layout frequently moves (e.g., during scrolling or animation).

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.Absolute.SpaceEvenly
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.sp

// In this sample, we are creating an animating clip bounds using shared element transition.
// In the meantime, we need to anchor the content that is being clipped at its target position
// using skipToLookaheadPosition while the clip bounds moves and resizes via animations.
var target by remember { mutableStateOf(true) }
BackHandler { target = !target }
// Creates a SharedTransitionLayout to provide its child content with a SharedTransitionScope.
// The child content can therefore set up shared element transitions, and access
// skipToLookaheadPosition modifier, as well as other functionalities available in
// SharedTransitionScope.
SharedTransitionLayout {
    AnimatedContent(targetState = target) {
        if (it) {
            Box(Modifier.fillMaxSize()) {
                Button(
                    modifier =
                        Modifier.align(Alignment.BottomCenter)
                            .sharedBounds(
                                rememberSharedContentState("clip"),
                                this@AnimatedContent,
                            ),
                    onClick = { target = false },
                ) {
                    Text("Toggle State")
                }
            }
        } else {
            Column(
                // Use sharedBounds chained with clipToBounds to animate the clip bounds
                // from the previous size and position derived from the shared bounds above
                // (when target == true).
                Modifier.sharedBounds(
                        rememberSharedContentState("clip"),
                        this@AnimatedContent,
                        resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
                    )
                    .clipToBounds()
                    // The sharedBounds above would resize its child layout and move it as
                    // needed. Here we use `skipToLookaheadSize` chained with
                    // `skipToLookaheadPosition` to keep the child content from being resized
                    // or moved. As such, only the clip bounds is being animated, creating
                    // a reveal animation.
                    .skipToLookaheadSize()
                    .skipToLookaheadPosition()
                    .fillMaxSize()
                    .background(Color.Black),
                verticalArrangement = Arrangement.SpaceEvenly,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Text("Hello", fontSize = 80.sp, color = Color.White)
                Text("Shared", fontSize = 80.sp, color = Color.White)
                Text("Clip", fontSize = 80.sp, color = Color.White)
                Text("Bounds", fontSize = 80.sp, color = Color.White)
            }
        }
    }
}
Parameters
isEnabled: () -> Boolean = { isTransitionActive }

A lambda that determines when the modifier should be active. Defaults to { isTransitionActive }, which enables the modifier only during active shared element transitions

skipToLookaheadSize

fun Modifier.skipToLookaheadSize(): Modifier

skipToLookaheadSize enables a layout to measure its child with the lookahead constraints, therefore laying out the child as if the transition has finished. This is particularly helpful for layouts where re-flowing content based on animated constraints is undesirable, such as texts.

In the sample below, try remove the skipToLookaheadSize modifier and observe the difference:

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeightIn
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

// Nested shared bounds sample.
val selectionColor = Color(0xff3367ba)
var expanded by remember { mutableStateOf(true) }
SharedTransitionLayout(
    Modifier.fillMaxSize().clickable { expanded = !expanded }.background(Color(0x88000000))
) {
    AnimatedVisibility(
        visible = expanded,
        enter = EnterTransition.None,
        exit = ExitTransition.None,
    ) {
        Box(modifier = Modifier.fillMaxSize()) {
            Surface(
                Modifier.align(Alignment.BottomCenter)
                    .padding(20.dp)
                    .sharedBounds(
                        rememberSharedContentState(key = "container"),
                        this@AnimatedVisibility,
                    )
                    .requiredHeightIn(max = 60.dp),
                shape = RoundedCornerShape(50),
            ) {
                Row(
                    Modifier.padding(10.dp)
                        // By using Modifier.skipToLookaheadSize(), we are telling the layout
                        // system to layout the children of this node as if the animations had
                        // all finished. This avoid re-laying out the Row with animated width,
                        // which is _sometimes_ desirable. Try removing this modifier and
                        // observe the effect.
                        .skipToLookaheadSize()
                ) {
                    Icon(
                        Icons.Outlined.Share,
                        contentDescription = "Share",
                        modifier =
                            Modifier.padding(
                                top = 10.dp,
                                bottom = 10.dp,
                                start = 10.dp,
                                end = 20.dp,
                            ),
                    )
                    Icon(
                        Icons.Outlined.Favorite,
                        contentDescription = "Favorite",
                        modifier =
                            Modifier.padding(
                                top = 10.dp,
                                bottom = 10.dp,
                                start = 10.dp,
                                end = 20.dp,
                            ),
                    )
                    Icon(
                        Icons.Outlined.Create,
                        contentDescription = "Create",
                        tint = Color.White,
                        modifier =
                            Modifier.sharedBounds(
                                    rememberSharedContentState(key = "icon_background"),
                                    this@AnimatedVisibility,
                                )
                                .background(selectionColor, RoundedCornerShape(50))
                                .padding(
                                    top = 10.dp,
                                    bottom = 10.dp,
                                    start = 20.dp,
                                    end = 20.dp,
                                )
                                .sharedElement(
                                    rememberSharedContentState(key = "icon"),
                                    this@AnimatedVisibility,
                                ),
                    )
                }
            }
        }
    }
    AnimatedVisibility(
        visible = !expanded,
        enter = EnterTransition.None,
        exit = ExitTransition.None,
    ) {
        Box(modifier = Modifier.fillMaxSize()) {
            Surface(
                Modifier.align(Alignment.BottomEnd)
                    .padding(30.dp)
                    .sharedBounds(
                        rememberSharedContentState(key = "container"),
                        this@AnimatedVisibility,
                        enter = EnterTransition.None,
                    )
                    .sharedBounds(
                        rememberSharedContentState(key = "icon_background"),
                        this@AnimatedVisibility,
                        resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
                        enter = EnterTransition.None,
                        exit = ExitTransition.None,
                    ),
                shape = RoundedCornerShape(30.dp),
                color = selectionColor,
            ) {
                Icon(
                    Icons.Outlined.Create,
                    contentDescription = "Create",
                    tint = Color.White,
                    modifier =
                        Modifier.padding(30.dp)
                            .size(40.dp)
                            .sharedElement(
                                rememberSharedContentState(key = "icon"),
                                this@AnimatedVisibility,
                            ),
                )
            }
        }
    }
}

Public properties

isTransitionActive

val isTransitionActiveBoolean

Indicates whether there is any ongoing transition between matched sharedElement or sharedBounds.