流程定制型动画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
        })
}
相关推荐
_一条咸鱼_3 小时前
Android Runtime虚拟机实例创建与全局状态初始化(11)
android·面试·android jetpack
我命由我1234511 天前
Android 动态申请 REQUEST_INSTALL_PACKAGES 权限问题:申请权限失败
android·java·开发语言·java-ee·android studio·android jetpack·android-studio
ljt272496066112 天前
Compose笔记(二十四)--Canvas
笔记·android jetpack
ljt272496066113 天前
Compose笔记(二十三)--多点触控
笔记·android jetpack
我命由我123451 个月前
Android 解绑服务问题:java.lang.IllegalArgumentException: Service not registered
android·java·开发语言·java-ee·安卓·android jetpack·android-studio
我命由我123451 个月前
MQTT - Android MQTT 编码实战(MQTT 客户端创建、MQTT 客户端事件、MQTT 客户端连接配置、MQTT 客户端主题)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
前行的小黑炭1 个月前
Android LiveData源码分析:为什么他刷新数据比Handler好,能更节省资源,解决内存泄漏的隐患;
android·kotlin·android jetpack
_一条咸鱼_1 个月前
深度剖析:Java PriorityQueue 使用原理大揭秘
android·面试·android jetpack
_一条咸鱼_1 个月前
揭秘 Java PriorityBlockingQueue:从源码洞悉其使用原理
android·面试·android jetpack
_一条咸鱼_1 个月前
深度揭秘:Java LinkedList 源码级使用原理剖析
android·面试·android jetpack