Android Compose 动画使用详解(八)Animatable的使用

语言: CN / TW / HK

theme: smartblue

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

前言

前面介绍了 Compose 的 animateXxxAsState动画 Api 的使用,以及如何通过 animateValueAsState实现自定义 animateXxxAsState动画 Api ,如何对动画进行详细配置从而达到灵活的实现各种动画效果。

本篇将为大家介绍更底层的动画 Api :Animatable

Animatable

在前面介绍 animateXxxAsState的时候我们跟踪源码发现其内部调用的是 animateValueAsState,那么 animateValueAsState 内部又是怎么实现的呢?来看看 animateValueAsState 的源码:

```kotlin fun animateValueAsState( targetValue: T, typeConverter: TwoWayConverter, animationSpec: AnimationSpec = remember { spring(visibilityThreshold = visibilityThreshold) }, visibilityThreshold: T? = null, finishedListener: ((T) -> Unit)? = null ): State {

val animatable = remember { Animatable(targetValue, typeConverter) }
val listener by rememberUpdatedState(finishedListener)
val animSpec by rememberUpdatedState(animationSpec)
val channel = remember { Channel<T>(Channel.CONFLATED) }
SideEffect {
    channel.trySend(targetValue)
}
LaunchedEffect(channel) {
    for (target in channel) {
        val newTarget = channel.tryReceive().getOrNull() ?: target
        launch {
            if (newTarget != animatable.targetValue) {
                animatable.animateTo(newTarget, animSpec)
                listener?.invoke(animatable.value)
            }
        }
    }
}
return animatable.asState()

} ```

可以发现,animateValueAsState 的内部其实就是通过 Animatable 来实现的。实际上 animateValueAsState 是对 Animatable 的上层使用封装,而 animateXxxAsState 又是对 animateValueAsState 的上层使用封装,所以 Animatable 是更底层的动画 api。

下面就来看一下如何使用 Animatable实现动画效果。首先还是通过其构造方法定义了解创建 Animatable需要哪些参数以及各个参数的含义,构造方法定义如下:

kotlin class Animatable<T, V : AnimationVector>( initialValue: T, val typeConverter: TwoWayConverter<T, V>, private val visibilityThreshold: T? = null )

构造方法有三个参数,参数解析如下:

  • initialValue:动画初始值,类型是泛型,即动画作用的数值类型,如 Float、Dp 等
  • typeConverter:类型转换器,类型是 TwoWayConverter,在 《自定义animateXxxAsState动画》一文中我们对其进行了详细介绍,作用是将动画的数值类型与 AnimationVector进行互相转换。
  • visibilityThreshold:可视阈值,即动画数值达到设置的值时瞬间到达目标值停止动画,可选参数,默认值为 null

了解了构造方法和参数,下面就来看一下怎么创建一个 Animatable,假设我们要对一个 float 类型的数据做动画,那么 initialValue就应该传入 float 的数值,那typeConverter传啥呢?要去自定义实现 TwoWayConverter么?大多数情况下并不用,因为 Compose 为我们提供了常用类型的转换器,如下:

```kotlin // package : androidx.compose.animation.core.VectorConverters

// Float 类型转换器 val Float.Companion.VectorConverter: TwoWayConverter

// Int 类型转换器 val Int.Companion.VectorConverter: TwoWayConverter

// Rect 类型转换器 val Rect.Companion.VectorConverter: TwoWayConverter

// Dp 类型转换器 val Dp.Companion.VectorConverter: TwoWayConverter

// DpOffset 类型转换器 val DpOffset.Companion.VectorConverter: TwoWayConverter

// Size 类型转换器 val Size.Companion.VectorConverter: TwoWayConverter

// Offset 类型转换器 val Offset.Companion.VectorConverter: TwoWayConverter

// IntOffset 类型转换器 val IntOffset.Companion.VectorConverter: TwoWayConverter

// IntSize 类型转换器 val IntSize.Companion.VectorConverter: TwoWayConverter

// package : androidx.compose.animation.ColorVectorConverter // Color 类型转换器 val Color.Companion.VectorConverter: (colorSpace: ColorSpace) -> TwoWayConverter ```

注意: Color 的转换器与其他转换器不是在同一个包下。

我们要作用于 Float 类型时就可以直接使用 Float.VectorConverter即可,代码如下:

kotlin val animatable = remember { Animatable(100f, Float.VectorConverter) }

