深入理解 Transition:更强大的动画管理

214 阅读7分钟

作用

在 Jetpack Compose 中,Transition 是用于Compose内部Composable组件自身更复杂、多属性的的状态转场动画的。

关于转场动画,其实就是状态切换/转移的动画。

从 animate*AsState() 到 Transition

我们之前是使用 animate*AsState() 来完成状态转移动画的。

比如:每次点击绿色矩形都会以动画的形式修改它的圆角大小。

@Composable
fun StatusTransferDemo() {
    var round by remember { mutableStateOf(false) }
    // 根据 round 状态的变化,平滑地过渡圆角大小
    val roundCornerSize by animateDpAsState(targetValue = if (round) 18.dp else 0.dp)

    Box(
        Modifier
            .padding(12.dp)
            .size(48.dp)
            .clip(RoundedCornerShape(size = roundCornerSize))
            .clickable {
                round = !round
            }
            .background(Color.Green)
    )
}
圆角动画效果

这样固然简单、直观,但是如果我们要同时对多个属性进行动画,并且这些动画都依赖于同一个状态呢?


再来看看如果用 Transition 该怎么实现?只需要这样:

@Composable
fun StatusTransferDemo() {
    var round by remember { mutableStateOf(false) } // 实际的状态

    // 1、创建并更新Transition对象,表示追踪的状态
    val roundTransition = updateTransition(targetState = round)

    // 2、根据状态定义动画
    val roundedCornerSize by roundTransition.animateDp { targetState -> // 目标状态,即将切换到的状态
        // 根据目标状态的不同,得到动画的目标值
        if (targetState) {
            18.dp
        } else {
            0.dp
        }
    }
    
    // 3、使用动画值
    Box(
        Modifier
            .padding(12.dp)
            .size(48.dp)
            .clip(RoundedCornerShape(size = roundedCornerSize))
            .clickable {
                round = !round
            }
            .background(Color.Green)
    )
}

其中 updateTransition 函数的作用是:

  1. 初始化(首次组合)时创建 Transition 对象,这个对象会在后续重组过程中存在,因为函数内部使用了 remember() 函数。
  2. 在后续重组过程中,每当 targetState 值(当前是 round)发生改变时,都会更新 Transition 对象内部的目标状态为 targetState 值。

并且这个状态更新的过程是渐变的

什么意思呢?

Transition 对象只是用于管理状态的,当 targetState 目标状态改变时,它不会让相关的动画属性“跳”到目标值。而是配合上 animate* 函数(如animateDp),在动画的每一帧,计算动画属性的中间值,平滑地从当前动画值过渡到目标动画值,从而形成我们看到的动画。

为什么选择 Transition?

这时,你可能会发现两者的代码效果是一样的,而且使用 Transition 还更繁琐了一些,那么,我们为什么要使用 Transition呢?

因为两者的视野和管理范围不同animate*AsState() 面向的是单一的动画值,它关心某个属性如何进行动画,如何从当前值动画到目标值;而 Transition 面向的是动画值背后的状态,它同时管理和协调多个属性,这些属性都响应同一个状态的变化。

当我们需要对基于同一个状态驱动的多个属性同时进行动画时,Transition 就可以很方便地建立多属性的状态模型

比如:我给上面的示例中,添加一个缩放的动画。当矩形是圆角时缩小,变为直角时放大。

圆角和缩放联动动画效果

使用 Transition 实现:

@Composable
fun StatusTransferDemo() {
    var round by remember { mutableStateOf(false) } // 实际的状态

    // 创建并更新Transition对象,时刻更新为实际的状态
    val roundTransition = updateTransition(targetState = round)

    // 定义多个动画属性
    val roundedCornerSize by roundTransition.animateDp { targetState -> // 目标状态,即将切换到的状态
        if (targetState) {
            18.dp
        } else {
            0.dp
        }
    }

    val size by roundTransition.animateDp { targetState ->
        if (targetState) {
            48.dp
        } else {
            96.dp
        }
    }

    // 使用动画值
    Box(
        Modifier
            .padding(12.dp)
            .size(size = size)
            .clip(RoundedCornerShape(size = roundedCornerSize))
            .clickable {
                round = !round
            }
            .background(Color.Green)
    )
}

如果使用 animateDpAsState 实现:

@Composable
fun StatusTransferDemo() {
    // 是否圆角
    var round by remember { mutableStateOf(false) } // 实际的状态
    
    // 需要为每个动画属性单独调用 animate*AsState
    val roundedCornerSize by animateDpAsState(if (round) 18.dp else 0.dp)
    val size by animateDpAsState(if (round) 48.dp else 96.dp)
    
    // 使用动画值
    Box(
        Modifier
            .padding(all = 12.dp)
            .size(size = size)
            .clip(shape = RoundedCornerShape(size = roundedCornerSize))
            .clickable {
                round = !round
            }
            .background(color = Color.Green)
    )
}

其中 Transition 统一管理多个动画的状态,而 animateDpAsState 管理的是某一个动画属性的动画值。

而且在修改动画属性时,animateDpAsState 会对每一个动画属性都启动一个协程, Transition 则会统一地更新,只开启一个协程,性能更好。

