流程定制型动画Animatable

前言

状态转移型动画animateXxxAsState写起来很简单,但也有一些局限性,比如:无法为每次动画单独设置初始值。它只允许设置第一次动画的初始值,而后续动画都只是在设置目标值。

这本质上是animateXxxAsState无法对动画的过程做随心所欲的控制。

如果要对动画的过程做随心所欲的控制,就需要使用Animatable对象。

Animatable()和animateXxxAsState的区别

我们先来看看Animatable()和animateXxxAsState的区别,只有了解它们之间的区别,我们才能在实际场景中选择最合适的方式来完成动画效果。

我们点进去animateDpAsState()函数的源码:

kotlin 复制代码
val size by animateDpAsState(targetValue = if (big) 96.dp else 48.dp) // 点击animateDpAsState
kotlin 复制代码
// AnimateAsState.kt
@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
    return animateValueAsState( // 点击animateValueAsState
        targetValue,
        Dp.VectorConverter,
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}

@Composable
fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    animationSpec: AnimationSpec<T> = remember { spring() },
    visibilityThreshold: T? = null,
    label: String = "ValueAnimation",
    finishedListener: ((T) -> Unit)? = null
): State<T> {

    val toolingOverride = remember { mutableStateOf<State<T>?>(null) }
    val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }
    ...
}

发现animateXxxAsState()函数内部使用的还是Animatable,那么我们就明白了:

==> animateXxxAsState是对Animatable的高级封装。

animateXxxAsState简化了API,使得我们写状态转换动画特别简单,但同时也有意地牺牲了部分功能:比如不能设置动画的初始值。

为什么要抛弃这部分功能呢?

这并非是不得已而为之,而是经过了精心考量的设计决策。因为animateXxxAsState的使用场景中不需要这些功能,所以抛弃就抛弃了呗,反正也用不到。animateXxxAsState专用于状态转换场景,它假设你使用它,只会关心"从当前状态动画到新状态"这一需求。在这种情况下,初始值就是组件的当前状态,所以无需额外指定。

Animatable作为底层API,功能最为强大,适用于各种复杂场景。而animateXxxAsState则是针对特定常见场景提供的简化方案。

如果你发现你的需求,使用animateXxxAsState难以实现,就应该使用更灵活的Animatable

所以现在你明白了吧!实战中的选择:如果是状态转移型场景,就使用animateXxxAsState(),否则使用Animatable;能用animateXxxAsState()完成需求,就使用animateXxxAsState(),难以实现,就使用Animatable

Animatable

创建Animatable对象

接下来我们看看是如何创建Animatable对象的,有两种方式。

第一种方式是调用Animatable()函数:

kotlin 复制代码
// Animatable.kt
fun Animatable(
    initialValue: Float, // 初始值
    visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
) = Animatable(
    initialValue,
    Float.VectorConverter,
    visibilityThreshold
)
kotlin 复制代码
// SingleValueAnimation.kt
// 重载
fun Animatable(initialValue: Color /*初始值*/): Animatable<Color, AnimationVector4D> =
    Animatable(initialValue, (Color.VectorConverter)(initialValue.colorSpace))

第二种方式是直接使用Animatable类的构造函数进行创建:

kotlin 复制代码
class Animatable<T, V : AnimationVector>(
    initialValue: T, // 初始值
    val typeConverter: TwoWayConverter<T, V>, // 在实际值(T)和动画向量(V)之间转换的转换器
    private val visibilityThreshold: T? = null,
    val label: String = "Animatable"
)

可以看到Animatable()函数体中,都使用了Animatable构造函数来创建对象,并且如果动画的初始值不是Float、Color类型,就必须使用Animatable的构造函数。

我们等下创建的动画的初始值都是Dp类型,所以我们来了解一下Animatable的构造函数。

使用Animatable构造函数创建对象,有两个必填参数,一个是initialValue初始值,另一个是typeConverter类型转换器。 类型转换器(TwoWayConverter)它是动画系统中的一个概念,解决了一个基本问题:如何让各种不同类型的值(如颜色、尺寸、位置等)都能进行平滑动画?

kotlin 复制代码
// VectorConverters.kt
interface TwoWayConverter<T, V : AnimationVector> {

    val convertToVector: (T) -> V // 将特定类型 T(如 Color、Dp)转换为动画向量 V
 
    val convertFromVector: (V) -> T // 将动画向量 V 转换回特定类型 T
}

因为动画的本质是数学运算,需要在向量空间进行插值计算。有了类型转换器,就可以将这些复杂类型转换为可计算的向量形式。使得动画系统可以:

  1. 将输入值分解为数值向量
  2. 在向量空间执行插值计算
  3. 将结果向量转换回原始类型

所以需要类型转换器这个参数。

Compose 提供了不同维度的动画向量:

  • AnimationVector1D: 单值(如透明度)
  • AnimationVector2D: 二维值(如位置)
  • AnimationVector3D: 三维值
  • AnimationVector4D: 四维值(如 RGBA 颜色)

并且对于常见的类型,Compose 已经实现了转换器,我们无需手动实现了,比如:Dp.VectorConverterFloat.VectorConverter

言归正传,回到创建Animatable对象上面来,创建一个Animatable对象非常简单,只需要这样:

