SharedTransitionLayout

Composable Function

Common
@ExperimentalSharedTransitionApi
@Composable
public fun SharedTransitionLayout(
    modifier: Modifier = Modifier,
    content: @Composable SharedTransitionScope.() -> Unit,
)

SharedTransitionLayout creates a layout and a SharedTransitionScope for the child layouts in content. Any child (direct or indirect) of the SharedTransitionLayout can use the receiver scope SharedTransitionScope to create shared element or shared bounds transitions.

Note: SharedTransitionLayout creates a new Layout. For use cases where it's preferable to not introduce a new layout between content and the parent layout, consider using SharedTransitionScope instead.

Below is an example of using SharedTransitionLayout to create a shared element transition and a shared bounds transition at the same time. Please see the API docs for SharedTransitionScope.sharedElement and SharedTransitionScope.sharedBounds for more simplified examples of using these APIs separately.

Parameters

modifierModifiers to be applied to the layout.
contentThe children composable to be laid out.

Code Examples

SharedElementInAnimatedContentSample

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedElementInAnimatedContentSample() {
    // This is the Image that we will add shared element modifier on. It's important to make sure
    // modifiers that are not shared between the two shared elements (such as size modifiers if
    // the size changes) are the parents (i.e. on the left side) of Modifier.sharedElement.
    // Meanwhile, the modifiers that are shared between the shared elements (e.g. Modifier.clip
    // in this case) are on the right side of the Modifier.sharedElement.
    @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)),
        )
    }
    // Shared element key is of type `Any`, which means it can be id, string, etc. The only
    // requirement for the key is that it should be the same for shared elements that you intend
    // to match. Here we use the image resource id as the key.
    val sharedElementKey = R.drawable.yt_profile
    var showLargeImage by remember { mutableStateOf(true) }
    // First, we need to create a SharedTransitionLayout, this Layout will provide the coordinator
    // space for shared element position animation, as well as an overlay for shared elements to
    // render in. Children content in this Layout will be able to create shared element transition
    // using the receiver scope: SharedTransitionScope
    SharedTransitionLayout(
        Modifier.clickable { showLargeImage = !showLargeImage }.fillMaxSize().padding(10.dp)
    ) {
        // In the SharedTransitionLayout, we will be able to access the receiver scope (i.e.
        // SharedTransitionScope) in order to create shared element transition.
        AnimatedContent(targetState = showLargeImage) { showLargeImageMode ->
            if (showLargeImageMode) {
                Cat(
                    Modifier.fillMaxSize()
                        .aspectRatio(1f)
                        // Creating a shared element. Note that this modifier is *after*
                        // the size modifier and aspectRatio modifier, because those size specs
                        // are not shared between the two shared elements.
                        .sharedElement(
                            rememberSharedContentState(sharedElementKey),
                            // Using the AnimatedVisibilityScope from the AnimatedContent
                            // defined above.
                            this@AnimatedContent,
                        )
                )
                Text(
                    "Cute Cat YT",
                    fontSize = 40.sp,
                    color = Color.Blue,
                    // Prefer Modifier.sharedBounds for text, unless the texts in both initial
                    // content and target content are exactly the same (i.e. same
                    // size/font/color)
                    modifier =
                        Modifier.fillMaxWidth()
                            // IMPORTANT: Prefer using wrapContentWidth/wrapContentSize over
                            // textAlign
                            // for shared text transition. This allows the layout system sees actual
                            // position and size of the text to facilitate bounds animation.
                            .wrapContentWidth(Alignment.CenterHorizontally)
                            .sharedBounds(
                                rememberSharedContentState(key = "text"),
                                this@AnimatedContent,
                            ),
                )
            } else {
                Column {
                    Row(verticalAlignment = Alignment.CenterVertically) {
                        Cat(
                            Modifier.size(100.dp)
                                // Creating another shared element with the same key.
                                // Note that this modifier is *after* the size modifier,
                                // The size changes between these two shared elements, i.e. the size
                                // is not shared between the two shared elements.
                                .sharedElement(
                                    rememberSharedContentState(sharedElementKey),
                                    this@AnimatedContent,
                                )
                        )
                        Text(
                            "Cute Cat YT",
                            // Change text color & size
                            fontSize = 20.sp,
                            color = Color.DarkGray,
                            // Prefer Modifier.sharedBounds for text, unless the texts in both
                            // initial content and target content are exactly the same (i.e. same
                            // size/font/color)
                            modifier =
                                Modifier
                                    // The modifier that is not a part of the shared content, but
                                    // rather
                                    // for positioning and sizes should be on the *left* side of
                                    // sharedBounds/sharedElement.
                                    .padding(start = 20.dp)
                                    .sharedBounds(
                                        // Here we use a string-based key, in contrast to the key
                                        // above.
                                        rememberSharedContentState(key = "text"),
                                        this@AnimatedContent,
                                    ),
                        )
                    }
                    Box(
                        Modifier.fillMaxWidth()
                            .height(100.dp)
                            .background(Color(0xffffcc5c), RoundedCornerShape(5.dp))
                    )
                    Box(
                        Modifier.fillMaxWidth()
                            .height(100.dp)
                            .background(Color(0xff2a9d84), RoundedCornerShape(5.dp))
                    )
                }
            }
        }
    }
}