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 个实现子类:AnimationVector1D
、AnimationVector2D
、AnimationVector3D
、AnimationVector4D
开发者应根据动画的数据类型所具有的维度来选择不同的 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 是怎么知道如何转换 Dp
和 AnimationVector
的?其实不是没有,我们不用写是因为 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()
背后帮你把这件事做了。