kotlin 复制代码
val anim = remember { // remember => 防止重组时的重复初始化
    Animatable(
        initialValue = 48.dp, // 设置初始值
        typeConverter = Dp.VectorConverter // 设置类型转换器
    )
}

animateTo()

创建好Animatable对象,就可以开始对动画进行配置了,具体的操作就是调用它的animateTo()函数。

这也是创建Animatable 对象时不使用 by 关键字进行属性委托的原因。

使用 by 委托的状态会自动解包,让你直接使用它的内部值,但是我们需要调用Animatable方法并且访问它的属性,而不仅仅是获取其值,因此不适合使用属性委托。

并且即使你使用了by,也会报错:

perl 复制代码
Type 'TypeVariable(T)' has no method 'getValue(Nothing?, KProperty<*>)' and thus it cannot serve as a delegate

说明Compose根本就没有实现Animatable 类的 getValue() 方法,它不是为作为属性委托而设计的。

调用animateTo()函数时,发现报错了,报错信息:

perl 复制代码
Suspend function 'animateTo' should be called only from a coroutine or another suspend function

这表明它是一个挂起(suspend)函数,需要在协程的作用域或其他 suspend 函数中调用,那我们现在来创建一个协程。

swift 复制代码
协程可以理解为"轻量级的线程",我们将耗时的操作,比如网络请求、数据库访问或动画放进去执行,这样不会阻塞UI线程。
kotlin 复制代码
LaunchedEffect(Unit) { 
    delay(2000) // 动画默认的时长是300ms,防止动画速度太快,没看清效果
    anim.animateTo(targetValue = 96.dp)
}

LaunchedEffect是 Compose 专门设计的协程 API,当 key 参数变化时会自动重启协程,我们这里只想它启动一次,所以就将参数填入一个不变的值Unit,这也是最常用的填法,你也可以填true、false、常量。

我们来看看效果如何:

我并没有点击它,它是在界面启动后2s,自动执行的动画。

完整代码如下:

kotlin 复制代码
val anim = remember { // remember => 防止重组时的重复初始化
    Animatable(
        initialValue = 48.dp,
        typeConverter = Dp.VectorConverter
    )
}

LaunchedEffect(Unit) {
    delay(2000)
    anim.animateTo(targetValue = 96.dp)
}

Box(Modifier
    .size(anim.value)
    .background(Color.Green))

现在实现点击绿色矩形,绿色矩形会变大或缩小,只需在上面的基础上简单改改,就可以了:

kotlin 复制代码
var big by remember { mutableStateOf(false) }
val size by remember(big) { mutableStateOf(if (big) 96.dp else 48.dp) }
val anim = remember {
    Animatable(
        initialValue = size,
        typeConverter = Dp.VectorConverter
    )
}


LaunchedEffect(big) {
    anim.animateTo(size)
}

Box(Modifier
    .size(anim.value)
    .background(Color.Green)
    .clickable {
        big = !big
    })

运行效果:

snapTo()

snapTo()函数可以将 Animatable对象的值设置为指定值,并不产生动画效果。适用于需要立即改变值而不需要平滑过渡的场景

回到本文的开头目标,来实现设置动画的初始值,其实也很简单只需加上一行代码:

diff 复制代码
LaunchedEffect(big) {
+   anim.snapTo(targetValue = if (big) 144.dp else 24.dp) // 瞬间完成值的突变
    anim.animateTo(size) // 动画完成值的渐变
}

第一次点击时,绿色矩形的大小会先突变到144.dp,然后以动画的形式渐变到96.dp;再次点击,会先突变到24.dp, 会以动画的形式渐变到48.dp。

并且初始运行时,会有一次动画:绿色矩形的大小会突变到24.dp,然后渐变到48.dp。

这是由于首次组合时,会触发LaunchedEffect 执行 ,即使 big 没有变化,首次组合时也会执行其中的代码块。这是LaunchedEffect的设计特性------当包含它的可组合函数首次进入组合时,它会立即启动协程并执行其中的代码。

完整代码:

kotlin 复制代码
@Composable
fun AnimatedBox(){
    var big by remember { mutableStateOf(false) }
    val size by remember(big) { mutableStateOf(if (big) 96.dp else 48.dp) }
    val anim = remember {
        Animatable(
            initialValue = size,
            typeConverter = Dp.VectorConverter
        )
    }


    LaunchedEffect(big) {
        anim.snapTo(targetValue = if (big) 144.dp else 24.dp)
        anim.animateTo(size)
    }

    Box(Modifier
        .size(anim.value)
        .background(Color.Green)
        .clickable {
            big = !big
        })
}
相关推荐
消失的旧时光-194311 小时前
Kotlinx.serialization 使用讲解
android·数据结构·android jetpack
Tans517 小时前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Lei活在当下2 天前
【业务场景架构实战】2. 对聚合支付 SDK 的封装
架构·android jetpack
Tans54 天前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
alexhilton5 天前
runBlocking实践:哪里该使用,哪里不该用
android·kotlin·android jetpack
Tans58 天前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
ljt27249606619 天前
Compose笔记(四十九)--SwipeToDismiss
android·笔记·android jetpack
4z3311 天前
Jetpack Compose重组优化:机制剖析与性能提升策略
性能优化·android jetpack
alexhilton11 天前
Android ViewModel数据加载:基于Flow架构的最佳实践
android·kotlin·android jetpack
水牛15 天前
一行代码完成startActivityForResult
android·android jetpack