我目前正在使用 Jetpack Compose 中的共享元素转换,并遇到了问题。
问题: 当共享元素转换发生时,共享元素内的运行动画将被重置。
重现代码:
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun SharedTransition(modifier: Modifier = Modifier) {
var inLeft by remember { mutableStateOf(true) }
SharedTransitionLayout(modifier) {
AnimatedContent(
modifier = Modifier.clickable { inLeft = !inLeft },
targetState = inLeft
) { targetState ->
if (targetState) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier.fillMaxWidth()
) {
AnimatedBox(
sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@AnimatedContent,
)
}
} else {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.fillMaxWidth()
) {
AnimatedBox(
sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@AnimatedContent,
)
}
}
}
}
}
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun AnimatedBox(
sharedTransitionScope: SharedTransitionScope,
animatedContentScope: AnimatedContentScope,
modifier: Modifier = Modifier
) {
with(sharedTransitionScope) {
val transition = rememberInfiniteTransition()
val color by transition.animateColor(
initialValue = Color.Green,
targetValue = Color.Black,
animationSpec = infiniteRepeatable(
tween(durationMillis = 1500, easing = LinearEasing),
RepeatMode.Reverse,
),
)
Box(
modifier
.size(BoxSize)
.sharedElement(
rememberSharedContentState(key = "animated_box"),
animatedContentScope,
)
.background(color)
)
}
}
private val BoxSize = 48.dp
这会导致:
动画应在绿色和黑色之间来回缓慢褪色。但是当通过点击触发过渡时,动画会跳转到最初的亮绿色。 当盒子位于右侧时,在视频中可以最好地看到这一点。
我希望动画不会因转换而重新启动。
我尝试将动画元素包装在
movableContentOf { }
中,但结果是相同的:
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun MovableContentSharedTransition(modifier: Modifier = Modifier) {
val animatedBox =
remember {
movableContentWithReceiverOf<SharedTransitionScope, AnimatedContentScope> { animatedContentScope ->
val transition = rememberInfiniteTransition()
val color by transition.animateColor(
initialValue = Color.Magenta,
targetValue = Color.Black,
animationSpec = infiniteRepeatable(
tween(durationMillis = 1500, easing = LinearEasing),
RepeatMode.Reverse,
),
)
Box(
Modifier
.size(48.dp)
.sharedElement(
rememberSharedContentState(key = "animated_box"),
animatedContentScope,
)
.background(color)
)
}
}
var inLeft by remember { mutableStateOf(true) }
SharedTransitionLayout {
AnimatedContent(
modifier = modifier.clickable { inLeft = !inLeft },
targetState = inLeft
) { targetState ->
if (targetState) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier.fillMaxWidth()
) {
animatedBox(this@SharedTransitionLayout, this@AnimatedContent)
}
} else {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.fillMaxWidth()
) {
animatedBox(this@SharedTransitionLayout, this@AnimatedContent)
}
}
}
}
}
撰写BOM版本:
"androidx.compose:compose-bom:2024.09.02"
。
有没有办法让动画继续运行?这可以用
moveableContentOf { }
解决吗?
完整代码:https://github.com/aruh/shared-element-transition-animation-reset-sample
这是因为
AnimatedContent
的工作原理。它显示状态之间的可组合项,并在转换结束时从组合中删除一个可组合项。
您正在向合成添加新的
AnimatedBox
可组合项,因此每次 InfiniteTransition
进入合成时都会生成新的 AnimatedBox
和颜色。
如果您希望颜色持续存在,只需将其移出 AnimatedBox 并通过 InfiniteTransition 将其作为参数从父级传递。
@Preview
@Composable
fun SharedTransition() {
var inLeft by remember { mutableStateOf(true) }
val transition: InfiniteTransition = rememberInfiniteTransition()
val color by transition.animateColor(
initialValue = Color.Green,
targetValue = Color.Black,
animationSpec = infiniteRepeatable(
tween(durationMillis = 1500, easing = LinearEasing),
RepeatMode.Reverse,
),
)
SharedTransitionLayout(Modifier) {
AnimatedContent(
modifier = Modifier.clickable { inLeft = !inLeft },
targetState = inLeft
) { targetState ->
if (targetState) {
Box(
contentAlignment = Alignment.CenterStart,
modifier = Modifier.fillMaxWidth()
) {
AnimatedBox(
sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@AnimatedContent,
color = color
)
}
} else {
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.fillMaxWidth()
) {
AnimatedBox(
sharedTransitionScope = this@SharedTransitionLayout,
animatedContentScope = this@AnimatedContent,
color = color
)
}
}
}
}
}
源代码
// TODO: remove screen as soon as they are animated out
val currentlyVisible = remember(this) { mutableStateListOf(currentState) }
val contentMap = remember(this) { mutableScatterMapOf<S, @Composable() () -> Unit>() }
// This is needed for tooling because it could change currentState directly,
// as opposed to changing target only. When that happens we need to clear all the
// visible content and only display the content for the new current state and target state.
if (!currentlyVisible.contains(currentState)) {
currentlyVisible.clear()
currentlyVisible.add(currentState)
}
if (currentState == targetState) {
if (currentlyVisible.size != 1 || currentlyVisible[0] != currentState) {
currentlyVisible.clear()
currentlyVisible.add(currentState)
}
if (contentMap.size != 1 || contentMap.containsKey(currentState)) {
contentMap.clear()
}
// TODO: Do we want to support changing contentAlignment amid animation?
rootScope.contentAlignment = contentAlignment
rootScope.layoutDirection = layoutDirection
}