前言

在上两篇文章中,我们了解了 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 从目标值开始重复,这个以前也讲过了,所有的配置基本都在这里了

最终它的效果是这样的