自定义共享元素过渡

要自定义共享元素过渡动画的运行方式,可以使用一些参数来更改共享元素的过渡方式。

动画规范

要更改用于大小和位置移动的动画规范,可以在 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

跳到最终布局

默认情况下,在两个布局之间过渡时,布局大小会在其起始状态和最终状态之间进行动画处理。这在为文本等内容制作动画时可能是不可取的行为。

以下示例演示了说明文本“Lorem Ipsum”以两种不同的方式进入屏幕。在第一个示例中,文本在容器大小增长时会重新流向,第二个示例中,文本不会在增长时重新流向。添加 Modifier.skipToLookaheadSize() 可防止其在增长时重新流向。

没有 Modifier.skipToLookahead() - 请注意“Lorem Ipsum”文本的重新流向

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

裁剪和覆盖

在 Compose 中创建共享元素时,一个重要的概念是,为了让它们在不同的可组合项之间共享,可组合项的呈现会被提升到过渡开始时其在目标中的匹配项的图层覆盖中。这种效果是,它将从父级的边界及其图层转换(例如 alpha 和缩放)中逃逸出来。

它将在其他非共享 UI 元素之上呈现,一旦过渡完成,元素将从覆盖层中删除到其自己的 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() 的裁剪路径。

要支持在共享元素过渡期间始终将特定 UI 元素(例如底部栏或浮动操作按钮)保留在最上面,请使用 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

(请注意列表中的其他项目如何响应一个项目增长而向下移动)