Compose 动画艺术之属性动画探索
作者:Zhujiang
前言
本篇文章是此专栏的第三篇文章,如果想阅读前两篇文章的话请点击下方链接:
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 支持将 Float
、Color
、Dp
以及其他基本数据类型作为 开箱即用的动画值,但有时我们需要为其他数据类型(比如自定义类型)添加动画效果。在动画播放期间,任何动画值都表示为 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
不为空并且 AnimationSpec
为 SpringSpec
的时候为新构建的一个 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 动画艺术之属性动画探索的文章就介绍到这了,更多相关Compose属性动画内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!