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

前语

前面介绍了 Compose 的 animateXxxAsState动画 Api 的运用,以及怎么经过 animateValueAsState完结自界说 animateXxxAsState动画 Api ,怎么对动画进行具体装备从而抵达灵活的完结各种动画作用。

本篇将为咱们介绍更底层的动画 Api :Animatable

Animatable

在前面介绍 animateXxxAsState的时分咱们盯梢源码发现其内部调用的是 animateValueAsState,那么 animateValueAsState 内部又是怎样完结的呢?来看看 animateValueAsState 的源码:

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> {
    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需求哪些参数以及各个参数的含义,结构办法界说如下:

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 为咱们供给了常用类型的转换器,如下:

// package : androidx.compose.animation.core.VectorConverters
// Float 类型转换器
val Float.Companion.VectorConverter: TwoWayConverter<Float, AnimationVector1D>
// Int 类型转换器
val Int.Companion.VectorConverter: TwoWayConverter<Int, AnimationVector1D>
// Rect 类型转换器
val Rect.Companion.VectorConverter: TwoWayConverter<Rect, AnimationVector4D>
// Dp 类型转换器
val Dp.Companion.VectorConverter: TwoWayConverter<Dp, AnimationVector1D>
// DpOffset 类型转换器
val DpOffset.Companion.VectorConverter: TwoWayConverter<DpOffset, AnimationVector2D>
// Size 类型转换器
val Size.Companion.VectorConverter: TwoWayConverter<Size, AnimationVector2D>
// Offset 类型转换器
val Offset.Companion.VectorConverter: TwoWayConverter<Offset, AnimationVector2D>
// IntOffset 类型转换器
val IntOffset.Companion.VectorConverter: TwoWayConverter<IntOffset, AnimationVector2D>
// IntSize 类型转换器
val IntSize.Companion.VectorConverter: TwoWayConverter<IntSize, AnimationVector2D>
// package : androidx.compose.animation.ColorVectorConverter 
// Color 类型转换器
val Color.Companion.VectorConverter: (colorSpace: ColorSpace) -> TwoWayConverter<Color, AnimationVector4D>

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

咱们要作用于 Float 类型时就能够直接运用 Float.VectorConverter即可,代码如下:

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

在 Compose 函数里创立 Animatable 方针时有必要运用 remember进行包裹,不然会报错。

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

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,只需求传入初始值即可,运用如下:

// 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)

package androidx.compose.animation.core
fun Animatable(
    initialValue: Float,
    visibilityThreshold: Float = Spring.DefaultDisplacementThreshold
) = Animatable(
    initialValue,
    Float.VectorConverter,
    visibilityThreshold
)

Animatable(Color) :

package androidx.compose.animation
fun Animatable(initialValue: Color): Animatable<Color, AnimationVector4D> =
    Animatable(initialValue, (Color.VectorConverter)(initialValue.colorSpace))

Animatable创立好后下面看看怎样触发动画履行。

animateTo

Animatable供给了一个 animateTo办法用于触发动画履行,看看这个办法的界说:

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的运用作用,代码如下:

// 创立状况 经过状况驱动动画
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 协程相关内容这儿就不做过多赘述,有兴趣的同学可自行查阅相关资料。

看一下运转作用:

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

除了经过状况触发 animateTo 外,也能够直接在按钮事情中触发,代码如下:

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()获取协程作用域后再用其启动协程。

作用如下:

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

动画监听

animateTo的最终一个参数是一个函数类型 (Animatable<T, V>.() -> Unit)?,能够用来对动画进行监听,在回调办法里能够经过 this 获取到当时动画 Animatable的实例,经过其能够获取到动画当时时刻的值、方针值、速度等。运用如下:

animatable.animateTo(200.dp){
    // 动画当时值
    val value = this.value
    // 当时速度
    val velocity = this.velocity
    // 动画方针值
    val targetValue = this.targetValue
}

能够经过监听动画完结界面的联动操作,比方让另一个组件跟从动画组件一同运动等。

回来成果

animateTo办法是有回来成果的,类型为AnimationResult,经过回来成果能够获取到动画完毕时的状况和原因,AnimationResult 源码如下:

class AnimationResult<T, V : AnimationVector>(
    // 完毕状况
    val endState: AnimationState<T, V>,
    // 完毕原因
    val endReason: AnimationEndReason
)

只有两个特点 endStateendReason别离代表动画完毕时的状况和原因

endStateAnimationState类型,经过其能够获取到动画完毕时的值、速度、时间等数据,源码界说如下:

class AnimationState<T, V : AnimationVector>(
    val typeConverter: TwoWayConverter<T, V>,
    initialValue: T,
    initialVelocityVector: V? = null,
    lastFrameTimeNanos: Long = AnimationConstants.UnspecifiedTime,
    finishedTimeNanos: Long = AnimationConstants.UnspecifiedTime,
    isRunning: Boolean = false
) : State<T> {
	// 动画值
    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类型的枚举,只有两个枚举值:

enum class AnimationEndReason {
    // 动画运转到鸿沟时中止完毕
    BoundReached,
    // 动画正常完毕
    Finished
}

Finished很好理解,便是动画正常履行完结;那 BoundReached抵达鸿沟中止是什么意思呢?Animatable是能够为动画设置鸿沟的,当动画运转到鸿沟时就会立即中止,此刻回来成果的中止原因便是 BoundReached,关于动画的鸿沟设置以及动画中止的更多内容会在后续文章中进行具体介绍。

那么回来值在哪些情况下会用到呢?比方一个动画被打断时另一个动画需求依靠上一个动画的值、速度等持续履行,或许动画遇到鸿沟中止时需求从头进行动画此刻就能够经过上一个动画的回来值获取到需求的数据后进行相关处理。

snapTo

除了 animateToAnimatable还供给了 snapTo履行动画,看到 snapTo咱们天然想到了前面介绍动画装备时的快闪动画 SnapSpec,即动画时长为 0 瞬间履行完结,snapTo也是相同的作用,能够让动画瞬间抵达方针值,办法界说如下:

suspend fun snapTo(targetValue: T)

相同是一个被 suspend润饰的挂起函数,即有必要在协程里履行;参数只有一个 targetValue即方针值,运用如下:

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)
            }
        }
)

作用如下:

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

经过 snapTo 咱们能够完结先让动画瞬间抵达某个值,再持续履行后边的动画,比方上面的动画咱们能够经过 snapTo让方块瞬间到 100.dp 位置然后运用 animateTo 动画到 200.dp,代码如下:

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

动画作用:

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

实战

在 《Android Compose 动画运用详解(三)自界说animateXxxAsState动画》一文中咱们经过 animateValueAsState自界说 animateUploadAsState完结了上传按钮的动画,现在咱们看看怎么经过 Animatable自界说完结相同的动画作用。

关于上传按钮动画的完结原理可查看 《Android Compose 动画运用详解(二)状况改动动画animateXxxAsState》一文的具体介绍。

首要自界说 UploadData实体类

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

然后自界说 animateUploadAsStateapi:

@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
    )
}

运用:

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("改动上传状况")
    }
}

运转作用如下:

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

最终

本篇介绍了更底层动画 api Animatable的创立以及 animateTosnapTo 的运用,并经过一个简略的实战实例完结了怎么经过 Animatable 完结自界说动画完结与 animateXxxAsState 相同的作用。除此之外 Animatable 还有 animateDecayapi 、鸿沟值的设置以及中止动画等,因为篇幅问题咱们将在后续文章中进行具体介绍,请持续关注本专栏了解更多 Compose 动画内容。