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() 背后帮你把这件事做了。

相关推荐
阿巴斯甜7 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker7 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95278 小时前
Andorid Google 登录接入文档
android
黄林晴10 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android