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

相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work1 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
ai2work1 天前
ch21 签名、校验与发版
kotlin
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui