SharedTransitionScope.SharedContentState


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.

Summary

Public functions

Unit

prepareTransitionWithInitialVelocity sets up the initial velocity for the upcoming shared element transition.

Cmn

Public properties

Path?

The resolved clip path in overlay based on the OverlayClip defined for the shared content.

Cmn
Boolean

Indicates whether a match of the same key has been found.

Cmn
Any
Cmn
SharedTransitionScope.SharedContentState?

Returns the SharedContentState of a parent sharedBounds, if any.

Cmn

Public functions

prepareTransitionWithInitialVelocity

fun prepareTransitionWithInitialVelocity(initialVelocity: Velocity): Unit

prepareTransitionWithInitialVelocity sets up the initial velocity for the upcoming shared element transition. This function should be called during the gesture handling to allow the system to acquire the animation start time in the next (i.e. earliest) animation frame to ensure a smooth velocity handoff.

The velocity will used for both incoming and outgoing shared content defined with the same key once shared element transition starts. The animationSpec used to animate the momentum will be the same as what is used by the incoming shared element's bounds transform to keep consistent motion. If the incoming shared element uses a duration-based animation, a default spring will be used instead.

Note: This function will have no effect if the shared element is not enabled (i.e. SharedContentConfig.isEnabled is false). This velocity will only apply to the next shared element transition.

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.SharedTransitionScope.SharedContentState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.draggable2D
import androidx.compose.foundation.gestures.rememberDraggable2DState
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.outlined.Create
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.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
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.round
import androidx.compose.ui.unit.sp

@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 showDetails 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(remember { MutableInteractionSource() }, indication = null) {
            showDetails = !showDetails
        }
        .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 = showDetails,
        transitionSpec = { fadeIn() togetherWith fadeOut() using null },
    ) { isDetails ->
        if (isDetails) {
            var offset by remember(transition.currentState) { mutableStateOf(Offset.Zero) }
            // Create a mutable state to hold the sharedContentState, so that we can invoke
            // prepareTransitionWithInitialVelocity on it in order to set up the initial
            // velocity in drag handler.
            var sharedContentStateForDraggableCat: SharedContentState? by remember {
                mutableStateOf(null)
            }
            Column(
                Modifier.fillMaxSize()
                    .draggable2D(
                        rememberDraggable2DState { offset += it },
                        onDragStopped = { velocity ->
                            // Set up the initial velocity for the upcoming shared element
                            // transition.
                            sharedContentStateForDraggableCat
                                ?.prepareTransitionWithInitialVelocity(velocity)
                            showDetails = false
                        },
                    )
            ) {
                Cat(
                    Modifier.fillMaxWidth()
                        .offset { offset.round() }
                        .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(
                            // Using the AnimatedVisibilityScope from the AnimatedContent
                            // defined above.
                            rememberSharedContentState("cat").also {
                                sharedContentStateForDraggableCat = it
                            },
                            this@AnimatedContent,
                        )
                )
            }
        } else {
            val colors = listOf(Color(0xff2a9d84), Color(0xffffcc5c), Color(0xffff6f69))
            FlowRow(modifier = Modifier.fillMaxWidth()) {
                repeat(7) {
                    if (it == 3) {
                        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("cat"),
                                    this@AnimatedContent,
                                )
                        )
                    } else {
                        Box(
                            Modifier.size(100.dp)
                                .background(colors[it % 3], RoundedCornerShape(5.dp))
                        )
                    }
                }
            }
        }
    }
}
Parameters
initialVelocity: Velocity

The velocity from the gesture system (e.g., from onDragEnd).

Public properties

clipPathInOverlay

val clipPathInOverlayPath?

The resolved clip path in overlay based on the OverlayClip defined for the shared content. clipPathInOverlay is set during Draw phase, before children are drawn. This means it is safe to query parentSharedContentState's clipPathInOverlay when the shared content is drawn.

isMatchFound

val isMatchFoundBoolean

Indicates whether a match of the same key has been found. sharedElement or sharedBounds will not have any animation unless a match has been found.

Caveat: isMatchFound is only set to true after a new sharedElement/sharedBounds of the same key has been composed. If the new sharedBounds/sharedElement is declared in subcomposition (e.g. a LazyList) where the composition happens as a part of the measure/layout pass, that's when isMatchFound will become true.

key

val keyAny

parentSharedContentState

val parentSharedContentStateSharedTransitionScope.SharedContentState?

Returns the SharedContentState of a parent sharedBounds, if any.