17.1 Compose 动画(一) —— Animation

342 阅读5分钟

本文按照自下而上的顺序介绍 Compose 动画,从基础开始介绍 Compose 动画的组成部分,前面的内容偏理论,干巴巴的不够生动。

官方文档是按照自上而下的顺序介绍的。始就是各种子可以直接看到效果的例,只是机械调用 Api 。

怕无聊的话先从 官方文档官方Demo 开始,再回头看这里。

Compose 动画

Compose 动画是属性动画,说白了就是不断改变某个与UI相关属性的值同时更新UI。

A7188D2F-232F-4933-A545-4C988672BEAA.png

  • AnimationState :为调用者提供 State,当屏幕刷新时从 Animation 对象中根据刷新时间计算出对应的动画值并更新到 State 中。 封装在动画 Api 内部,调用者只需要关心其提供的 value 即可。

  • Animation :本身是无状态的,其属性定义了动画的范围,其方法可以通过属性(初始值、结束值、时长等)计算出动画在某一时刻的值。这些都以 Api 参数的形式交给调用者自定义。

AnimationState 和 Animation 都被封装到了动画 Api 内部,这些对于调用者都是透明的。调用者只需要关注两点:

  1. 用参数来定义动画
  2. 将 Api 提供的 value 与相应的属性进行关联

Animation

Animation 是接口类型定义了动画必要的参数和方法。

interface Animation<T, V : AnimationVector> {
    //1 将泛型类型转换成动 AnimationVector 的工具
    val typeConverter: TwoWayConverter<T, V>
	
    //2 动画结束的目标值
    val targetValue: T 
	
    //3 下面的属性和方法决定了动画具体是怎么完成的
    @get:Suppress("MethodNameUnits")
    val durationNanos: Long
  
    val isInfinite: Boolean

    fun getValueFromNanos(playTimeNanos: Long): 

    fun getVelocityVectorFromNanos(playTimeNanos: Long): V
    
	fun isFinishedFromNanos(playTimeNanos: Long): Boolean {
		return playTimeNanos >= durationNanos
	}
}

我们从注释中的 1、2、3 处来了解 Animation

1 TwoWayConverter

它是对 Animation 泛型中的两个类型进行转换的工具。

interface TwoWayConverter<T, V : AnimationVector> {
    //将属性类型转换成 AnimationVector
    val convertToVector: (T) -> 
    //将 AnimationVector 转换成属性类型
    val convertFromVector: (V) -> T
}

Animation 接口泛型这种设计使得 Animation 内部不关注属性的具体类型,提高了接口的通用性

  1. Animation 对象创建时先使用 converter 将传入的 T 类型值转换成 AnimationVector 类型

  2. Animation 对象内部仅使用 AnimationVector 类型处理动画

  3. 当需要 T 类型值时再使用 converter 将当前动画改变过的 AnimationVector 类型值转换成 T 类型

例如 Size 类型的动画,Size 有 width, height 2个属性,Size(50f,50f) 变成 Size(100f,200f) 过程中 width, height 是分别变化的。

private val SizeToVector: TwoWayConverter<Size, AnimationVector2D> 
    TwoWayConverter(
        convertToVector = { AnimationVector2D(it.width, it.height) },
        convertFromVector = { Size(it.v1, it.v2) }
    )

AnimationVector 和 TwoWayConverter

一维到四维的 AnimationVector 在 Compose 中已经声明好了可以直接使用,上面 Size 有 width, height 2个属性就转换成 2D , 如果是 Color(a,r,g,b)就转换成 4D 。

具体转成几维根据需要来选择,如果需要 5D 、6D 可以自己扩充

6A061ED6-97D9-47B6-8BD2-6E7AB32179BF.png

再说 TwoWayConvert ,显然想要使用动画接口的类型必须要有自己的 TwoWayConvert,但是不用慌 Compose 中同样已经声明了很多类型的 TwoWayConvert 可以直接拿来使用。

13593ADA-D3C3-4EA3-9D09-0C7F74A4E2A2.png

52B6ACF9-2A54-4727-9708-969C7E36166F.png 这些 VectorConverter 都拓展成了类型的静态属性可以直接  类型.VectorConverter 来使用,例如 Color.VectorConverter

2 TargetBasedAnimation 和 DecayAnimation

对于 targetValue Compose 动画框架里提供了两个实现类。

一个是有确定 targetValue 的 TargetBasedAnimation 动画从给定的初始值过渡到目标值

一个是给定初始值和初始速度的 DecayAnimation ,动画执行过程中速度逐渐衰减到 0,可以通过计算算出 targetValue

大多数情况下我们都是使用 TargetBasedAnimation 动画,处理拖拽事才使用 DecayAnimation。

我们越过框架实现,来模拟一下这两个类的实现,都实现最简单的一维线性动画。

TargetBasedAnimation 是给定值的初始值和目标值,通过 getValueFromNanos() 计算出动画运行时的值。

