• Compose 动画艺术探索之 AnimationVector


    本篇文章是此专栏的第六篇文章,前几篇文章大概将 Compose 中的动画都简单过了一遍,如果想阅读前几篇文章的话可以点击下方链接:

    AnimationVector 是啥?

    大家可能都知道或听说在 Compose 中动画使用起来很简单,但其实是使用起来很简单,内部逻辑其实也不简单。。。大家用了这么久的 Compose ,里面的动画也都使用了很多,但这个问题其实没有多少人能回答出来。

    对啊,AnimationVector 是个啥呢?其实虽然不知道它是个啥,但其实我们却在一直使用它!来举个栗子🌰吧!

    val colors by animateColorAsState(
        Color.Red,
        animationSpec = spring(Spring.StiffnessVeryLow)
    )
    ​
    val sizes by animateDpAsState(
        15.dp,
        animationSpec = spring(Spring.StiffnessVeryLow)
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的代码熟悉么?看起来很简单,并且也经常使用,其实在第三篇文章 Compose 动画艺术探索之属性动画 中也提到了 AnimationVector ,其实说白了它就是个类型转换器,并没有什么神秘的。

    AnimationVector

    下面来直接看下 AnimationVector 的源码吧:

    sealed class AnimationVector {
        internal abstract fun reset()
        internal abstract fun newVector(): AnimationVector
    ​
        internal abstract operator fun get(index: Int): Float
        internal abstract operator fun set(index: Int, value: Float)
        internal abstract val size: Int
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可以看到 AnimationVector 是一个密封类,它是 AnimationVector1DAnimationVector2DAnimationVector3DAnimationVector4D 的基类。为了动画任何任意类型,它需要提供一个 TwoWayConverter 来定义如何将任意类型 T 转换为 AnimationVector,反之亦然。取决于这个类型 T 有多少维度,它可能需要转换为AnimationVector 的任何子类。例如,基于位置的对象应该转换为 AnimationVector2D (x、y 的坐标),而描述矩形边界的对象应该转换为 AnimationVector4D (左上右下的坐标)。

    下面来看下 AnimationVector1D 的源码吧!

    class AnimationVector1D(initVal: Float) : AnimationVector() {
        // 这个字段保存了对象中唯一的Float值。
        var value: Float = initVal
            internal set
    ​
        override fun reset() {
            value = 0f
        }
    ​
        override fun newVector(): AnimationVector1D = AnimationVector1D(0f)
        override fun get(index: Int): Float {
            if (index == 0) {
                return value
            } else {
                return 0f
            }
        }
    ​
        override fun set(index: Int, value: Float) {
            if (index == 0) {
                this.value = value
            }
        }
    ​
        override val size: Int = 1
    }
    
    • 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

    可以看到 AnimationVector1D 类中的代码并不难理解,首先继承自 AnimationVector ,然后实现了几个抽象方法,一共有四个嘛,咱们再来看下 AnimationVector1D 就基本能知道这四个类的区别了。

    class AnimationVector3D(v1: Float, v2: Float, v3: Float) : AnimationVector() {
        var v1: Float = v1
            internal set
    ​
        var v2: Float = v2
            internal set
    ​
        var v3: Float = v3
            internal set
    ​
        override fun reset() {
            v1 = 0f
            v2 = 0f
            v3 = 0f
        }
    ​
        override fun newVector(): AnimationVector3D = AnimationVector3D(0f, 0f, 0f)
    ​
        override fun get(index: Int): Float {
            return when (index) {
                0 -> v1
                1 -> v2
                2 -> v3
                else -> 0f
            }
        }
    ​
        override fun set(index: Int, value: Float) {
            when (index) {
                0 -> v1 = value
                1 -> v2 = value
                2 -> v3 = value
            }
        }
    ​
        override val size: Int = 3
    }
    
    • 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

    其实内部和 AnimationVector1D 没啥区别,只不过多了两个参数而已。AnimationVector2DAnimationVector4D 的代码猜也能猜到了,AnimationVector2D 中有两个参数,AnimationVectorD 中有四个参数嘛!大家可以去看看,就是想的那样!

    TwoWayConverter

    上面一直提到 TwoWayConverter ,那就来看看吧!

    interface TwoWayConverter {
        // 定义如何将类型 T 转换为向量类型
        val convertToVector: (T) -> V
        // 定义如何将向量类型转换为类型 T 
        val convertFromVector: (V) -> T
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    TwoWayConverter 是一个接口,包含了如何从任意类型 T 转换为 AnimationVector,并将AnimationVector 转换回类型 T 的定义。这允许动画在任何类型的对象上运行,例如位置,矩形,颜色等。

    到这里这些类的定义都走了一遍,那么咱们就来看看上面写的 animateDpAsState 中是如何使用 AnimationVector 的吧!

    之前第三篇文章中提到过,Compose 中的属性动画最后都是调用的 animateValueAsState ,那就先来看下 animateValueAsState 吧!

    @Composable
    fun  animateValueAsState(
        targetValue: T,
        typeConverter: TwoWayConverter, // 转换器
        animationSpec: AnimationSpec = remember { spring() },
        visibilityThreshold: T? = null,
        label: String = "ValueAnimation",
        finishedListener: ((T) -> Unit)? = null
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    是不是有 TwoWayConverter !之前说这块的时候只是一带而过了,因为没有办法把所有的内容一次都说清楚,这样就没有重点或者说全是重点了。可以看到 typeConverter 是一个必须填写的参数,但是咱们在调用 animateXXXAsState 的时候并没有填写,为啥呢?很简单,官方帮我们将一些基本数据的转换都写好了,一起来看下!

    @Composable
    fun animateDpAsState(
        targetValue: Dp,
        animationSpec: AnimationSpec = dpDefaultSpring,
        label: String = "DpAnimation",
        finishedListener: ((Dp) -> Unit)? = null
    ): State {
        return animateValueAsState(
            targetValue,
            Dp.VectorConverter, // 转换器
            animationSpec,
            label = label,
            finishedListener = finishedListener
        )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到上面有一行重点代码:Dp.VectorConverter ,这个就构建了 animateValueAsState 中所需要的 TwoWayConverter ,下面就来看看是如何构建的吧!

    val Dp.Companion.VectorConverter: TwoWayConverter
        get() = DpToVector
    ​
    private val DpToVector: TwoWayConverter = TwoWayConverter(
        convertToVector = { AnimationVector1D(it.value) },
        convertFromVector = { Dp(it.value) }
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    代码并不多,可以看到这里使用了 AnimationVector1D ,为啥呢?因为 Dp 确实只需要一个值就可以表示,转换方法也很简单,直接将 Dp 中的 value 值进行转换即可。再来看几个吧!

    private val SizeToVector: TwoWayConverter =
        TwoWayConverter(
            convertToVector = { AnimationVector2D(it.width, it.height) },
            convertFromVector = { Size(it.v1, it.v2) }
        )
    ​
    private val RectToVector: TwoWayConverter =
        TwoWayConverter(
            convertToVector = {
                AnimationVector4D(it.left, it.top, it.right, it.bottom)
            },
            convertFromVector = {
                Rect(it.v1, it.v2, it.v3, it.v4)
            }
        )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    可以看到上面代码是 SizeRect 的转换,Size 需要宽高来进行描述,所以就使用的是 AnimationVector2D ,而 Rect 则需要四个值来分别表示左上右下的坐标点,所以就需要使用 AnimationVector4D 来描述。

    自定义转换器

    没错,咱们可以自定义转换器的!首先咱们来自定义一个类型吧!

    data class RealSize(val length: Dp, val width: Dp, val height: Dp)
    
    • 1

    比如这块定义一个 RealSize,里面有三个值分别表示长宽高,下面就来看看如何自定义转换器吧!

    val realSize: RealSize by animateValueAsState(
        RealSize(10.dp, 20.dp, 30.dp),
        TwoWayConverter(
            convertToVector = { size: RealSize ->
                // 从每个“Dp”字段中提取一个浮点值。
                AnimationVector3D(size.length.value, size.width.value, size.height.value)
            },
            convertFromVector = { vector: AnimationVector3D ->
                RealSize(vector.v1.dp, vector.v2.dp, vector.v3.dp)
            }
        )
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    是不是也不难,只需要将需要转换的值确定好,然后调用对用的 AnimationVectorXD 进行转换即可。

    结尾

    本篇文章主要带大家一起看一下 AnimationVector ,大部分使用的时候只关注如何使用,并没有看看里面的具体实现,Compose 虽好,可不要贪杯哦。简单的一些当然可以直接使用官方提供的,但如果复杂一些需要自定义的话还是看看比较好的。

    本文至此结束,有用的地方大家可以参考,当然如果能帮助到大家,哪怕是一点也足够了。就这样。

  • 相关阅读:
    文档翻译软件-大家都在使用的批量文档翻译软件
    列表初始化与右值引用
    npm install 报错
    Springboot毕设项目基于SpringBoot的图片网站f3yv9(java+VUE+Mybatis+Maven+Mysql)
    图书巨头Baker&Taylor遭勒索软件攻击 系统中断一周仍未恢复
    华为认证云计算专家(HCIE-Cloud Computing)–单选题
    小望电商通:无代码开发,轻松实现电商平台、客服系统和用户运营的集成
    总不能因为杯子碎了就不再喝水了吧
    Matlab 如何选择采样频率和信号长度
    SQL binary 轉float 絕對好用
  • 原文地址:https://blog.csdn.net/haojiagou/article/details/128158462