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

相关推荐
编程乐学(Arfan开发工程师)5 小时前
06、基础入门-SpringBoot-依赖管理特性
android·spring boot·后端
androidwork5 小时前
使用 Kotlin 和 Jetpack Compose 开发 Wear OS 应用的完整指南
android·kotlin
_龙小鱼_5 小时前
Kotlin变量与数据类型详解
开发语言·微信·kotlin
繁依Fanyi6 小时前
Animaster:一次由 CodeBuddy 主导的 CSS 动画编辑器诞生记
android·前端·css·编辑器·codebuddy首席试玩官
奔跑吧 android8 小时前
【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
android·bluetooth·bt·aosp13·storagemodule
田一一一12 小时前
Android framework 中间件开发(三)
android·中间件·framework·jni
androidwork16 小时前
掌握 Kotlin Android 单元测试:MockK 框架深度实践指南
android·kotlin
田一一一17 小时前
Android framework 中间件开发(二)
android·中间件·framework
追随远方17 小时前
FFmpeg在Android开发中的核心价值是什么?
android·ffmpeg
神探阿航18 小时前
HNUST湖南科技大学-安卓Android期中复习
android·安卓·hnust