本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
上一篇《状态改变动画animateXxxAsState》介绍了 animateXxxAsState 系列动画 Api 的基本使用,但其中还有一个 animateValueAsState
的 Api 并没有介绍,实际上一篇介绍的所有 animateXxxAsState 系列动画 Api 最终内部调用的都是 animateValueAsState
,那么这一篇我们就来详细了解一下 animateValueAsState
的作用及其如何通过 animateValueAsState
来实现自定义 animateXxxAsState 动画 Api。
animateValueAsState 使用探索
上面说到其他 animateXxxAsState 系列 Api 最终都是调用 animateValueAsState
,那么我们就以 animateDpAsState
为例来看一下是不是这样,查看animateDpAsState
的源码:
@Composable
fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp> = dpDefaultSpring,
finishedListener: ((Dp) -> Unit)? = null
): State<Dp> {
return animateValueAsState(
targetValue,
Dp.VectorConverter,
animationSpec,
finishedListener = finishedListener
)
}
通过源码发现,animateDpAsState
的实现其实就是直接调用了animateValueAsState
。
实际上除了
animateFloatAsState
、animateColorAsState
做了额外处理后再调用animateValueAsState
外,其他 animateXxxAsState 的 Api 的实现都是直接调用animateValueAsState
,可查看对应源码详细了解
先看一下animateValueAsState
定义:
@Composable
fun <T, V : AnimationVector> animateValueAsState(
targetValue: T,
typeConverter: TwoWayConverter<T, V>,
animationSpec: AnimationSpec<T> = remember {
spring(visibilityThreshold = visibilityThreshold)
},
visibilityThreshold: T? = null,
finishedListener: ((T) -> Unit)? = null
): State<T>
对比 animateDpAsState
的参数发现多了两个参数:typeConverter
和 visibilityThreshold
,再看 animateDpAsState
中的调用 visibilityThreshold
参数是没传的,即使用的是默认值 null
,那么关键就在于 typeConverter
参数。
visibilityThreshold
为动画显示阈值,将在后续文章中进行详细介绍,本篇将不做过多阐述,请持续关注本专栏后续文章。
typeConverter
是一个 TwoWayConverter
类型,源码如下:
/**
* [TwoWayConverter] class contains the definition on how to convert from an arbitrary type [T]
* to a [AnimationVector], and convert the [AnimationVector] back to the type [T]. This allows
* animations to run on any type of objects, e.g. position, rectangle, color, etc.
*/
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
}
TwoWayConverter
是一个接口类型,有两个泛型 T
和 V
,其中T
为数据类型,V
为 AnimationVector
的子类。然后定义了两个方法:convertToVector
和 convertFromVector
,其作用是将数据类型与 AnimationVector
进行互相转换,即 T
和 V
互相转换。
继续往下看 AnimationVector
的源码:
sealed class AnimationVector {
// 重置
internal abstract fun reset()
// 创建新的实例
internal abstract fun newVector(): AnimationVector
// 通过 index 获取对应的值
internal abstract operator fun get(index: Int): Float
// 通过 index 设置对应的值
internal abstract operator fun set(index: Int, value: Float)
// 数据属性数量
internal abstract val size: Int
}
可以看出 AnimationVector
是一个密封类,提供了一些列的抽象方法和属性,具体可看上面对应注释。既然是个封闭类,那就看看其对应的实现类有哪些,通过 Ctrl/Command + Alt + B
进行查看:
发现只有四个实现类,分别是 AnimationVector1D
、AnimationVector2D
、AnimationVector3D
AnimationVector4D
,即一维、二维、三维、四维动画。
查看期中 AnimationVector1D
的源码:
class AnimationVector1D(initVal: Float) : AnimationVector() {
/**
* This field holds the only Float value in this [AnimationVector1D] object.
*/
var value: Float = initVal
internal set
// internal
override fun reset() {
value = 0f
}
override fun newVector(): AnimationVector1D = AnimationVector1D(0f)
override fun get(index: Int): Float {
if (index == 0) {
return value
} else {
return 0f
}
}
override fun set(index: Int, value: Float) {
if (index == 0) {
this.value = value
}
}
override val size: Int = 1
override fun toString(): String {
return "AnimationVector1D: value = $value"
}
override fun equals(other: Any?): Boolean =
other is AnimationVector1D && other.value == value
override fun hashCode(): Int = value.hashCode()
}
实现很简单,这里着重介绍一下 value 和 size,其对应的是动画的数据值和数据数量,因为一维动画只有一个数据,所以这里只定义了一个 value 属性,对应的 size 的值也为 1。那么如果是AnimationVector2D
、AnimationVector3D
AnimationVector4D
则对应分别有 2、3、4 个 value 属性,对应的 size 也应该分别是 2、3、4,通过查看源码发现也确实如此,这里就不贴对应的源码了,大家有兴趣可自行查看源码。
了解了 TwoWayConverter
和 AnimationVector
后我们再回到 animateDpAsState
的源码,在 animateDpAsState
中调用 animateValueAsState
时其中typeConverter
参数传入的是 Dp.VectorConverter
,来看一下 Dp.VectorConverter
的实现:
val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
get() = DpToVector
private val DpToVector: TwoWayConverter<Dp, AnimationVector1D> = TwoWayConverter(
convertToVector = { AnimationVector1D(it.value) },
convertFromVector = { Dp(it.value) }
)
Dp.VectorConverter
实际返回的是 DpToVector
,而 DpToVector
又是通过 TwoWayConverter
创建的,看到这里估计很多同学就纳闷了,不对呀,上面不是说 TwoWayConverter
是一个接口么,咋还能直接创建一个实例呢?我们点进去查看一下这里 TwoWayConverter
的源码:
fun <T, V : AnimationVector> TwoWayConverter(
convertToVector: (T) -> V,
convertFromVector: (V) -> T
): TwoWayConverter<T, V> = TwoWayConverterImpl(convertToVector, convertFromVector)
发现这里的 TwoWayConverter
并不是上面介绍的接口类型而是一个方法,方法返回的是 TwoWayConverterImpl
,而 TwoWayConverterImpl
才是上面说到的 TwoWayConverter
接口的实现类:
private class TwoWayConverterImpl<T, V : AnimationVector>(
override val convertToVector: (T) -> V,
override val convertFromVector: (V) -> T
) : TwoWayConverter<T, V>
实际上是将 TwoWayConverter
接口的两个方法抽成了两个参数传入进来,这样让使用时更加方便。
到这里我们就明白了为啥 DpToVector
是通过一个 TwoWayConverter
方法创建的了,其中转换为 Vector 是通过 AnimationVector1D(it.value)
创建一个 AnimationVector 实例,其中 it 即为 Dp 类型的数据实例;而将 Vector 转换为 Dp 则是直接通过 Dp(it.value)
创建一个 Dp 类型的实例,其中 it 为 AnimationVector 类型,即上面创建的 AnimationVector1D 对象,这样就完成了 Dp 与 AnimationVector1D 的互相转换。
其他 animateXxxAsState 的 Api 与 animateDpAsState 的实现方式基本类似,这里就不重复介绍了,感兴趣的同学可通过源码查看了解更多。
自定义 animateXxxAsState
了解了 animateDpAsState
是如何调用 animateValueAsState
实现的以后,下面我们就按照同样的思路和流程就可以来实现一个自定义的 animateXxxAsState
动画 Api 了。
上一篇我们实现改变组件大小时使用的是 animateSizeAsState
,因为 Modifier.size()
接收的是一个 DpSize
类型参数而不是 Size,所以并不能直接传入 Size 数据,而是需要进行转换后使用,上一篇的实现如下:
@Composable
fun SizeAnimationBox() {
var changeSize by remember { mutableStateOf(false) }
// 定义 Size 动画
val size by animateSizeAsState(if (changeSize) Size(200f, 50f) else Size(100f, 100f))
Box(Modifier
.padding(10.dp)
// 设置 Size 值
.size(size.width.dp, size.height.dp)
.background(Color.Blue)
.clickable {
changeSize = !changeSize
}
)
}
我们期望的是有一个animateDpSizeAsState
方法直接返回一个 State<DpSize>
数据,这样就能直接将其用于 Modifier.size()
参数而无需额外转换。
下面就来自定义这个 animateDpSizeAsState
Api,首先仿照上面的 Dp.VectorConverter
实现DpSize.VectorConverter
,如下:
val DpSize.Companion.VectorConverter: TwoWayConverter<DpSize, AnimationVector2D>
get() = DpSizeToVector
private val DpSizeToVector: TwoWayConverter<DpSize, AnimationVector2D> = TwoWayConverter(
// 创建 AnimationVector2D 分别传入 DpSize 的宽和高
convertToVector = { AnimationVector2D(it.width.value, it.height.value) },
// 创建 DpSize 将 AnimationVector2D 的 v1 和 v2 转换为 Dp 传入 DpSize 的宽高
convertFromVector = { DpSize(Dp(it.v1), Dp(it.v2)) }
)
因为 DpSize 有宽和高两个属性,所以这里需要使用 AnimationVector2D 来进行转换。
然后创建 animateDpSizeAsState
方法:
@Composable
fun animateDpSizeAsState(
targetValue: DpSize,
finishedListener: ((DpSize) -> Unit)? = null
): State<DpSize> {
return animateValueAsState(
targetValue,
DpSize.VectorConverter,
finishedListener = finishedListener
)
}
直接调用 animateValueAsState
传入上面定义的 DpSize.VectorConverter
即可,然后将之前的 animateSizeAsState
创建的动画换成使用 animateDpSizeAsState
,代码如下:
@Composable
fun DpSizeAnimationBox() {
var changeSize by remember { mutableStateOf(false) }
// 创建目标值的 DpSize 对象
val targetValue = if (changeSize) DpSize(200.dp, 50.dp) else DpSize(100.dp, 100.dp)
val size by animateDpSizeAsState(targetValue)
Box(Modifier
.padding(10.dp)
// 直接使用 animateDpSizeAsState 返回的 size
.size(size)
.background(Color.Blue)
.clickable {
changeSize = !changeSize
}
)
}
实现效果:
跟之前通过 animateSizeAsState
实现的效果一样,但是代码却简洁了很多,这就是自定义 animateXxxAsState 的好处。
实战
了解了自定义 animateXxxAsState
的实现,下面再将上一篇实现的上传按钮动画通过自定义 animateXxxAsState
的方式来实现。
关于上传按钮效果的详细实现思路可查看上一篇《状态改变动画animateXxxAsState》进行了解
上一篇我们在实现上传按钮效果时定义了 5 个变量,如下:
// 文字透明度
var textAlphaValue = 1f
// 按钮颜色
var backgroundColorValue = Color.Blue
// 按钮宽度
var boxWidthValue = originWidth
// 进度透明度
var progressAlphaValue = 0f
// 进度值
var progressValue = 0
通过上面对自定义 animateXxxAsState
的了解,我们需要创建 AnimationVector
,而 Compose 只给我们提供了最多 4 个数据属性的 AnimationVector4D
,但这里却有 5 个属性,那怎么办呢?
首先想到的是自定义一个继承自 AnimationVector
的 AnimationVector5D
类,但是 AnimationVector
是一个封闭类,不能在他自身 module 外继承使用,即不能在我们的项目中继承AnimationVector
,所以这条路行不通。
再来看上面的 5 个变量,其中 backgroundColorValue 是 Color 类型,而对于 Color 类型 Compose 本身提供了 Color.VectorConverter
用于 animateColorAsState
使用,其内部用到的 AnimationVector
是 AnimationVector4D
,源码如下:
val ColorToVector: (colorSpace: ColorSpace) -> TwoWayConverter<Color, AnimationVector4D>
所以这么算的话 Color 类型其实相当于是 4 个数据值,那上面 5 个变量如果要封装成一个实体类的话,相当于实际有 8 个数据值,但又没有 AnimationVector8D
又不能自己继承实现,那么就只能将 backgroundColor 单独提出来,然后把其他 4 个变量封装成一个实体类,如下:
data class UploadValue(val textAlpha : Float, val boxWidth : Dp, val progress:Int, val progressAlpha:Float){
companion object
}
然后创建对应的 VectorConverter
:
val UploadValue.Companion.VectorConverter: TwoWayConverter<UploadValue, AnimationVector4D>
get() = UploadToVector
private val UploadToVector: TwoWayConverter<UploadValue, AnimationVector4D> = TwoWayConverter(
convertToVector = { AnimationVector4D(it.textAlpha, it.boxWidth.value, it.progress.toFloat(), it.progressAlpha) },
convertFromVector = { UploadValue(it.v1, Dp(it.v2), it.v3.toInt(), it.v4) }
)
再创建 animateUploadAsState
方法:
@Composable
fun animateUploadAsState(
targetValue: UploadValue,
finishedListener: ((UploadValue) -> Unit)? = null
): State<UploadValue> {
return animateValueAsState(
targetValue,
UploadValue.VectorConverter,
finishedListener = finishedListener
)
}
最后将代码实现换成 animateUploadAsState
:
@Composable
fun UploadNewAnimation() {
val originWidth = 180.dp
val circleSize = 48.dp
var uploadState by remember { mutableStateOf(UploadState.Normal) }
var text by remember { mutableStateOf("Upload") }
// 根据状态创建 UploadValue 实体
val uploadValue = when (uploadState) {
UploadState.Normal -> UploadValue(1f, originWidth, 0, 0f)
UploadState.Start -> UploadValue(0f, circleSize, 0, 1f)
UploadState.Uploading -> UploadValue(0f, circleSize, 100, 1f)
UploadState.Success -> UploadValue(1f, originWidth, 100, 0f)
}
// 根据状态创建背景颜色
val backgroundColorValue = when (uploadState) {
UploadState.Normal -> Color.Blue
UploadState.Start -> Color.Gray
UploadState.Uploading -> Color.Gray
UploadState.Success -> Color.Red
}
// 创建 UploadValue 的 State
val upload by animateUploadAsState(uploadValue){
// 监听动画完成修改状态
if(uploadState == UploadState.Start){
uploadState = UploadState.Uploading
}else if(uploadState == UploadState.Uploading){
uploadState = UploadState.Success
text = "Success"
}
}
val backgroundColor by animateColorAsState(backgroundColorValue)
Box(
modifier = Modifier
.padding(start = 10.dp, top = 10.dp)
.width(originWidth),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(circleSize / 2))
.background(backgroundColor)
// 替换为使用 upload.boxWidth
.size(upload.boxWidth, circleSize)
.clickable {
uploadState = UploadState.Start
},
contentAlignment = Alignment.Center,
) {
Box(
// 替换为使用 upload.progress
modifier = Modifier.size(circleSize).clip(ArcShape(upload.progress))
// 替换为使用 upload.progressAlpha
.alpha(upload.progressAlpha).background(Color.Blue)
)
Box(
modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp))
// 替换为使用 upload.progressAlpha
.alpha(upload.progressAlpha).background(Color.White)
)
// 替换为使用 upload.textAlpha
Text(text, color = Color.White, modifier = Modifier.alpha(upload.textAlpha))
}
}
}
最终实现效果如下:
跟上一篇的实现效果一致,说明代码没问题。
最后
本篇通过 animateDpAsState
的源码分析对 animateValueAsState
的使用进行了探索,并详细介绍了如何通过 animateValueAsState
实现自定义 animateXxxAsState
动画 Api。关于 animateXxxAsState
的使用就只剩下了 animationSpec
参数的使用了,在下一篇将详细介绍 animationSpec
动画配置的使用,让我们的动画更加灵活,比如设置动画时长等,敬请期待。