Jetpack Compose 动画 —— animate**AsState

animate**AsState

1. animate**AsState

在传统 Android View 系统中,要对 View 的通用属性做动画,可以利用 ViewPropertyAnimator:

kotlin 复制代码
view.animate()
	.alpha(0.5f)
    .rotation(90f)
	.start()

而对于其他属性,可以使用 ObjectAnimator:

kotlin 复制代码
ObjectAnimator
	.ofFloat(circleView, "radius", 50f, 100f)
	.start()

可以看出传统 View 的属性动画过程,就是在一段时间内持续调用 view 的 setXXX 函数,以此来刷新界面显示动画,这个过程在不断地操纵 view 对象。

而由于 Compose 是声明式 UI 框架,是没有办法获取到界面元素对象的。更新界面的唯一方法是通过新参数调用同一可组合项,这些参数是界面状态的表现形式。

kotlin 复制代码
setContent {
    var size by remember { mutableStateOf(100.dp) }
    Box(
        modifier =
            Modifier.size(size)
                .clip(RoundedCornerShape(50))
                .background(color = MaterialTheme.colorScheme.primary)
                .clickable { size = 200.dp }
    )
}

提到 Compose 更新界面,每个人都很容易写出上面的代码。可是现在要做的是动画,我们想要不是瞬间完成的界面刷新,而是在一段时间内不断刷新至目标值的界面刷新。

所以这里不能用 remember + mutableStateOf 来创建 size 这个 State 对象,我们要使用 animateDpAsState。因为 animateDpAsState 返回的本来就是一个 State 对象,所以不需要用 mutableStateOf 再次包裹它,同时因为其内部会自动用 remember 包装,所以我们写的时候也无需手动包装一层 remember 函数。

kotlin 复制代码
setContent {
    var size by animateDpAsState(100.dp)
    Box(
        modifier =
            Modifier.size(size)
                .clip(RoundedCornerShape(50))
                .background(color = MaterialTheme.colorScheme.primary)
                .clickable { size = 200.dp }
    )
}

但上面的代码还是有问题的,IDE 会自动报错提示:

perl 复制代码
Type 'State<Dp>' has no method 'setValue(Nothing?, KProperty<*>, Dp)' and thus it cannot serve as a delegate for var (read-write property)

意思是说 animateDpAsState 函数返回的对象不能代理变量的 set 操作,唯一的办法就是将 size 声明为不可变对象:

kotlin 复制代码
val size by animateDpAsState(100.dp)

...... 做动画肯定要改变状态的值吧,现在状态声明为不可变,那怎么改?Compose 团队将 animateDpAsState 返回值设计成 State 而不是 MutableState,很明显就是不想开发者去操纵这个 State 对象赋予新的动画 target 值。动画 target value 是通过 animateDpAsState 函数参数来设置的:

kotlin 复制代码
setContent {
    var isSmall by remember { mutableStateOf(true) }
    val size by animateDpAsState(targetValue = if (isSmall) 100.dp else 200.dp)
    Box(
        modifier =
            Modifier.size(size)
                .clip(RoundedCornerShape(50))
                .background(color = MaterialTheme.colorScheme.primary)
                .clickable { isSmall = !isSmall }
    )
}

为了给 animateDpAsState 传递不同的 targetValue(100.dp 和 200.dp:100.dp 是 small 状态下的值,200.dp 是非 small 状态下的值),所以还需要新建一个状态变量 isSmall。这么写的好处是:动画的 targetValue 与状态是挂钩的,触发动画时,不需要关心动画具体 target value 是什么,点击事件产生后只需专注于状态本身的改变。这种设计是一种思路上的改变,在上面的例子里,对于事件产生处 .clickable { ... },它脑子里只有一件事:点击切换大小状态,如果现在大就切换到小,如果现在小就切换到大。大的尺寸和小的尺寸?我不知道啊,不归我管吧。

2. animateValueAsState

前面只提到 animateDpAsState,其实还有很多孪生函数,用法都是一样的

也就是说,Dp、Color、Int、Float、Rect...等等类型,我们都能使用 animate*AsState 轻松创建动画,那如果自定义类型呢?如果想让字符 Char 根据 ASCII 值的改变进行动画(从 A 到 Z),在传统 View 里面,可以使用 ObjectAnimator.ofObject() + 自定义 TypeEvaluator

Compose 里面虽然没有 animateCharAsState 或 animateAsciiAsState,不过我们可以用 animateValueAsState