在 Compose 函数里创建 Animatable 对象时必须使用 remember进行包裹,否则会报错。

创建其他数值类型的动画则传对应的 VectorConverter即可,如 Dp、Size、Color,创建代码如下:

kotlin val animatable1 = remember { Animatable(100.dp, Dp.VectorConverter) } val animatable2 = remember { Animatable(Size(100f, 100f), Size.VectorConverter) } val animatable3 = remember { Animatable(Color.Blue, Color.VectorConverter(Color.Blue.colorSpace)) }

除此之外,Compose 还为 Float 和 Color 提供了简便方法 Animatable,只需要传入初始值即可,使用如下:

```kotlin // Float 简便方法使用 import androidx.compose.animation.core.Animatable

val animatableFloat = remember { Animatable(100f) }

// Color 简便方法使用 import androidx.compose.animation.Animatable

val animatable5 = remember { Animatable(Color.Blue) } ```

需要注意的是虽然都是叫 Animatable,但是引入的包是不一样的,且这里的 Animatable 不是构造函数而是一个方法,在方法的实现里再调用的 Animatable 构造函数创建真正的 Animatable实例,源码分别如下:

Animatable(Float)

```kotlin package androidx.compose.animation.core

fun Animatable( initialValue: Float, visibilityThreshold: Float = Spring.DefaultDisplacementThreshold ) = Animatable( initialValue, Float.VectorConverter, visibilityThreshold ) ```

Animatable(Color) :

```kotlin package androidx.compose.animation

fun Animatable(initialValue: Color): Animatable = Animatable(initialValue, (Color.VectorConverter)(initialValue.colorSpace)) ```

Animatable创建好后下面看看怎么触发动画执行。

animateTo

Animatable提供了一个 animateTo方法用于触发动画执行,看看这个方法的定义:

kotlin suspend fun animateTo( targetValue: T, animationSpec: AnimationSpec<T> = defaultSpringSpec, initialVelocity: T = velocity, block: (Animatable<T, V>.() -> Unit)? = null ): AnimationResult<T, V>

首先animateTo 方法是用 suspend修饰的,即只能在协程中调用;其次方法有四个参数,对应解析如下:

  • targetValue:动画目标值
  • animationSpec:动画规格配置,这个前面几篇文件进行了详细介绍,共有 6 种动画规格可进行设置
  • initialVelocity:初始速度
  • block:函数类型参数,动画运行的每一帧都会回调这个 block 方法,可用于动画监听

最后返回值为 AnimationResult类型,包含动画结束时的状态和原因。

执行动画

我们还是以前面文章熟悉的方块移动动画来看一下 animateTo的使用效果,代码如下:

```kotlin // 创建状态 通过状态驱动动画 var moveToRight by remember { mutableStateOf(false) } // 动画实例 val animatable = remember { Animatable(10.dp, Dp.VectorConverter) }

// animateTo 是 suspend 方法,所以需要在协程中进行调用 LaunchedEffect(moveToRight) { // 根据状态确定动画移动的目标值 animatable.animateTo(if (moveToRight) 200.dp else 10.dp) } Box( Modifier // 使用动画值 .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { // 修改状态 moveToRight = !moveToRight } ) ```

animateTo 需要在协程中进行调用,这里使用的是 LaunchedEffect来开启协程,他是 Compose 提供的专用协程开启方法,其特点是不会在每次 UI 重组时都重新启动协程,只会在 LaunchedEffect 参数发生变化时才会重新启动协程执行协程中的代码。

因为本篇主要介绍 Compose 动画的使用,关于 Compose 协程相关内容这里就不做过多赘述,有兴趣的同学可自行查阅相关资料。

看一下运行效果:

除了通过状态触发 animateTo 外,也可以直接在按钮事件中触发,代码如下:

```kotlin val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } // 获取协程作用域 val scope = rememberCoroutineScope()

Box( Modifier .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { // 开启协程 scope.launch { // 执行动画 animatable.animateTo(200.dp) } } ) ```

因为 LaunchedEffect 只能在 Compose 函数中使用,而点击事件并不是 Compose 函数,所以这里需要使用 rememberCoroutineScope()获取协程作用域后再用其启动协程。

效果如下:

动画监听

animateTo的最后一个参数是一个函数类型 (Animatable<T, V>.() -> Unit)?,可以用来对动画进行监听,在回调方法里可以通过 this 获取到当前动画 Animatable的实例,通过其可以获取到动画当前时刻的值、目标值、速度等。使用如下:

