美文网首页
Compose 动画艺术探索之属性动画

Compose 动画艺术探索之属性动画

作者: 我爱田Hebe | 来源:发表于2022-10-18 10:41 被阅读0次

    本篇文章是此专栏的第三篇文章,如果想阅读前两篇文章的话请点击下方链接:

    Compose的属性动画

    属性动画是通过不断地修改值来实现的,而初始值和结束值之间的过渡动画就需要来计算了。在 Compose 中为我们提供了一整套 api 来实现属性动画,具体有哪些呢?让我们一起来看下吧!

    官方为我们提供了上图这十种方法,我们可以根据实际项目中的需求进行挑选使用。

    在第一篇文章中也提到了 Compose 的属性动画,但只是简单使用了下,告诉大家 Compose 有这个东西,今天咱们来具体看下!

    先来看下 animateColorAsState 的代码吧:

    @Composable
    fun animateColorAsState(
        targetValue: Color,
        animationSpec: AnimationSpec<Color> = colorDefaultSpring,
        label: String = "ColorAnimation",
        finishedListener: ((Color) -> Unit)? = null
    ): State<Color> {
        val converter = remember(targetValue.colorSpace) {
            (Color.VectorConverter)(targetValue.colorSpace)
        }
        return animateValueAsState(
            targetValue, converter, animationSpec, label = label, finishedListener = finishedListener
        )
    }
    

    可以看到一共接收四个参数,来分别看下代表什么吧:

    • targetValue:顾名思义,目标值,这里对应的就是想要转换成的颜色
    • animationSpec:动画规格,动画随着时间改变值的一种规格吧,上一篇文章中也提到了,但由于上一篇文章主要内容并不是这个,也就没有讲,本篇文章会详细说明
    • label:标签,以区别于其他动画
    • finishedListener:在动画完成时会进行回调

    参数并不算多,而且有三个是可选参数,也就只有 targetValue 必须要进行设置。方法体内只通过 Color.colorSpace 强转构建了一个 TwoWayConverter

    前面说过,大多数 Compose 动画 API 支持将 FloatColorDp 以及其他基本数据类型作为 开箱即用的动画值,但有时我们需要为其他数据类型(比如自定义类型)添加动画效果。在动画播放期间,任何动画值都表示为 AnimationVector。使用相应的 TwoWayConverter 即可将值转换为 AnimationVector,反之亦然,这样一来,核心动画系统就可以统一对其进行处理了。由于颜色有 argb,所以构建的时候使用的是 AnimationVector4D ,来看下吧:

    val Color.Companion.VectorConverter:
        (colorSpace: ColorSpace) -> TwoWayConverter<Color, AnimationVector4D>
            get() = ColorToVector
    

    如果按照我之前的习惯肯定要接着看 animateValueAsState 方法内部的代码了,但今天等会再看!再来看看 animateDpAsState 的代码吧!

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

    发现了点什么没有,参数基本一摸一样,别着急,咱们再看看别的!

    @Composable
    fun animateIntAsState(
        targetValue: Int,
        animationSpec: AnimationSpec<Int> = intDefaultSpring,
        label: String = "IntAnimation",
        finishedListener: ((Int) -> Unit)? = null
    )
    
    @Composable
    fun animateSizeAsState(
        targetValue: Size,
        animationSpec: AnimationSpec<Size> = sizeDefaultSpring,
        label: String = "SizeAnimation",
        finishedListener: ((Size) -> Unit)? = null
    )
    
    @Composable
    fun animateRectAsState(
        targetValue: Rect,
        animationSpec: AnimationSpec<Rect> = rectDefaultSpring,
        label: String = "RectAnimation",
        finishedListener: ((Rect) -> Unit)? = null
    )
    

    不能说是大同小异,只能说是一摸一样!既然一摸一样的话咱们就以文章开头的 animateColorAsState 来看吧!

    上面的说法其实是不对的,并不是有十种,而是九种,因为九种都调用了 animateValueAsState ,其实也可以说有无数种,因为可以自定义。。。。

    参数

    下面先来看下 animateValueAsState 的方法体吧:

    @Composable
    fun <T, V : AnimationVector> animateValueAsState(
        targetValue: T,
        typeConverter: TwoWayConverter<T, V>,
        animationSpec: AnimationSpec<T> = remember { spring() },
        visibilityThreshold: T? = null,
        label: String = "ValueAnimation",
        finishedListener: ((T) -> Unit)? = null
    ): State<T>
    

    来看看接收的参数吧,可以发现有两个参数没有见过:

    • typeConverter:类型转换器,将需要的类型转换为 AnimationVector
    • visibilityThreshold:一个可选的阈值,用于定义何时动画值可以被认为足够接近targetValue以结束动画

    OK,剩下的参数在上面都介绍过,就不重复进行介绍了。

    方法体

    由于 animateValueAsState 方法有点长,所以分开来看吧,接下来看下 animateValueAsState 方法中的前半部分:

    val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }
    val listener by rememberUpdatedState(finishedListener)
    val animSpec: AnimationSpec<T> by rememberUpdatedState(
        animationSpec.run {
            if (visibilityThreshold != null && this is SpringSpec &&
                this.visibilityThreshold != visibilityThreshold
            ) {
                spring(dampingRatio, stiffness, visibilityThreshold)
            } else {
                this
            }
        }
    )
    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)
                }
            }
        }
    }
    

    可以看到首先构建了一个 Animatable ,然后记录了完成回调,又记录了 AnimationSpec ,之后有个判断,如果 visibilityThreshold 不为空并且 AnimationSpecSpringSpec 的时候为新构建的一个 AnimationSpec ,反之则还是传进来的 AnimationSpec

    Animatable 是个啥呢?它是一个值容器,它可以在通过 animateTo 更改值时为值添加动画效果,它可确保一致的连续性和互斥性,这意味着值变化始终是连续的,并且会取消任何正在播放的动画。Animatable 的许多功能(包括 animateTo)以挂起函数的形式提供,所以需要封装在适当的协程作用域内,所以下面使用了 LaunchedEffect 来包裹执行 animateTo 方法,最后调用了动画完成的回调。

    由于 Animatable 类中代码比较多,先来看下类的初始化及构造方法吧!

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

    可以看到这里使用到的参数在 animateValueAsState 中都有,就不一一介绍了,挑着重点来,来看看上面使用到的 animateTo 吧:

    suspend fun animateTo(
        targetValue: T,
        animationSpec: AnimationSpec<T> = defaultSpringSpec,
        initialVelocity: T = velocity,
        block: (Animatable<T, V>.() -> Unit)? = null
    ): AnimationResult<T, V> {
        val anim = TargetBasedAnimation(
            animationSpec = animationSpec,
            initialValue = value,
            targetValue = targetValue,
            typeConverter = typeConverter,
            initialVelocity = initialVelocity
        )
        return runAnimation(anim, initialVelocity, block)
    }
    

    可以看到 animateTo 使用传进来的参数构建了一个 TargetBasedAnimation ,这是一个方便的动画包装类,适用于所有基于目标的动画,即具有预定义结束值的动画。然后返回调用了 runAnimation ,返回值为 AnimationResult ,来看下吧:

    class AnimationResult<T, V : AnimationVector>(
    
        val endState: AnimationState<T, V>,
    
        val endReason: AnimationEndReason
    ) {
        override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
    }
    

    AnimationResult 在动画结尾包含关于动画的信息,endState 捕获动画在最后一帧的值 evelocityframe time 等。它可以用于启动另一个动画以从先前中断的动画继续速度。endReason 描述动画结束的原因。

    下面看下 runAnimation 吧:

    private suspend fun runAnimation(
        animation: Animation<T, V>,
        initialVelocity: T,
        block: (Animatable<T, V>.() -> Unit)?
    ): AnimationResult<T, V> {
    
        val startTime = internalState.lastFrameTimeNanos
        return mutatorMutex.mutate {
            try {
                ......
                endState.animate(
                    animation,
                    startTime
                ) {
                    updateState(internalState)
                    ......
                }
                val endReason = if (clampingNeeded) BoundReached else Finished
                endAnimation()
                AnimationResult(endState, endReason)
            } catch (e: CancellationException) {
                // Clean up internal states first, then throw.
                endAnimation()
                throw e
            }
        }
    }
    

    这里需要注意:所有不同类型的动画代码路径最终都会汇聚到这个方法中。

    好了,基本快见到阳光了!

    天亮了

    上面方法中有一行:endState.animate ,这个是关键,来看下!

    internal suspend fun <T, V : AnimationVector> AnimationState<T, V>.animate(
        animation: Animation<T, V>,
        startTimeNanos: Long = AnimationConstants.UnspecifiedTime,
        block: AnimationScope<T, V>.() -> Unit = {}
    ) {
        val initialValue = animation.getValueFromNanos(0)
        val initialVelocityVector = animation.getVelocityVectorFromNanos(0)
        var lateInitScope: AnimationScope<T, V>? = null
        try {
            if (startTimeNanos == AnimationConstants.UnspecifiedTime) {
                val durationScale = coroutineContext.durationScale
                animation.callWithFrameNanos {
                    lateInitScope = AnimationScope(...).apply {
                        // 第一帧
                        doAnimationFrameWithScale(it, durationScale, animation, this@animate, block)
                    }
                }
            } else {
                lateInitScope = AnimationScope(...).apply {
                    // 第一帧
                    doAnimationFrameWithScale()
                }
            }
            // 后续帧
            while (lateInitScope!!.isRunning) {
                val durationScale = coroutineContext.durationScale
                animation.callWithFrameNanos {
                    lateInitScope!!.doAnimationFrameWithScale(it, durationScale, animation, this, block)
                }
            }
            // 动画结束
        } catch (e: CancellationException) {
            lateInitScope?.isRunning = false
            if (lateInitScope?.lastFrameTimeNanos == lastFrameTimeNanos) {
                isRunning = false
            }
            throw e
        }
    }
    

    嗯,柳暗花明!这个动画函数从头到尾运行给定 animation 中定义的动画。在动画过程中,AnimationState 将被更新为最新的值,速度,帧时间等。

    到这里 animateColorAsState 大概过了一遍,但也只是简单走了一遍流程,并没有深究里面的细节,比如 Animatable 类中都没看,runAnimation 方法也只是看了主要的代码等等。

    结尾

    本篇文章先写到这里吧,属性动画其实都差不多,区别只是泛型不同以及一些特定实现,大家如果有需要可以一个一个去看看。

    本文所有源码基于 Compose 1.3.0-beta02

    本文至此结束,有用的地方大家可以参考,当然如果能帮助到大家,哪怕是一点也足够了。就这样。

    作者:Zhujiang
    链接:https://juejin.cn/post/7148631147705008142

    相关文章

      网友评论

          本文标题:Compose 动画艺术探索之属性动画

          本文链接:https://www.haomeiwen.com/subject/dwmjzrtx.html