深入 Transition.animate*()

目前我们了解了 Transition.animateDp 的基本用法,它其实是一个系列函数animate<T>

@Composable
inline fun <S> Transition<S>.animateDp(
    noinline transitionSpec: @Composable Transition.Segment<S>.() -> FiniteAnimationSpec<Dp> = {
        spring(visibilityThreshold = Dp.VisibilityThreshold)
    }, // 动画规格
    label: String = "DpAnimation", // 动画标签
    targetValueByState: @Composable (state: S) -> Dp // 根据状态计算目标值的 lambda 表达式
): State<Dp> =
    animateValue(Dp.VectorConverter, transitionSpec, label, targetValueByState)

targetValueByState 是一个函数类型的参数,S 是泛型,就是 Transition 对象的目标状态的类型,参数state是目标状态,是即将切换的状态,返回值是动画属性的目标值。

label参数方便我们调试动画的。比如给当前的示例函数添加上 @Preview 注解,来到预览界面,进入动画预览。

未指定label的动画属性

你可以在这里调试动画,并且你可以看到有两个动画属性,不过是通用的名称,虽然我们能猜出来它们分别对应的谁。

动画预览界面

但是不方便,变量一多,就更加难以辨认了。这时我们填上 label 参数:

// 定义动画
val roundedCornerSize by roundTransition.animateDp(label = "roundedCornerSize") { targetState -> // 目标状态,即将切换到的状态
    // ..
}

val size by roundTransition.animateDp(label = "rectSize") { targetState ->
    // ..
}

再回到刚刚的界面,发现动画属性的名称就是刚刚填的标签值。

image.png

这样我们调试动画细节也就更方便了,这也是由于 Transition 统一管理带来的好处。

并且当一个 Composable 函数中,有多个 Transition 实例时,它会以分组进行显示。

transitionSpec参数也是一个函数类型的参数,可以让我们为动画指定动画规格(AnimationSpec),它的返回值是 FiniteAnimationSpec<T>,查看它的继承树:

FiniteAnimationSpec继承树

可以看到除了无限循环动画,其它的动画规格都实现了 FiniteAnimationSpec<T> 接口。

对于无限循环的动画,Compose 团队给我们提供了 rememberInfiniteTransition() 函数

之所以 transitionSpec 参数设计为函数类型,是为了让我们可以根据不同的情况,给出不同的动画规格。

Compose 还提供了 isTransitioningTo 中缀函数,让我们直观、方便地表达状态切换过程。

例如:

val alpha by transition.animateFloat(
    label = "alpha",
    transitionSpec = {
        // this 指向 Segment<Boolean>
        when {
            // 当状态从 false (initialState) 转换到 true (targetState)
            false isTransitioningTo true -> spring(stiffness = Spring.StiffnessMedium)
            // 当状态从 true (initialState) 转换到 false (targetState)
            true isTransitioningTo false -> tween(durationMillis = 1000)
            // 其他情况或默认
            else -> spring()
        }
    }
) { isActive ->
    if (isActive) 1f else 0.5f
}

在这个例子中,当 active 状态从 false 变为 true 时,透明度动画使用 spring;当从 true 变为 false 时,使用持续1秒的 tween

上述的 initialStatetargetState 都是由 Transition.Segment<S> 上下文提供的。

interface Segment<S> {
    // 初始状态
    val initialState: S

    // 目标状态
    val targetState: S

    // 判断是否从 this (某个具体状态) 转换到 targetState (另一个具体状态)
    infix fun S.isTransitioningTo(targetState: S): Boolean {
        return this == initialState && targetState == this@Segment.targetState
    }
}

更精细的控制:MutableTransitionState

其实 Transitionanimate*AsState 本质上是一样的,都是做状态转移的动画的,Transition也和animate*AsState一样,无法像Animatable 那样对动画流程做详细的定制,比如不能设置每次动画的初始值。

不过,不过要定制的话也不是不可以,我们点进去 updateTransition() 的源码:

@Composable
fun <T> updateTransition(
    targetState: T,
    label: String? = null
): Transition<T> {
    // 关键在于 Transition 的构造
    val transition = remember { Transition(targetState, label = label) }
    transition.animateTo(targetState) // 每次重组时更新目标状态
    DisposableEffect(transition) {
        onDispose {
            // Clean up on the way out, to ensure the observers are not stuck in an in-between
            // state.
            transition.onTransitionEnd()
        }
    }
    return transition
}

再点进去 Transition 类的构造函数:

internal constructor(
    initialState: S,
    label: String?
) : this(MutableTransitionState(initialState), label)

可以看到我们的状态值是由 MutableTransitionState 对象进行管理的,所以我们也可以这样写:

// 创建并更新Transition对象,时刻更新为实际的状态
val roundTransition = updateTransition(targetState = MutableTransitionState(initialState = round))

这样写,我们就可以直接控制 MutableTransitionState 实例的 initialStatetargetState

比如在组件首次出现界面时,启动一个动画,实际上就是组件的入场动画。

val roundedState = remember { MutableTransitionState(initialState = !round) }
roundedState.targetState = round
val roundTransition = updateTransition(targetState = roundedState)