kotlin animatable.animateTo(200.dp){ // 动画当前值 val value = this.value // 当前速度 val velocity = this.velocity // 动画目标值 val targetValue = this.targetValue }

可以通过监听动画实现界面的联动操作,比如让另一个组件跟随动画组件一起运动等。

返回结果

animateTo方法是有返回结果的,类型为AnimationResult,通过返回结果可以获取到动画结束时的状态和原因,AnimationResult 源码如下:

```kotlin class AnimationResult(

// 结束状态
val endState: AnimationState<T, V>,

// 结束原因
val endReason: AnimationEndReason

) ```

只有两个属性 endStateendReason分别代表动画结束时的状态和原因

endStateAnimationState类型,通过其可以获取到动画结束时的值、速度、时间等数据,源码定义如下:

```kotlin class AnimationState( val typeConverter: TwoWayConverter, initialValue: T, initialVelocityVector: V? = null, lastFrameTimeNanos: Long = AnimationConstants.UnspecifiedTime, finishedTimeNanos: Long = AnimationConstants.UnspecifiedTime, isRunning: Boolean = false ) : State {

// 动画值
override var value: T by mutableStateOf(initialValue)
    internal set

// 动画速度矢量
var velocityVector: V =
    initialVelocityVector?.copy() ?: typeConverter.createZeroVectorFrom(initialValue)
    internal set

// 最后一帧时间(纳秒)
@get:Suppress("MethodNameUnits")
var lastFrameTimeNanos: Long = lastFrameTimeNanos
    internal set

// 结束时的时间(纳秒)
@get:Suppress("MethodNameUnits")
var finishedTimeNanos: Long = finishedTimeNanos
    internal set

// 是否正在运行
var isRunning: Boolean = isRunning
    internal set

// 动画速度
val velocity: T
    get() = typeConverter.convertFromVector(velocityVector)

} ```

注意这里的 lastFrameTimeNanosfinishedTimeNanos是基于 System.nanoTime获取到的纳秒值,不是系统时间。

endReason是一个 AnimationEndReason类型的枚举,只有两个枚举值:

```kotlin enum class AnimationEndReason {

// 动画运行到边界时停止结束
BoundReached,

// 动画正常结束
Finished

} ```

Finished很好理解,就是动画正常执行完成;那 BoundReached到达边界停止是什么意思呢?Animatable是可以为动画设置边界的,当动画运行到边界时就会立即停止,此时返回结果的停止原因就是 BoundReached,关于动画的边界设置以及动画停止的更多内容会在后续文章中进行详细介绍。

那么返回值在哪些情况下会用到呢?比如一个动画被打断时另一个动画需要依赖上一个动画的值、速度等继续执行,或者动画遇到边界停止时需要重新进行动画此时就可以通过上一个动画的返回值获取到需要的数据后进行相关处理。

snapTo

除了 animateToAnimatable还提供了 snapTo执行动画,看到 snapTo我们自然想到了前面介绍动画配置时的快闪动画 SnapSpec,即动画时长为 0 瞬间执行完成,snapTo也是同样的作用,可以让动画瞬间达到目标值,方法定义如下:

kotlin suspend fun snapTo(targetValue: T)

同样是一个被 suspend修饰的挂起函数,即必须在协程里执行;参数只有一个 targetValue即目标值,使用如下:

```kotlin val animatable = remember { Animatable(10.dp, Dp.VectorConverter) } val scope = rememberCoroutineScope()

Box( Modifier .padding(start = animatable.value, top = 30.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { scope.launch { // 通过 snapTo 瞬间到达目标值位置 animatable.snapTo(200.dp) } } ) ```

效果如下:

通过 snapTo 我们可以实现先让动画瞬间达到某个值,再继续执行后面的动画,比如上面的动画我们可以通过 snapTo让方块瞬间到 100.dp 位置然后使用 animateTo 动画到 200.dp,代码如下:

kotlin scope.launch { // 先瞬间到达 100.dp animatable.snapTo(100.dp) // 再从 100.dp 动画到 200.dp animatable.animateTo(200.dp, animationSpec = tween(1000)) }

动画效果:

实战

《Android Compose 动画使用详解(三)自定义animateXxxAsState动画》一文中我们通过 animateValueAsState自定义 animateUploadAsState实现了上传按钮的动画,现在我们看看如何通过 Animatable自定义实现同样的动画效果。

关于上传按钮动画的实现原理可查看 《Android Compose 动画使用详解(二)状态改变动画animateXxxAsState》一文的详细介绍。

首先自定义 UploadData实体类:

kotlin data class UploadData( val backgroundColor: Color, val textAlpha: Float, val boxWidth: Dp, val progress: Int, val progressAlpha: Float )

然后自定义 animateUploadAsStateapi:

```kotlin @Composable fun animateUploadAsState( // 上传按钮动画数据 value: UploadData, // 状态 state: Any, ): UploadData {

// 创建对应值的 Animatable 实例
val bgColorAnimatable = remember {
    Animatable(
        value.backgroundColor,
        Color.VectorConverter(value.backgroundColor.colorSpace)
    )
}
val textAlphaAnimatable = remember { Animatable(value.textAlpha) }
val boxWidthAnimatable = remember { Animatable(value.boxWidth, Dp.VectorConverter) }
val progressAnimatable = remember { Animatable(value.progress, Int.VectorConverter) }
val progressAlphaAnimatable = remember { Animatable(value.progressAlpha) }

// 当状态改变时在协程里分别执行 animateTo
LaunchedEffect(state) {
    bgColorAnimatable.animateTo(value.backgroundColor)
}
LaunchedEffect(state) {
    textAlphaAnimatable.animateTo(value.textAlpha)
}
LaunchedEffect(state) {
    boxWidthAnimatable.animateTo(value.boxWidth)
}
LaunchedEffect(state) {
    progressAnimatable.animateTo(value.progress)
}
LaunchedEffect(state) {
    progressAlphaAnimatable.animateTo(value.progressAlpha)
}

// 返回最新数据
return UploadData(
    bgColorAnimatable.value,
    textAlphaAnimatable.value,
    boxWidthAnimatable.value,
    progressAnimatable.value,
    progressAlphaAnimatable.value
)

} ```

使用:

```kotlin val originWidth = 180.dp val circleSize = 48.dp // 上传状态 var uploadState by remember { mutableStateOf(UploadState.Normal) } // 按钮文字 var text by remember { mutableStateOf("Upload") }

// 根据状态创建目标动画数据 val uploadValue = when (uploadState) { UploadState.Normal -> UploadData(Color.Blue, 1f, originWidth, 0, 0f) UploadState.Start -> UploadData(Color.Gray, 0f, circleSize, 0, 1f) UploadState.Uploading -> UploadData(Color.Gray, 0f, circleSize, 100, 1f) UploadState.Success -> UploadData(Color.Red, 1f, originWidth, 100, 0f) }

// 通过自定义api创建动画 val upload = animateUploadAsState(uploadValue, uploadState)

Column { // 按钮布局 Box( modifier = Modifier .padding(start = 10.dp, top = 20.dp) .width(originWidth), contentAlignment = Alignment.Center ) { Box( modifier = Modifier .clip(RoundedCornerShape(circleSize / 2)) .background(upload.backgroundColor) .size(upload.boxWidth, circleSize), contentAlignment = Alignment.Center, ) { Box( modifier = Modifier.size(circleSize).clip(ArcShape(upload.progress)) .alpha(upload.progressAlpha).background(Color.Blue) ) Box( modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp)) .alpha(upload.progressAlpha).background(Color.White) ) Text(text, color = Color.White, modifier = Modifier.alpha(upload.textAlpha)) } }

// 辅助按钮,用于模拟上传状态的改变
Button(onClick = {
    when (uploadState) {
        UploadState.Normal -> {
            uploadState = UploadState.Start
        }
        UploadState.Start -> {
            uploadState = UploadState.Uploading
        }
        UploadState.Uploading -> {
            uploadState = UploadState.Success
            text = "Success"
        }
        UploadState.Success -> {
            uploadState = UploadState.Normal
        }
    }
}, modifier = Modifier.padding(start = 10.dp, top = 20.dp)) {
    Text("改变上传状态")
}

} ```

运行效果如下:

最后

本篇介绍了更底层动画 api Animatable的创建以及 animateTosnapTo 的使用,并通过一个简单的实战实例完成了如何通过 Animatable 实现自定义动画完成与 animateXxxAsState 同样的效果。除此之外 Animatable 还有 animateDecayapi 、边界值的设置以及停止动画等,由于篇幅问题我们将在后续文章中进行详细介绍,请持续关注本专栏了解更多 Compose 动画内容。