前言
在上两篇文章中,我们了解了 Compose 中动画的各种 animationSpec 的使用和 anim.xxx 的方法参数使用说明,这节开始学习 Transition 的使用
Transition
可对多个属性值进行动画
在 Android 原生中, Transition 动画是用来做 activity 或 Fragment 的转场动画的, 因为 Activity 或 Fragment 是比 View 更外层的,是不能使用 View 的动画, 所以单独使用了 Transition 来做转场动画的, 而在 compose 中, 他和 Activity 和 Fragment 都没有关系, 它只属于 compose 的转场动画
我们先通过一个例子来看看他是如何使用的, 后面再讲讲具体的源码的一些细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
@Composable
fun TransitionAnim() {
var big by remember { mutableStateOf(false) }
val scaleTransition = updateTransition(targetState = big, label = "scaleTransition")
/** animatexxx 有非常多的方法 , 根据状态的设置**/
val size by scaleTransition.animateDp(
transitionSpec = { tween(durationMillis = 2000) }, label = "size"
) { if (it) 200.dp else 100.dp }
val corner by scaleTransition.animateDp(
transitionSpec = { tween(durationMillis = 2000) }, label = "corner"
) {
if (it) 100.dp else 0.dp
}
/**
* transitionSpec -> Segment 分段的意思,可以设置分段的动画
*/
val color by scaleTransition.animateColor(
transitionSpec = {
when {
/** 初始状态从 false 转换到 true 的目标状态(这里仅仅使用 Boolan 类型,实际情况可以是任意类型的) **/
/** 设置 false 到 true 的颜色 tween 渐变 **/
false isTransitioningTo true -> tween(durationMillis = 3000)
else -> tween(durationMillis = 3000)
}
}, label = "color"
) {
if (it) Color.Red else Color.Green
}
Box(modifier = Modifier
.size(size)
.clip(RoundedCornerShape(corner))
.background(color)
.clickable {
big = !big
})
}
|
这段代码的意思是通过 updateTransition 创建了一个 Transition 动画, 并且设置的初始状态是一个 Boolean 的变量为 false, 通过触发点击事件改变这个 Boolean 类型的 big 状态, 从而改变 size , color, corner 属性, 而他们的改变又是通过 Transition.animateXXX 去改变的, 最终开启转场动画, 最终这个动画的效果是这样的
我们先看看 Transition 是如何通过 updateTransition 创建的,看看源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
fun <T> updateTransition(
targetState: T,
label: String? = null
): Transition<T> {
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
}
|
第一个参数是泛型状态的初始状态, 意味着可以传递任意类型, Boolean 或者枚举, 又或者是其他的各种状态, 第二个参数是 label 参数, 这个参数可以认为给这个动画起的一个名称, 后面会再介绍他在哪里具体使用场景
后面使用了 remember 包裹并创建了 Transition 对象, 再看看 Transition 内部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
class Transition<S> @PublishedApi internal constructor(
private val transitionState: MutableTransitionState<S>,
val label: String? = null
) {
internal constructor(
initialState: S,
label: String?
) : this(MutableTransitionState(initialState), label)
// 初始状态
var currentState: S
get() = transitionState.currentState
internal set(value) {
transitionState.currentState = value
// 目标状态
var targetState: S by mutableStateOf(currentState)
internal set
/**从初始状态到目标状态的转换
* [segment] contains the initial state and the target state of the currently on-going
* transition.
*/
var segment: Segment<S> by mutableStateOf(SegmentImpl(currentState, currentState))
private set
/**
* Indicates whether there is any animation running in the transition.
*/
val isRunning: Boolean
get() = startTimeNanos != AnimationConstants.UnspecifiedTime
|
Transition 中,它的内部属性维护了一些初始状态和目标状态,和动画是否正在运行等,然后我们传递进来的参数又通过他的构造函数, 又使用 MutableTransitionState 对象进行了包裹一层, 而他的内部, 也维护了一些状态
1
2
3
4
5
6
7
8
9
10
|
class MutableTransitionState<S>(initialState: S) {
// 初始状态
var currentState: S by mutableStateOf(initialState)
internal set
// 目标状态
var targetState: S by mutableStateOf(initialState)
val isIdle: Boolean get() = (currentState == targetState) && !isRunning
internal var isRunning: Boolean by mutableStateOf(false)
}
|
updateTransition 基本介绍完了, 它这里仅仅是瞄准的是状态, 因为我们传递进来的参数就是状态,但是值又是哪里改变的呢 ?
值的改变其实是在 animatexxx 方法中, 这样的方法有很多, 大概有这些
我们选一个 color 配置的看看 animateColor 动画,源码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Composable
inline fun <S> Transition<S>.animateColor(
noinline transitionSpec:
@Composable Transition.Segment<S>.() -> FiniteAnimationSpec<Color> = { spring() },
label: String = "ColorAnimation",
targetValueByState: @Composable() (state: S) -> Color
): State<Color> {
val colorSpace = targetValueByState(targetState).colorSpace
val typeConverter = remember(colorSpace) {
Color.VectorConverter(colorSpace)
}
return animateValue(typeConverter, transitionSpec, label, targetValueByState)
}
|
第一个参数 transitionSpec 是配置转场动画的, 默认返回 spring 弹簧动画, 返回的类型是 FiniteAnimationSpec, 关于 FiniteAnimationSpec 我们之前的文章有讲到过,通过它的类的继承结构,它可以使用如下这些 Spec
还有一个需要注意的是, 这个 transitionSpec 属性有一个上下文环境 Transition.Segment , 他可以认为是动画的一个段落,他的内部维护了初始状态和目标状态,我们可以看下它是怎么创建的,在 animateColor 源码中,将 transitionSpec 传入到了 animateValue 方法中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
inline fun <S, T, V : AnimationVector> Transition<S>.animateValue(
typeConverter: TwoWayConverter<T, V>,
noinline transitionSpec: @Composable Transition.Segment<S>.() -> FiniteAnimationSpec<T> =
{ spring() },
label: String = "ValueAnimation",
targetValueByState: @Composable (state: S) -> T
): State<T> {
val initialValue = targetValueByState(currentState) // 初始状态
val targetValue = targetValueByState(targetState) // 目标状态
val animationSpec = transitionSpec(segment) // 创建了 animationSpec 对象
return createTransitionAnimation(initialValue, targetValue, animationSpec, typeConverter, label)
}
|
这里可以看到通过 targetValueByState 创建了一个初始状态和目标状态值,而这里对应例子的代码也就是,初始状态是 Color.Red,目标状态是 Color.Green 然后,将这个值都存储在了 segment 变量中,而 segment 传入到 transitionSpec 表达式中做上下文对象,看看 segment 对象是如何存储的,代码如下
1
|
var segment: Segment<S> by mutableStateOf(SegmentImpl(currentState, currentState))
|
这里一个疑问,那就是它不是把动画分成一部分的段落吗,那他是决定那个段落运行的是那段代码呢 ?也就是执行那个动画呢,其实是在 updateTransition 中的 animateTo 中,先看看
updateTransition 中 animateTo 的方法
1
2
3
4
|
internal fun animateTo(targetState: S) {
if (!isSeeking) {
updateTarget(targetState)
}
|
接着再看看 updateTarget 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
internal fun updateTarget(targetState: S) {
if (!isSeeking) {
if (this.targetState != targetState) {
// Starting state should be the "next" state when waypoints are impl'ed
segment = SegmentImpl(this.targetState, targetState) // 设置状态值和目标值
currentState = this.targetState // 上一次目前状态设置为当前的初始状态, 也就是动画这一刻的状态就是初始状态
this.targetState = targetState // 设置目标状态
if (!isRunning) {
updateChildrenNeeded = true
}
_animations.forEach { it.resetAnimation() }
}
}
}
|
所以这里的整体逻辑就是当我们的 big 状态值发生改变的时候,最终会调用到 updateTransition 方法,最终调用 animateTo 方法,然后内部设置初始值和状态值的改变,也就是 segment 改变,最终回到例子中我们配置的 transitionSpec 是这样的
1
2
3
4
|
/** 初始状态从 false 转换到 true 的目标状态(现实场景中,可能是一个枚举常亮) **/
/** 设置 false 到 true 的颜色 tween 渐变 **/
false isTransitioningTo true -> tween(durationMillis = 2000)
else -> tween(durationMillis = 3000)
|
所以从目标状态 false 到 true 状态转变的时候,使用的 tween(durationMillis = 2000) 动画,而 true 到 false 的情况使用的就是 tween(durationMillis = 3000) 动画
最后看下 isTransitioningTo 方法是什么
1
2
3
|
infix fun S.isTransitioningTo(targetState: S): Boolean {
return this == initialState && targetState == this@Segment.targetState
}
|
方法也比较简单,他是一个中缀函数,这段代码也等价于 false == this.initialState && true == this.targetState
表示一个状态转换为另外一个状态,是否匹配
第二个参数可以认为是一个名称, 第三个参数 targetValueByState 是根据状态返回目标值, 上面的 color 配置的动画, if (it) Color.Red else Color.Green, 那么就是 true 状态返回红色, false 返回绿色
MutableTransitionState 设置初始状态
代码进入组合阶段后立即开始播放动画
通过上面的分析我们都知道,在 Transition 创建的时候,我们传入的初始状态,最终会被传入到 MutableTransitionState 对象中,而上面例子的我们是直接传入的一个初始状态,但是现在我的需求变化了,我需要手动设置初始状态和目标状态,那么就可以使用 MutableTransitionState 对象,他允许我们在代码进入组合阶段后立即开始播放动画,能够在创建对象的时候,显示的设置初始状态和目标状态,他是这么使用的
1
2
3
|
var currentState = remember { MutableTransitionState(initialState) } // 设置初始状态
currentState.targetState = targetState
val transition = updateTransition(currentState)
|
改造一下上面的例子, 界面在刚进入组合阶段就开启动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@Preview
@Composable
fun TransitionAnim2() {
var big = remember {
MutableTransitionState(true).apply {
// 设置状态 true 到 false 的转变
targetState = false
}
}
val scaleTransition = updateTransition(transitionState = big, label = "scaleTransition")
val size by scaleTransition.animateDp(
transitionSpec = { tween(durationMillis = 2000) }, label = "size"
) { if (it) 200.dp else 100.dp }
val corner by scaleTransition.animateDp(
transitionSpec = { tween(durationMillis = 2000) }, label = "corner"
) { if (it) 100.dp else 0.dp }
val color by scaleTransition.animateColor(
transitionSpec = { tween(2000) }, label = "color"
) { if (it) Color.Red else Color.Green }
Box(modifier = Modifier
.size(size)
.clip(RoundedCornerShape(corner))
.background(color)
.clickable { big.targetState = !big.targetState })
}
|
代码基本一致, 只是手动设置了初始状态和目标状态(红色往绿色的渐变动画), 在刚进入界面的时候就开启动画了, 运行代码效果如下
Transition 动画基本就介绍完成了,仔细观察上面的动画,我们是不是也可以使用 animationxxAsStatus 同样可以做到,也就是需要创建 3 个 asStatus 对象,类似于这样
1
2
3
|
val size by animateXXAsState()
val corner by animateXXAsState()
val color by animateXXAsState()
|
这样确实也是可以做到的,就是会比较麻烦,需要改变3 个状态值来做动画效果,而 Transition 不一样,它可以认为将状态统一提升了,视角变得更高了,针对 Transition 的状态来统一对多个属性做动画效果
动画预览
还记得我们上面设置的 label 吗,他就是我们预览的时候,可以看到设置的一个名称,这样预览动画的时候,比较方便,如图,点击 start preview 就可以进入预览模式
进入预览模式后,左下角的操控面板就可以看到我们设置的 label 值,和状态值,过多的就不介绍了,这个面板调试动画还是很方便的
rememberInfiniteTransition
可对多个属性值进行无线循环的动画
这个动画比较简单,直接上代码看看效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Composable
fun RepeatAnim() {
val infiniteTransition = rememberInfiniteTransition(label = "")
val color by infiniteTransition.animateColor(
initialValue = Color.Red,
targetValue = Color.Green,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
), label = ""
)
// infiniteTransition.animateDp() ...
Box(
Modifier
.fillMaxSize()
.background(color)
)
}
|
这个代码使用 rememberInfiniteTransition 创建了 infiniteTransition 对象,其中 rememberInfiniteTransition 并没有什么参数可以配置的, 接着调用了 animateColor 方法,他能配置的参数基本都写出来的,第一个参数是初始状态为红色,第二个参数是目标状态是绿色,所以他是一个红色转为绿色的一个动画第三个参数是 animationSpec ,这个参数返回的是一个 InfiniteRepeatableSpec 对象,目前这个参数只有一个 infiniteRepeatable 的实现,他是一个无限循环动画,他的第一个参数是 animation,返回的对象是 DurationBasedAnimationSpec ,这个是基于时间设置的动画,我在基础篇动画的时候讲过,repeatMode 是重复模式, start, 从起始位置开始重复, Reverse 从目标值开始重复,这个以前也讲过了,所有的配置基本都在这里了
最终它的效果是这样的