Jetpack Compose - Animatable与animateAsState (六)

1,204 阅读4分钟

Compose 能用属性动画吗?

不可以,至少目前没有方便使用属性动画的方式,传统view开发的时候用的是属性动画比较多,但是在Compose中 不存在属性动画的概念, 谷歌针对Compose的特性,开发了一套新的动画api

生硬的界面变化

来看下下面的代码,这个代码很简单 其实就是点击了以后 这个区域变大了而已,但是显然这是没有动画的, 效果比较生硬

setContent {

    var size by remember {
        mutableStateOf(50.dp)
    }

    Box(
        Modifier
            .size(size)
            .background(Color.Blue)
            .clickable {
                size = 100.dp
            }) {

    }

}

动画渐变大小

看下 下面的代码,主要看 size2的写法,size 就是上一小节的写法 主要用来对比用的

var change by mutableStateOf(false)

setContent {

    var size by remember {
        mutableStateOf(50.dp)
    }


    // MutableState 可以修改 而State 不能直接手动改
    val size2 by animateDpAsState(if (change) 100.dp else 50.dp)

    Row{
        Box(
            Modifier
                .size(size)
                .background(Color.Blue)
                .clickable {
                    size = 100.dp
                }) {

        }
        Box(
            Modifier
                .size(size2)
                .background(Color.Red)
                .clickable {
                    change = true
                }) {

        }
    }
}

有兴趣的可以自己跑一下代码 看看 两者之间效果的差别

要理解上面的写法 不然后面很容易蒙蔽

首先要注意 下面的返回值

image.png

他返回的是一个State 而不是mutableState

这两者什么区别?

image.png

1个是可变,一个是不可变, 反馈到我们代码层面,就是State 是一个只读的,mutableState是可读且可写的

所以这行代码我们再仔细品味一下 就可以知道啥意思了

image.png

**这个targetValue的数值的变化 最终会促使 animateDpAsState发生变化 **

类似的 我们可以看下 还有多少种animate的api

image.png

animate 动画的缺陷

其实这个动画还是很好用的,但是有个缺点,是他没办法 设置动画的初始值, 为啥? 因为你不管怎么设置 animate的参数, 他始终都代表一个state的渐变过程, 不管是50-100 还是100-50 他都是渐变过去的, 他是没办法直接从50秒到100的

你再回过头想想看我们传统view的属性动画,是不是可以方便的设置 初始值?

我们还可以跟进一下代码:

image.png

可以发现 我们的animate动画的实现 实际上是基于这个animatable的实现!

那么 animatable是不是可以支持 设置动画初始值的操作呢?(当然可以

Animatable 的 用法

看下面这段代码,其实运行起来的结果和前面一个小节的代码是一模一样的,只不过这里使用的是 Animatable 这个会比前面那个小节的写法要复杂许多

var change by mutableStateOf(false)
setContent {
    val size = remember(change) {
        if (change) 50.dp else 100.dp
    }
    val anim = remember { Animatable(size, Dp.VectorConverter) }
    LaunchedEffect(change) {
        anim.animateTo(size)
    }
    Row {

        Box(
            Modifier
                .size(anim.value)
                .background(Color.Red)
                .clickable {
                    change = !change
                }) {

        }
    }
}

下面我们来解释下Animatable的用法 要点

首先我们来看下 下面代码的意思

image.png

这里其实字面上很好理解就是 创建了一个默认值,也就是初始值,只是有人会觉得奇怪,这个VectroConvter是用来干啥的?

这里要注意,对于Animatable来说,他在底层运算的时候 其实都是Float,而我们的dp 类型显然不是Float 类型,所以 通常情况下 我们要实现 TwoWayConverter 这个接口

image.png

这里 系统默认给我们实现了一个,所以 我们直接用就行

AnimationVector1D 这个东西又是啥?

细看一下 竟然还有2d 3d 4d?

image.png

其实不难理解,这个就是动画计算的维度,我们这里只是宽高 那自然就是1维的,如果是坐标系 那自然就是2d的

再接着看:

image.png

这里是干啥的? 为啥要这么写?

其实你可以发现这个animateto 是一个suspend协程的方法 image.png

有人问 那不是可以直接lifecyclescpe。launch?

image.png

其实ide 也已经告诉你了, 在compose中 不要直接使用 lifecyclescope ,为啥?

因为直接使用 会导致 每次recompose的时候。这些协程 又走一遍,十分低效。

所以用 LaunchedEffect 来替代前者,可以方便的解决这个问题

如图所示: image.png

这个key 用来干啥?

key的意思就是 当这个key 发生变化的时候,这个block里面的协程 会执行, 如果key不变 则不执行(换句话说 只在初始化的时候执行一次)

很多时候 下面这种写法更为流行,代表就只在初始化的时候 执行一次

LaunchedEffect(Unit){
    
}

另外回到开始的问题,Animatable 如何设置动画的初始值?

LaunchedEffect(change) {
    anim.snapTo(size)
    //anim.animateTo(size)
}

关键就是snapTo方法,这个地方可以设置初始值,和下面的animateTo 其实不一样,下面的animateTo 是 动画执行,而snapTo 是直接执行,瞬间执行

用哪个更好?

能用animateAsState实现的 就用这个,因为简单,否则 就用Animatable