Compose Transition中断动画的特殊性

433 阅读2分钟

Compose动画中有一个Interrupted中断效果,当上一个动画还没有执行完成时,立刻触发下一个动画,控件会从当前状态往新的状态平滑过渡。

问题

先看下面动图,分别使用了AnimateAsState和Transition来触发动画,动画规格都是2秒的tween动画,当触发打断时,效果却是不一样的,Transition动画似乎突变了,和设计的平滑过渡不一致。 在这里插入图片描述

源码

  • offsetOne,offsetTwo和offsetThree都是用的是tween(durationMillis = 2000)
@Preview
@Composable
fun AnimationExample() {

    var boxRight by remember { mutableStateOf(false) }
    var iconIsPlay by remember { mutableStateOf(false) }

    val updateTransition = updateTransition(
        targetState = boxRight,
        label = "updateTransition"
    )
    val offsetOne = updateTransition.animateOffset(
        targetValueByState = { boxRight ->
            if (boxRight) {
                Offset(x = 350f, y = 0f)
            } else {
                Offset(x = 0f, y = 0f)
            }
        },
        transitionSpec = {
            tween(durationMillis = 2000)
//            spring(stiffness = 50f)
        }
    )
    LaunchedEffect(updateTransition.isRunning) {
        iconIsPlay = updateTransition.isRunning
    }

    val offsetTwo by animateOffsetAsState(
        targetValue = if (boxRight) {
            Offset(x = 350f, y = 0f)
        } else {
            Offset(x = 0f, y = 0f)
        },
        animationSpec = tween(durationMillis = 2000)
    )

    val transitionState = remember { MutableTransitionState(false) }
    val rememberOffset = rememberTransition(transitionState = transitionState)
    val offsetThree = rememberOffset.animateOffset(
        targetValueByState = { boxRight ->
            if (boxRight) {
                Offset(x = 350f, y = 0f)
            } else {
                Offset(x = 0f, y = 0f)
            }
        },
        transitionSpec = {
            tween(durationMillis = 2000)
//            spring(stiffness = 50f)
        }
    )

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "UpdateTransition")
        Box(
            modifier = Modifier
                .width(400.dp)
                .height(50.dp)
                .background(Color(0x405E5D5D))
        ) {
            Box(
                modifier = Modifier
                    .size(50.dp)
                    .offset(offsetOne.value.x.dp, offsetOne.value.y.dp)
                    .background(Color(0xFF2196F3))
            )
        }
        Spacer(modifier = Modifier.height(10.dp))
        Text(text = "RememberTransition")
        Box(
            modifier = Modifier
                .width(400.dp)
                .height(50.dp)
                .background(Color(0x405E5D5D))
        ) {
            Box(
                modifier = Modifier
                    .size(50.dp)
                    .offset(offsetThree.value.x.dp, offsetThree.value.y.dp)
                    .background(Color(0xFFCDDC39))
            )
        }
        Spacer(modifier = Modifier.height(10.dp))
        Text(text = "AnimateAsState")
        Box(
            modifier = Modifier
                .width(400.dp)
                .height(50.dp)
                .background(Color(0x405E5D5D))
        ) {
            Box(
                modifier = Modifier
                    .size(50.dp)
                    .offset(offsetTwo.x.dp, offsetTwo.y.dp)
                    .background(Color(0xFF4CAF50))
            )
        }
        Spacer(modifier = Modifier.height(10.dp))
        Image(
            imageVector = if (iconIsPlay) {
                Icons.Filled.PauseCircle
            } else {
                Icons.Filled.PlayCircle
            },
            contentDescription = null,
            modifier = Modifier
                .size(50.dp)
                .clickable {
                    boxRight = !boxRight
                    transitionState.targetState = boxRight
                }
        )
    }
}
  • 接下来将动画全部换成弹簧动画spring(stiffness = 50f),看看效果: 在这里插入图片描述 当换成spring动画后发现,Transition的打断动画和AnimateAsState一样了,打断后就不瞬间突变了。

根因

在Transition.kt中可以看到源码如下:

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

可以看到当动画规格是spring的时候,打断后的新动画保留animationSpec,否则使用默认的interruptionSpec。那么默认的interruptionSpec则是spring(visibilityThreshold = visibilityThreshold),默认的弹性刚度(弹性刚度越大,动画速度越快)是StiffnessMedium: Float = 1500f

        private val interruptionSpec: FiniteAnimationSpec<T>

        init {
            val visibilityThreshold: T? =
                VisibilityThresholdMap.get(typeConverter)?.let {
                    val vector = typeConverter.convertToVector(initialValue)
                    for (id in 0 until vector.size) {
                        vector[id] = it
                    }
                    typeConverter.convertFromVector(vector)
                }
            interruptionSpec = spring(visibilityThreshold = visibilityThreshold)
        }

总结:当transition设置的动画不是spring类型时,transition的中断动画并不是突变,而是默认的spring动画,刚度1500f,所以看起来速度非常快。