Compose 动画艺术探索之属性动画
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
本篇文章是此专栏的第三篇文章,如果想阅读前两篇文章的话请点击下方链接:
Compose 动画艺术探索之瞅下 Compose 的动画Compose 动画艺术探索之可见性动画Compose的属性动画属性动画是通过不断地修改值来实现的,而初始值和结束值之间的过渡动画就需要来计算了。在Compose中为我们提供了一整套 api 来实现属性动画,具体有哪些呢?让我们一起来看下吧!
image.png官方为我们提供了上图这十种方法,我们可以根据实际项目中的需求进行挑选使用。
在第一篇文章中也提到了Compose的属性动画,但只是简单使用了下,告诉大家Compose有这个东西,今天咱们来具体看下!
先来看下animateColorAsState的代码吧:
@Composable
funanimateColorAsState(
targetValue:Color,
animationSpec:AnimationSpecColor=colorDefaultSpring,
label:String="ColorAnimation",
finishedListener:((Color)-Unit)?=null
):StateColor{
valconverter=remember(targetValue.colorSpace){
(Color.VectorConverter)(targetValue.colorSpace)
}
returnanimateValueAsState(
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,来看下吧:
valColor.Companion.VectorConverter:
(colorSpace:ColorSpace)-TwoWayConverterColor,AnimationVector4D
get()=ColorToVector
如果按照我之前的习惯肯定要接着看animateValueAsState方法内部的代码了,但今天等会再看!再来看看animateDpAsState的代码吧!
@Composable
funanimateDpAsState(
targetValue:Dp,
animationSpec:AnimationSpec=dpDefaultSpring,
label:String="DpAnimation",
finishedListener:((Dp)-Unit)?=null
):State{
returnanimateValueAsState(
targetValue,
Dp.VectorConverter,
animationSpec,
label=label,
finishedListener=finishedListener
)
}
发现了点什么没有,参数基本一摸一样,别着急,咱们再看看别的!
@Composable
funanimateIntAsState(
targetValue:Int,
animationSpec:AnimationSpecInt=intDefaultSpring,
label:String="IntAnimation",
finishedListener:((Int)-Unit)?=null
)
@Composable
funanimateSizeAsState(
targetValue:Size,
animationSpec:AnimationSpecSize=sizeDefaultSpring,
label:String="SizeAnimation",
finishedListener:((Size)-Unit)?=null
)
@Composable
funanimateRectAsState(
targetValue:Rect,
animationSpec:AnimationSpecRect=rectDefaultSpring,
label:String="RectAnimation",
finishedListener:((Rect)-Unit)?=null
)
不能说是大同小异,只能说是一摸一样!既然一摸一样的话咱们就以文章开头的animateColorAsState来看吧!
上面的说法其实是不对的,并不是有十种,而是九种,因为九种都调用了animateValueAsState,其实也可以说有无数种,因为可以自定义。。。。
参数下面先来看下animateValueAsState的方法体吧:
@Composable
funT,V:AnimationVectoranimateValueAsState(
targetValue:T,
typeConverter:TwoWayConverterT,V,
animationSpec:AnimationSpec=remember{spring()},
visibilityThreshold:T?=null,
label:String="ValueAnimation",
finishedListener:((T)-Unit)?=null
):State
来看看接收的参数吧,可以发现有两个参数没有见过:
typeConverter:类型转换器,将需要的类型转换为AnimationVectorvisibilityThreshold:一个可选的阈值,用于定义何时动画值可以被认为足够接近targetValue以结束动画OK,剩下的参数在上面都介绍过,就不重复进行介绍了。
方法体由于animateValueAsState方法有点长,所以分开来看吧,接下来看下animateValueAsState方法中的前半部分:
valanimatable=remember{Animatable(targetValue,typeConverter,visibilityThreshold,label)}
vallistenerbyrememberUpdatedState(finishedListener)
valanimSpec:AnimationSpecbyrememberUpdatedState(
animationSpec.run{
if(visibilityThreshold!=nullthisisSpringSpec&&
this.visibilityThreshold!=visibilityThreshold
){
spring(dampingRatio,stiffness,visibilityThreshold)
}else{
this
}
}
)
valchannel=remember{Channel(Channel.CONFLATED)}
SideEffect{
channel.trySend(targetValue)
}
LaunchedEffect(channel){
for(targetinchannel){
valnewTarget=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类中代码比较多,先来看下类的初始化及构造方法吧!
classAnimatableT,V:AnimationVector(
initialValue:T,
valtypeConverter:TwoWayConverterT,V,
privatevalvisibilityThreshold:T?=null,
vallabel:String="Animatable"
)
可以看到这里使用到的参数在animateValueAsState中都有,就不一一介绍了,挑着重点来,来看看上面使用到的animateTo吧:
suspendfunanimateTo(
targetValue:T,
animationSpec:AnimationSpec=defaultSpringSpec,
initialVelocity:T=velocity,
block:(AnimatableT,V.()-Unit)?=null
):AnimationResultT,V{
valanim=TargetBasedAnimation(
animationSpec=animationSpec,
initialValue=value,
targetValue=targetValue,
typeConverter=typeConverter,
initialVelocity=initialVelocity
)
returnrunAnimation(anim,initialVelocity,block)
}
可以看到animateTo使用传进来的参数构建了一个TargetBasedAnimation,这是一个方便的动画包装类,适用于所有基于目标的动画,即具有预定义结束值的动画。然后返回调用了runAnimation,返回值为AnimationResult,来看下吧:
classAnimationResultT,V:AnimationVector(
valendState:AnimationStateT,V,
valendReason:AnimationEndReason
){
overridefuntoString():String="AnimationResult(endReason=$endReason,endState=$endState)"
}
AnimationResult在动画结尾包含关于动画的信息,endState捕获动画在最后一帧的值evelocityframe time等。它可以用于启动另一个动画以从先前中断的动画继续速度。endReason描述动画结束的原因。
下面看下runAnimation吧:
privatesuspendfunrunAnimation(
animation:AnimationT,V,
initialVelocity:T,
block:(AnimatableT,V.()-Unit)?
):AnimationResultT,V{
valstartTime=internalState.lastFrameTimeNanos
returnmutatorMutex.mutate{
try{
......
endState.animate(
animation,
startTime
){
updateState(internalState)
......
}
valendReason=if(clampingNeeded)BoundReachedelseFinished
endAnimation()
AnimationResult(endState,endReason)
}catch(e:CancellationException){
//Cleanupinternalstatesfirst,thenthrow.
endAnimation()
throwe
}
}
}
这里需要注意:所有不同类型的动画代码路径最终都会汇聚到这个方法中。
好了,基本快见到阳光了!
天亮了上面方法中有一行:endState.animate,这个是关键,来看下!
internalsuspendfunT,V:AnimationVectorAnimationStateT,V.animate(
animation:AnimationT,V,
startTimeNanos:Long=AnimationConstants.UnspecifiedTime,
block:AnimationScopeT,V.()-Unit={}
){
valinitialValue=animation.getValueFromNanos(0)
valinitialVelocityVector=animation.getVelocityVectorFromNanos(0)
varlateInitScope:AnimationScopeT,V?=null
try{
if(startTimeNanos==AnimationConstants.UnspecifiedTime){
valdurationScale=coroutineContext.durationScale
animation.callWithFrameNanos{
lateInitScope=AnimationScope(...).apply{
//第一帧
doAnimationFrameWithScale(it,durationScale,animation,this@animate,block)
}
}
}else{
lateInitScope=AnimationScope(...).apply{
//第一帧
doAnimationFrameWithScale()
}
}
//后续帧
while(lateInitScope!!.isRunning){
valdurationScale=coroutineContext.durationScale
animation.callWithFrameNanos{
lateInitScope!!.doAnimationFrameWithScale(it,durationScale,animation,this,block)
}
}
//动画结束
}catch(e:CancellationException){
lateInitScope?.isRunning=false
if(lateInitScope?.lastFrameTimeNanos==lastFrameTimeNanos){
isRunning=false
}
throwe
}
}
嗯,柳暗花明!这个动画函数从头到尾运行给定animation中定义的动画。在动画过程中,AnimationState将被更新为最新的值,速度,帧时间等。
到这里animateColorAsState大概过了一遍,但也只是简单走了一遍流程,并没有深究里面的细节,比如Animatable类中都没看,runAnimation方法也只是看了主要的代码等等。
结尾本篇文章先写到这里吧,属性动画其实都差不多,区别只是泛型不同以及一些特定实现,大家如果有需要可以一个一个去看看。
本文所有源码基于Compose 1.3.0-beta02。
本文至此结束,有用的地方大家可以参考,当然如果能帮助到大家,哪怕是一点也足够了。就这样。
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线