自定义共享元素过渡

如需自定义共享元素转换动画的运行方式,您可以使用一些参数来更改共享元素的转换方式。

动画规范

如需更改用于尺寸和位置移动的动画规范,您可以在 Modifier.sharedElement() 上指定其他 boundsTransform 参数。这会提供初始 Rect 位置和目标 Rect 位置。

例如,如需使上例中的文字以弧线运动,请指定 boundsTransform 参数以使用 keyframes 规范:

val textBoundsTransform = BoundsTransform { initialBounds, targetBounds ->
    keyframes {
        durationMillis = boundsAnimationDurationMillis
        initialBounds at 0 using ArcMode.ArcBelow using FastOutSlowInEasing
        targetBounds at boundsAnimationDurationMillis
    }
}
Text(
    "Cupcake", fontSize = 28.sp,
    modifier = Modifier.sharedBounds(
        rememberSharedContentState(key = "title"),
        animatedVisibilityScope = animatedVisibilityScope,
        boundsTransform = textBoundsTransform
    )
)

您可以使用任何 AnimationSpec。此示例使用 keyframes 规范。

图 1. 显示不同 boundsTransform 参数的示例

调整大小模式

在两个共享边界之间进行动画处理时,您可以将 resizeMode 参数设置为 RemeasureToBoundsScaleToBounds。此参数用于确定共享元素在两种状态之间的过渡方式。ScaleToBounds 首先使用预先查看(或目标)约束条件测量子布局。然后,子布局的稳定布局会缩放以适应共享边界。 ScaleToBounds 可以视为状态之间的“图形比例”。

相比之下,RemeasureToBounds 会根据目标大小,使用动画固定约束条件重新测量和重新布局 sharedBounds 的子布局。重新衡量由边界大小变化触发,这可能发生在每一帧。

对于 Text 可组合项,建议使用 ScaleToBounds,因为它可以避免重新布局和将文本重新排版到不同的行。建议在边界具有不同宽高比的情况下使用 RemeasureToBounds,如果您希望两个共享元素之间实现流畅的连续性,也可以使用此属性。

以下示例展示了这两种调整大小模式之间的区别:

ScaleToBounds

RemeasureToBounds

动态启用和停用共享元素

默认情况下,sharedElement()sharedBounds() 配置为在目标状态中找到匹配的键时,为布局更改添加动画效果。不过,您可能需要根据特定条件(例如导航方向或当前界面状态)动态停用此动画。

如需控制是否发生共享元素过渡,您可以自定义传递给 rememberSharedContentState()SharedContentConfigisEnabled 属性用于确定共享元素是否处于活跃状态。

以下示例演示了如何定义一种配置,该配置仅在特定屏幕之间导航时启用共享过渡(例如,仅从 A 到 B),而对其他屏幕禁用共享过渡。

SharedTransitionLayout {
    val transition = updateTransition(currentState)
    transition.AnimatedContent { targetState ->
        // Create the configuration that depends on state changing.
        fun animationConfig() : SharedTransitionScope.SharedContentConfig {
            return object : SharedTransitionScope.SharedContentConfig {
                override val SharedTransitionScope.SharedContentState.isEnabled: Boolean
                    // For this example, we only enable the transition in one direction
                    // from A -> B and not the other way around.
                    get() =
                        transition.currentState == "A" && transition.targetState == "B"
            }
        }
        when (targetState) {
            "A" -> Box(
                modifier = Modifier
                    .sharedElement(
                        rememberSharedContentState(
                            key = "shared_box",
                            config = animationConfig()
                        ),
                        animatedVisibilityScope = this
                    )
                    // ...
            ) {
                // Your content
            }
            "B" -> {
                Box(
                    modifier = Modifier
                        .sharedElement(
                            rememberSharedContentState(
                                key = "shared_box",
                                config = animationConfig()
                            ),
                            animatedVisibilityScope = this
                        )
                        // ...
                ) {
                    // Your content
                }
            }
        }
    }
}

