@Composable
fun DynamicallyEnabledSharedElementInPagerSample() {
// 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,
object : SharedTransitionScope.SharedContentConfig {
override val SharedContentState.isEnabled: Boolean
get() {
// 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 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.
lookaheadScopeCoordinates(nonNullCoordinates)
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.
return 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 }
)
}
}
}
}
SharedContentConfigSample
@Composable
fun SharedContentConfigSample() {
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
}
}
}
}