Android Compose 动画使用详解(三)自定义animateXxxAsState动画

1,491 阅读8分钟

本文为稀土掘金技术社区首发签约文章,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

实际上除了animateFloatAsStateanimateColorAsState做了额外处理后再调用 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 的参数发现多了两个参数:typeConvertervisibilityThreshold,再看 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 是一个接口类型,有两个泛型 TV ,其中T为数据类型,VAnimationVector的子类。然后定义了两个方法:convertToVectorconvertFromVector,其作用是将数据类型与 AnimationVector进行互相转换,即 TV互相转换。

继续往下看 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进行查看:

发现只有四个实现类,分别是 AnimationVector1DAnimationVector2DAnimationVector3D 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。那么如果是AnimationVector2DAnimationVector3D AnimationVector4D则对应分别有 2、3、4 个 value 属性,对应的 size 也应该分别是 2、3、4,通过查看源码发现也确实如此,这里就不贴对应的源码了,大家有兴趣可自行查看源码。

了解了 TwoWayConverterAnimationVector后我们再回到 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()参数而无需额外转换。

下面就来自定义这个 animateDpSizeAsStateApi,首先仿照上面的 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 个属性,那怎么办呢?

首先想到的是自定义一个继承自 AnimationVectorAnimationVector5D类,但是 AnimationVector是一个封闭类,不能在他自身 module 外继承使用,即不能在我们的项目中继承AnimationVector,所以这条路行不通。

再来看上面的 5 个变量,其中 backgroundColorValue 是 Color 类型,而对于 Color 类型 Compose 本身提供了 Color.VectorConverter 用于 animateColorAsState 使用,其内部用到的 AnimationVectorAnimationVector4D,源码如下:

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动画配置的使用,让我们的动画更加灵活,比如设置动画时长等,敬请期待。