默认情况下,如果共享元素在动画进行期间被停用,它仍会完成当前正在进行的动画,以防止意外移除正在进行的动画。如果您需要在动画进行时移除元素,可以在 SharedContentConfig 接口中替换 shouldKeepEnabledForOngoingAnimation 以返回 false。

跳至最终布局

默认情况下,在两个布局之间过渡时,布局大小会在其初始状态和最终状态之间添加动画效果。在为文本等内容添加动画效果时,这种行为可能并不理想。

以下示例展示了说明文字“Lorem Ipsum”以两种不同的方式进入屏幕。在第一个示例中,文本在进入时会随着容器尺寸的增大而重排。在第二个示例中,文本不会随着其增大而重新排版。添加 Modifier.skipToLookaheadSize() 可防止在增长时重新排布。

Modifier.skipToLookaheadSize() - 注意到“Lorem Ipsum”文本重新排版

Modifier.skipToLookaheadSize() - 请注意,“Lorem Ipsum”文本在动画开始时保持其最终状态

片段和叠加层

为了使共享元素能够在不同的可组合项之间共享,当过渡开始时,可组合项的渲染会提升到图层叠加层,以匹配目标中的相应元素。这样做的效果是,它会超出父级的边界及其图层转换(例如,透明度和缩放)。

它将渲染在其他非共享界面元素的顶部。过渡完成后,元素将从叠加层放置到其自己的 DrawScope 中。

如需将共享元素剪裁为某种形状,请使用标准 Modifier.clip() 函数。将其置于 sharedElement() 之后:

Image(
    painter = painterResource(id = R.drawable.cupcake),
    contentDescription = "Cupcake",
    modifier = Modifier
        .size(100.dp)
        .sharedElement(
            rememberSharedContentState(key = "image"),
            animatedVisibilityScope = this@AnimatedContent
        )
        .clip(RoundedCornerShape(16.dp)),
    contentScale = ContentScale.Crop
)

如果您需要确保共享元素永远不会在父容器之外呈现,可以在 sharedElement() 上设置 clipInOverlayDuringTransition。默认情况下,对于嵌套的共享边界,clipInOverlayDuringTransition 使用来自父级 sharedBounds() 的剪切路径。

如需支持在共享元素过渡期间始终将特定界面元素(例如底部栏或悬浮操作按钮)保持在最前面,请使用 Modifier.renderInSharedTransitionScopeOverlay()。默认情况下,此修饰符会在共享过渡处于活跃状态期间将内容保留在叠加层中。

例如,在 Jetsnack 中,BottomAppBar 需要放置在共享元素的顶部,直到屏幕不可见为止。将修饰符添加到可组合项上可使其保持高程。

未设置 Modifier.renderInSharedTransitionScopeOverlay()

通过 Modifier.renderInSharedTransitionScopeOverlay()

您可能希望非共享的可组合项在过渡之前也能够动画式消失,并保持在其他可组合项的顶部。在这种情况下,请使用 renderInSharedTransitionScopeOverlay().animateEnterExit() 在共享元素转换运行时为可组合项添加退出动画:

JetsnackBottomBar(
    modifier = Modifier
        .renderInSharedTransitionScopeOverlay(
            zIndexInOverlay = 1f,
        )
        .animateEnterExit(
            enter = fadeIn() + slideInVertically {
                it
            },
            exit = fadeOut() + slideOutVertically {
                it
            }
        )
)

图 2. 底部应用栏在动画过渡时滑入和滑出。

在极少数情况下,如果您不希望共享元素在叠加层中呈现,可以将 sharedElement()renderInOverlayDuringTransition 设置为 false。

将共享元素大小的更改通知给同级布局

默认情况下,sharedBounds()sharedElement() 在布局过渡时不会将任何大小变化通知给父容器。

为了在过渡时将大小变化传播到父容器,请将 placeHolderSize 参数更改为 PlaceHolderSize.animatedSize。这样做会导致相应项增大或缩小。布局中的所有其他项都会响应此更改。

PlaceholderSize.contentSize(默认)

PlaceholderSize.animatedSize

(请注意,列表中的其他项如何因其中一项变大而向下移动)