如果更改为新动画,UpdateTransition 动画会保持其 运行 速度吗?

Will UpdateTransition animation maintain its running velocity when if being changed to new animation?

我使用 AnimationAsStateAnimatableUpdateTransition 创建了相同的动画并进行了尝试。

所有动画都使用向前或向后移动。

    tween(
        durationMillis = 3000,
        easing = LinearOutSlowInEasing
    )

下面我们可以看到

如果动画执行到一半,我再次点击按钮,我会假设 运行 动画速度保持不变并传递给后续动画。

这似乎适用于 AnimateAsStateAnimatable。然而,令我惊讶的是, UpdateTransition 似乎没有那样做。我想它已经恢复到默认的动画持续时间(运行得非常快)。

我们可以看到如下。

我的问题是,是吗

  1. 预计如果 UpdateTransition 被后续动画中途取消,它不会保留 运行 动画规范?
  2. 我的代码遗漏了什么(我在下面分享了整个代码)?
  3. 这是一个 UpdateTransition 错误,需要报告给 Google?

上述动画的完整工作代码如下

import androidx.compose.animation.core.*
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

@Composable
fun Combination() {
    var enabled by remember { mutableStateOf(false) }

    val dbAnimateAsState: Dp by animateDpAsState(
        targetValue = switch(enabled),
        animationSpec = animationSpec()
    )

    val dbAnimatable = remember { Animatable(0.dp) }

    val transition = updateTransition(enabled, label = "")
    val dbTransition by transition.animateDp(
        transitionSpec = { animationSpec() }, label = "") {
        switch(it)
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {

        Text("AnimateAsState")
        animateBoxHorizontal(dbAnimateAsState)
        Text("Animatable")
        animateBoxHorizontal(dbAnimatable.value)
        Text("UpdateTransition")
        animateBoxHorizontal(dbTransition)

        Button(onClick = { enabled = !enabled }) {
            Text("Click Me")
        }
    }

    LaunchedEffect(key1 = enabled) {
        dbAnimatable.animateTo(
            targetValue = switch(enabled),
            animationSpec = animationSpec()
        )
    }
}

private fun animationSpec(): TweenSpec<Dp> =
    tween(
        durationMillis = 3000,
        easing = LinearOutSlowInEasing
    )

private fun switch(enabled: Boolean) = if (enabled) 268.dp else 0.dp

fun Animatable(initialValue: Dp) = Animatable(
    initialValue,
    DpToVector,
)

private val DpToVector: TwoWayConverter<Dp, AnimationVector1D> =
    TwoWayConverter({ AnimationVector1D(it.value) }, { it.value.dp })

@Composable
private fun animateBoxHorizontal(dbAnimateAsState: Dp) {
    Box(
        modifier = Modifier
            .height(32.dp)
            .width(300.dp)
            .background(Color.Yellow)
    ) {
        Box(
            modifier = Modifier
                .offset(dbAnimateAsState, 0.dp)
                .size(32.dp)
                .background(Color.Red)
        )
    }
    Spacer(modifier = Modifier.height(16.dp))
}

注意:更新信息

如果我从 tweenSpec 更改为 springSpec

    tween(
        durationMillis = 3000,
        easing = LinearOutSlowInEasing
    )

    spring(stiffness =20f, dampingRatio = 0.25f)

然后 updateTransition 照常工作,其中 Spring 速度和动画在我们用新的动画打断时保持并连续。

updateTransition 仍然保持速度——如果你能减慢它可能会更明显。它在中断时切换到 spring,而 animateAsStateAnimatable 使用您提供的 AnimationSpec。回退 spring 可能有点过于僵硬,因此它被认为是一个非常快速的变化。

此设计的目的是允许 Transition 在使用不同类型的 AnimationSpec 时适应中断。例如,从状态 A 到状态 B,您可能使用 keyFrames,从状态 B 到状态 C 或者可能使用 snap()(因为状态 B 和 C 中的值相同) .因此,当您以 C 作为新目的地从 A -> B 中断动画时,keyframessnap 都会看起来很奇怪。这就是为什么当 Transition 被打断时我们回退到 spring

请注意,如果您已在 Transition 中为动画使用 spring,则 spring 将用于处理该动画中的中断,因为它与您的动画更相关。这就是为什么当您向上面代码片段中的过渡提供 low-stiffness spring 时,您会看到动画的“路线修正”要慢得多。

我们确实计划支持中断处理的自定义,您可以在其中指定中断时使用的 AnimationSpec。计划将其作为未来的工作。如果您对如何处理中断有任何具体要求,或者回退 spring 应该慢一点,请随时提交功能请求。 :)

经过更多调查,显然,这是设计使然。

如果我们检查 Google 转换 class 代码 class Transition<S> @PublishedApi internal constructor

我们可以看到

 val spec = if (isInterrupted) {
                // When interrupted, use the default spring, unless the spec is also a spring.
                if (animationSpec is SpringSpec<*>) animationSpec else interruptionSpec
            } else {
                animationSpec
            }

interruptionSpec = spring(visibilityThreshold = visibilityThreshold)

因此 interruptionSpec 设置为 Spring,如果用户提供 Spring,它会被保留。如果不是,它将被设置为 spring(visibilityThreshold = visibilityThreshold).