class MyTargetBasedAnimation<T>(
    initValue:T,
    override val targetValue: T,
    override val durationNanos: Long,
    override val typeConverter: TwoWayConverter<T, AnimationVector1D>
):Animation<T,AnimationVector1D> {

    override val isInfinite: Boolean = false

    private val initValueVector = typeConverter.convertToVector(initValue
    private val targetValueVector = typeConverter.convertToVector(targetValue)
    
    override fun getValueFromNanos(playTimeNanos: Long): T {
        val valueDiff = targetValueVector.value - initValueVector.value
        val progress = playTimeNanos / durationNanos.toFloat()
        val valueVector = AnimationVector1D(valueDiff * progress)
        return typeConverter.convertFromVector(valueVector)
    }

    //这里不做速度相关计算
    override fun getVelocityVectorFromNanos(playTimeNanos: Long): AnimationVector1D {
        return AnimationVector1D(0f)
    }
}

DecayAnimation 是提供初始值和初始速度,通过 getValueFromNanos() 计算出动画运行时的值。

/*
    衰减动画  匀减速运动
    Vo = initVelocityVector ,Vt = 0,
    加速度公式 a=(Vt-Vo)/t ,Vt = 0 => a = -Vo/t
    位移公式 ∆x = Vo.t - ½ a.t ²
 */
const val SecondToNanos: Long1_000_000_1000L
class MyDecayAnimation<T>(
    initValue:T,
    private val initVelocityVector:AnimationVector1D,
    override val durationNanos: Long,
    override val typeConverter: TwoWayConverter<T, AnimationVector1D
):Animation<T,AnimationVector1D> {
    
    private val initValueVector = typeConverter.convertToVector(initValue)
    //a = -Vo/t
    private val acceleration:Float = -initVelocityVector.value / (durationNanos.toFloat() / SecondToNanos)

    override val isInfinite: Boolean = false
    override val targetValue: T
        get() = getValueFromNanos(durationNanos)
    
    // initValue + ∆x
    override fun getValueFromNanos(playTimeNanos: Long): T {
        val playTimeSecond = playTimeNanos.toFloat() / SecondToNanos
        val deltaX = (initVelocityVector.value * playTimeSecond) - (0.5f * acceleration * playTimeSecond * playTimeSecond)
        val value = initValueVector.value + deltaX
        val valueVector = AnimationVector1D(value)
        return typeConverter.convertFromVector(valueVector)
    }
    // Vt= v0 + at
    override fun getVelocityVectorFromNanos(playTimeNanos: Long): AnimationVector1D {
        val playTimeSecond = playTimeNanos.toFloat() / SecondToNanos
        val vt = initVelocityVector.value + acceleration * playTimeSecond
        return AnimationVector1D(vt)
    }
}

从上面两个模拟实现可以看出真正决定了他们动画效果的是 getValueFromNanos() 、getVelocityVectorFromNanos() 的返回值,想要不同的动画效果就要再这两个方法中使用不同的算法返回预期的值,当然 durationNanos 代表的动画时长也会对预期值产生影响。

3 VectorizedAnimationSpec 和 VectorizedDecayAnimationSpec

Compose 动画框架将 Animation 中控制动画变具体细节的部分封装到 Spec 中作为参数传递给TargetBasedAnimation 和 DecayAnimation ,由 Spec 来代理 TargetBasedAnimation 和 DecayAnimation 对应的实现。这样不同的动画效果就可以通过传递不同的 Spec 来实现了。

73A3F0FA-39B1-4450-893E-AFD596C77D0C.png

FDCB4229-8952-4466-9E0A-276AB3E3E179.png

这些的 Spec 都是泛型 AnimationVector ,供给框架内部使用。

Compose 动画 Api 入参的 Spec 是 AnimationSpec 或 DecayAnimationSpec ,他们的泛型都是 T 并且在内部都声明了转换方法 。

83279019-695B-44F5-9C90-E917AD2F8E17.png

interface AnimationSpec<T> {
    fun <V : AnimationVector> vectorize(
        converter: TwoWayConverter<T, V>
    ): VectorizedAnimationSpec<V>
}

现在我们看 Compose 中的动画 Api ,虽然不知道 Api 的具体作用,但是参数含义我们已经可以看懂了 O(∩_∩)O

@Composable
fun <T, V : AnimationVector> animateValueAsState(
 	//有确定的目标值 这是个 TargetBasedAnimation
    targetValue: T, 
  	//类型转换工具
    typeConverter: TwoWayConverter<T, V>, 
  	//定义动画执行的细节
    animationSpec: AnimationSpec<T> = remember {
        spring(visibilityThreshold = visibilityThreshold)
    },
    visibilityThreshold: T? = null,
    finishedListener: ((T) -> Unit)? = null
): State<T> { //返回值 State ,动画启动后每帧变化的值会更新到返回的 State 中