kotlin 复制代码
setContent {
    var start by remember { mutableStateOf(true) }
    val char by animateValueAsState(
        targetValue = if (start) 'A' else 'Z',
        typeConverter = /* need a type converter here */
    )

    Text(
        text = char.toString(),
        modifier = Modifier.clickable { start = !start },
        fontSize = MaterialTheme.typography.displayLarge.fontSize
    )
}

与其他几个 animate**AsState 函数不同,除了 targetValue,animateValueAsState 还要求必须填入一个TwoWayConverter 类型的 type converter

kotlin 复制代码
@Composable
fun <T, V : AnimationVector> animateValueAsState(
    targetValue: T,
    typeConverter: TwoWayConverter<T, V>,
    ...
): State<T>

interface TwoWayConverter<T, V : AnimationVector> {
    /**
     * Defines how a type [T] should be converted to a Vector type (i.e. [AnimationVector1D],
     * [AnimationVector2D], [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of
     * type T).
     */
    val convertToVector: (T) -> V
    /**
     * Defines how to convert a Vector type (i.e. [AnimationVector1D], [AnimationVector2D],
     * [AnimationVector3D] or [AnimationVector4D], depends on the dimensions of type T) back to type
     * [T].
     */
    val convertFromVector: (V) -> T
}

ObjectAnimator 使用的 TypeEvaluator 类似,TwoWayConverter 用于数据类型与进度之间的双向转换

从源码及注释中也能看出来,AnimationVector 是个密封类,共有 4 个实现子类:AnimationVector1DAnimationVector2DAnimationVector3DAnimationVector4D

开发者应根据动画的数据类型所具有的维度来选择不同的 AnimationVector,我们现在要对字符 Char 做动画,具体到数值就是 ASCII,1 个 Char 对应 1 个 ASCII 值,也就是说动画数据类型 Char 的维度是 1。

kotlin 复制代码
val Char.Companion.VectorConverter: TwoWayConverter<Char, AnimationVector1D>
    get() = object : TwoWayConverter<Char, AnimationVector1D> {
        override val convertFromVector: (AnimationVector1D) -> Char
            get() = { vector -> Char(code = vector.value.toInt()) }
        override val convertToVector: (Char) -> AnimationVector1D
            get() = { char -> AnimationVector1D(char.code.toFloat()) }
    }

setContent {
    var start by remember { mutableStateOf(true) }
    val char by animateValueAsState(
        targetValue = if (start) 'A' else 'Z',
        Char.VectorConverter
    )

    Text(
        text = char.toString(),
        modifier = Modifier.clickable { start = !start },
        fontSize = MaterialTheme.typography.displayLarge.fontSize
    )
}

使用 animateDpAsState 的时候,为什么并不需要写一个 typeConverter: TwoWayConverter?没有 typeConverter,Compose 是怎么知道如何转换 DpAnimationVector 的?其实不是没有,我们不用写是因为 Google 已经帮我们写好了:

kotlin 复制代码
@Composable
fun animateDpAsState(
    targetValue: Dp,
    animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
    label: String = "DpAnimation",
    finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
    return animateValueAsState(
        targetValue,
        Dp.VectorConverter,  <----------------------
        animationSpec,
        label = label,
        finishedListener = finishedListener
    )
}

val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
    get() = DpToVector

private val DpToVector: TwoWayConverter<Dp, AnimationVector1D> = TwoWayConverter(
    convertToVector = { AnimationVector1D(it.value) },
    convertFromVector = { Dp(it.value) }
)

这就好比 ObjectAnimator.ofArgb(),并不需要自己传递一个 ArgbEvaluator,只是因为 .ofArgb() 背后帮你把这件事做了。

相关推荐
用户2018792831671 小时前
如何利用AI工具快速学习Android源码
android
音视频牛哥2 小时前
Android 平台RTSP/RTMP播放器SDK接入说明
android·音视频·大牛直播sdk·rtsp播放器·rtmp播放器·rtmp低延迟播放·rtmpplayer
aningxiaoxixi3 小时前
Android Framework 之 AudioDeviceBroker
android·windows·ffmpeg
~Yogi3 小时前
今日学习:工程问题(场景题)
android·学习
奔跑吧 android4 小时前
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
android·bluetooth·bt·aosp13
移动开发者1号4 小时前
Android Activity状态保存方法
android·kotlin
移动开发者1号4 小时前
Volley源码深度分析与设计亮点
android·kotlin
张风捷特烈4 小时前
每日一题 Flutter#7,8 | 关于 State 两道简答题
android·flutter·面试
计蒙不吃鱼12 小时前
一篇文章实现Android图片拼接并保存至相册
android·java·前端
LucianaiB13 小时前
如何做好一份优秀的技术文档:专业指南与最佳实践
android·java·数据库