作用
在 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 函数的作用是:
- 初始化(首次组合)时创建
Transition对象,这个对象会在后续重组过程中存在,因为函数内部使用了remember()函数。 - 在后续重组过程中,每当
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 参数:
// 定义动画
val roundedCornerSize by roundTransition.animateDp(label = "roundedCornerSize") { targetState -> // 目标状态,即将切换到的状态
// ..
}
val size by roundTransition.animateDp(label = "rectSize") { targetState ->
// ..
}
再回到刚刚的界面,发现动画属性的名称就是刚刚填的标签值。
这样我们调试动画细节也就更方便了,这也是由于 Transition 统一管理带来的好处。
并且当一个 Composable 函数中,有多个 Transition 实例时,它会以分组进行显示。
transitionSpec参数也是一个函数类型的参数,可以让我们为动画指定动画规格(AnimationSpec),它的返回值是 FiniteAnimationSpec<T>,查看它的继承树:
可以看到除了无限循环动画,其它的动画规格都实现了 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。
上述的 initialState、targetState 都是由 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
其实 Transition 和 animate*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 实例的 initialState 和 targetState。
比如在组件首次出现界面时,启动一个动画,实际上就是组件的入场动画。
val roundedState = remember { MutableTransitionState(initialState = !round) }
roundedState.targetState = round
val roundTransition = updateTransition(targetState = roundedState)