SharedTransitionScope.SharedContentConfig


SharedContentConfig allows a shared element to be disabled or enabled dynamically through isEnabled property. By default, shouldKeepEnabledForOngoingAnimation is true. This means if the shared element transition is already running for the layout that this SharedContentConfig is applied to, we will keep the shared element enabled until the animation is finished. In other words, disabling shared element while the animation is in-flight will have no effect, unless shouldKeepEnabledForOngoingAnimation is overridden.

alternativeTargetBoundsInTransitionScopeAfterRemoval defines an alternative target bounds for when the target shared element is disposed amid animation (e.g., scrolled out of the viewport and subsequently disposed). By default, no alternative target bounds is defined - As soon as the target shared element (i.e. the shared element in the incoming/target content) is removed, the shared element transition for the shared elements with the same key will be cancelled.

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.SharedTransitionScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.sp

val customConfig = remember {
    // Creates a custom SharedContentConfig to configure the alternative target
    // bounds in the case of the target shared element being disposed amid
    // shared element transition.
    object : SharedTransitionScope.SharedContentConfig {
        override fun SharedTransitionScope.SharedContentState
            .alternativeTargetBoundsInTransitionScopeAfterRemoval(
            targetBoundsBeforeRemoval: Rect,
            sharedTransitionLayoutSize: Size,
        ): Rect? {

            // If the bottom edge of the target shared element is below the
            // viewport, we move the target bounds to 300 pixels below the
            // viewport in this example, while keeping the same left position,
            // and target size.
            if (targetBoundsBeforeRemoval.bottom >= sharedTransitionLayoutSize.height) {
                return Rect(
                    Offset(
                        targetBoundsBeforeRemoval.left,
                        sharedTransitionLayoutSize.height + 300f,
                    ),
                    targetBoundsBeforeRemoval.size,
                )
            }

            // If the top edge of the target shared element is above the
            // viewport before it is disposed, we will move the target bounds
            // to 300 pixels above the viewport in this example, while keeping
            // the same left position and target size.
            if (targetBoundsBeforeRemoval.top < 0) {

                return Rect(
                    Offset(
                        targetBoundsBeforeRemoval.left,
                        -300 - targetBoundsBeforeRemoval.height,
                    ),
                    targetBoundsBeforeRemoval.size,
                )
            }

            // If the target bounds were well within the range of the viewport
            // height, we will use the last seen target bounds as the new
            // target bounds. Note: The default alternative bounds is null,
            // meaning the animation will be stopped if the target shared
            // element is removed.
            return targetBoundsBeforeRemoval
        }
    }
}

Summary

Public functions

open Rect?
SharedTransitionScope.SharedContentState.alternativeTargetBoundsInTransitionScopeAfterRemoval(
    targetBoundsBeforeRemoval: Rect,
    sharedTransitionLayoutSize: Size
)

alternativeTargetBoundsInTransitionScopeAfterRemoval returns an alternative target bounds for when the target shared element is disposed amid animation (e.g., scrolled out of the viewport and subsequently disposed).

Cmn

Public properties

open Boolean

isEnabled returns a boolean indicating whether the shared element is enabled.

Cmn
open Boolean

shouldKeepEnabledForOngoingAnimation returns a boolean indicating whether the shared element should be enabled for ongoing animation.

Cmn

Public functions

alternativeTargetBoundsInTransitionScopeAfterRemoval

open fun SharedTransitionScope.SharedContentState.alternativeTargetBoundsInTransitionScopeAfterRemoval(
    targetBoundsBeforeRemoval: Rect,
    sharedTransitionLayoutSize: Size
): Rect?

alternativeTargetBoundsInTransitionScopeAfterRemoval returns an alternative target bounds for when the target shared element is disposed amid animation (e.g., scrolled out of the viewport and subsequently disposed).

By default, no alternative target bounds is defined - As soon as the target shared element (i.e. the shared element in the incoming/target content) is removed, the shared element transition for the shared elements with the same key will be cancelled.

import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Share
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.sp

val customConfig = remember {
    // Creates a custom SharedContentConfig to configure the alternative target
    // bounds in the case of the target shared element being disposed amid
    // shared element transition.
    object : SharedTransitionScope.SharedContentConfig {
        override fun SharedTransitionScope.SharedContentState
            .alternativeTargetBoundsInTransitionScopeAfterRemoval(
            targetBoundsBeforeRemoval: Rect,
            sharedTransitionLayoutSize: Size,
        ): Rect? {

            // If the bottom edge of the target shared element is below the
            // viewport, we move the target bounds to 300 pixels below the
            // viewport in this example, while keeping the same left position,
            // and target size.
            if (targetBoundsBeforeRemoval.bottom >= sharedTransitionLayoutSize.height) {
                return Rect(
                    Offset(
                        targetBoundsBeforeRemoval.left,
                        sharedTransitionLayoutSize.height + 300f,
                    ),
                    targetBoundsBeforeRemoval.size,
                )
            }

            // If the top edge of the target shared element is above the
            // viewport before it is disposed, we will move the target bounds
            // to 300 pixels above the viewport in this example, while keeping
            // the same left position and target size.
            if (targetBoundsBeforeRemoval.top < 0) {

                return Rect(
                    Offset(
                        targetBoundsBeforeRemoval.left,
                        -300 - targetBoundsBeforeRemoval.height,
                    ),
                    targetBoundsBeforeRemoval.size,
                )
            }

            // If the target bounds were well within the range of the viewport
            // height, we will use the last seen target bounds as the new
            // target bounds. Note: The default alternative bounds is null,
            // meaning the animation will be stopped if the target shared
            // element is removed.
            return targetBoundsBeforeRemoval
        }
    }
}
Parameters
targetBoundsBeforeRemoval: Rect

The target bounds of the shared element relative to the SharedTransitionLayout before it is removed.

sharedTransitionLayoutSize: Size

The size of the shared transition layout for convenient calculation.

Public properties

open val SharedTransitionScope.SharedContentState.isEnabledBoolean

isEnabled returns a boolean indicating whether the shared element is enabled. By default, it is true.

shouldKeepEnabledForOngoingAnimation

open val shouldKeepEnabledForOngoingAnimationBoolean

shouldKeepEnabledForOngoingAnimation returns a boolean indicating whether the shared element should be enabled for ongoing animation. By default, shared elements will be kept enabled for ongoing animation until the animation is finished. This means disabling shared element while the animation is in-flight will have no effect, unless shouldKeepEnabledForOngoingAnimation is overridden to return false. This default is intended to ensure a continuous experience out-of-the-box by avoiding accidentally removing in-flight animations.