Android Compose 动画使用详解(八)Animatable的使用
本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言前面介绍了 Compose 的animateXxxAsState动画 Api 的使用,以及如何通过animateValueAsState实现自定义animateXxxAsState动画 Api ,如何对动画进行详细配置从而达到灵活的实现各种动画效果。
本篇将为大家介绍更底层的动画 Api :Animatable
Animatable在前面介绍animateXxxAsState的时候我们跟踪源码发现其内部调用的是animateValueAsState,那么animateValueAsState内部又是怎么实现的呢?来看看animateValueAsState的源码:
funT,V:AnimationVectoranimateValueAsState(
targetValue:T,
typeConverter:TwoWayConverterT,V,
animationSpec:AnimationSpecT=remember{
spring(visibilityThreshold=visibilityThreshold)
},
visibilityThreshold:T?=null,
finishedListener:((T)-Unit)?=null
):State{
valanimatable=remember{Animatable(targetValue,typeConverter)}
vallistenerbyrememberUpdatedState(finishedListener)
valanimSpecbyrememberUpdatedState(animationSpec)
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)
}
}
}
}
returnanimatable.asState()
}
可以发现,animateValueAsState的内部其实就是通过Animatable来实现的。实际上animateValueAsState是对Animatable的上层使用封装,而animateXxxAsState又是对animateValueAsState的上层使用封装,所以Animatable是更底层的动画 api。
下面就来看一下如何使用Animatable实现动画效果。首先还是通过其构造方法定义了解创建Animatable需要哪些参数以及各个参数的含义,构造方法定义如下:
classAnimatableT,V:AnimationVector(
initialValue:T,
valtypeConverter:TwoWayConverterT,V,
privatevalvisibilityThreshold: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类型转换器
valFloat.Companion.VectorConverter:TwoWayConverterFloat,AnimationVector1D
//Int类型转换器
valInt.Companion.VectorConverter:TwoWayConverterInt,AnimationVector1D
//Rect类型转换器
valRect.Companion.VectorConverter:TwoWayConverterRect,AnimationVector4D
//Dp类型转换器
valDp.Companion.VectorConverter:TwoWayConverterDp,AnimationVector1D
//DpOffset类型转换器
valDpOffset.Companion.VectorConverter:TwoWayConverterDpOffset,AnimationVector2D
//Size类型转换器
valSize.Companion.VectorConverter:TwoWayConverterSize,AnimationVector2D
//Offset类型转换器
valOffset.Companion.VectorConverter:TwoWayConverterOffset,AnimationVector2D
//IntOffset类型转换器
valIntOffset.Companion.VectorConverter:TwoWayConverterIntOffset,AnimationVector2D
//IntSize类型转换器
valIntSize.Companion.VectorConverter:TwoWayConverterIntSize,AnimationVector2D
//package:androidx.compose.animation.ColorVectorConverter
//Color类型转换器
valColor.Companion.VectorConverter:(colorSpace:ColorSpace)-TwoWayConverterColor,AnimationVector4D
注意: Color 的转换器与其他转换器不是在同一个包下。
我们要作用于 Float 类型时就可以直接使用Float.VectorConverter即可,代码如下:
valanimatable=remember{Animatable(100f,Float.VectorConverter)}
在 Compose 函数里创建Animatable对象时必须使用remember进行包裹,否则会报错。
创建其他数值类型的动画则传对应的VectorConverter即可,如 Dp、Size、Color,创建代码如下:
valanimatable1=remember{Animatable(100.dp,Dp.VectorConverter)}
valanimatable2=remember{Animatable(Size(100f,100f),Size.VectorConverter)}
valanimatable3=remember{Animatable(Color.Blue,Color.VectorConverter(Color.Blue.colorSpace))}
除此之外,Compose 还为 Float 和 Color 提供了简便方法Animatable,只需要传入初始值即可,使用如下:
//Float简便方法使用
importandroidx.compose.animation.core.Animatable
valanimatableFloat=remember{Animatable(100f)}
//Color简便方法使用
importandroidx.compose.animation.Animatable
valanimatable5=remember{Animatable(Color.Blue)}
需要注意的是虽然都是叫Animatable,但是引入的包是不一样的,且这里的Animatable不是构造函数而是一个方法,在方法的实现里再调用的Animatable构造函数创建真正的Animatable实例,源码分别如下:
Animatable(Float):
packageandroidx.compose.animation.core
funAnimatable(
initialValue:Float,
visibilityThreshold:Float=Spring.DefaultDisplacementThreshold
)=Animatable(
initialValue,
Float.VectorConverter,
visibilityThreshold
)
Animatable(Color):
packageandroidx.compose.animation
funAnimatable(initialValue:Color):AnimatableColor,AnimationVector4D=
Animatable(initialValue,(Color.VectorConverter)(initialValue.colorSpace))
Animatable创建好后下面看看怎么触发动画执行。
animateToAnimatable提供了一个animateTo方法用于触发动画执行,看看这个方法的定义:
suspendfunanimateTo(
targetValue:T,
animationSpec:AnimationSpecT=defaultSpringSpec,
initialVelocity:T=velocity,
block:(AnimatableT,V.()-Unit)?=null
):AnimationResultT,V
首先animateTo方法是用suspend修饰的,即只能在协程中调用;其次方法有四个参数,对应解析如下:
targetValue:动画目标值animationSpec:动画规格配置,这个前面几篇文件进行了详细介绍,共有 6 种动画规格可进行设置initialVelocity:初始速度block:函数类型参数,动画运行的每一帧都会回调这个 block 方法,可用于动画监听最后返回值为AnimationResult类型,包含动画结束时的状态和原因。
执行动画我们还是以前面文章熟悉的方块移动动画来看一下animateTo的使用效果,代码如下:
//创建状态通过状态驱动动画
varmoveToRightbyremember{mutableStateOf(false)}
//动画实例
valanimatable=remember{Animatable(10.dp,Dp.VectorConverter)}
//animateTo是suspend方法,所以需要在协程中进行调用
LaunchedEffect(moveToRight){
//根据状态确定动画移动的目标值
animatable.animateTo(if(moveToRight)200.dpelse10.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外,也可以直接在按钮事件中触发,代码如下:
valanimatable=remember{Animatable(10.dp,Dp.VectorConverter)}
//获取协程作用域
valscope=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的最后一个参数是一个函数类型(AnimatableT, V.() - Unit)?,可以用来对动画进行监听,在回调方法里可以通过 this 获取到当前动画Animatable的实例,通过其可以获取到动画当前时刻的值、目标值、速度等。使用如下:
animatable.animateTo(200.dp){
//动画当前值
valvalue=this.value
//当前速度
valvelocity=this.velocity
//动画目标值
valtargetValue=this.targetValue
}
可以通过监听动画实现界面的联动操作,比如让另一个组件跟随动画组件一起运动等。
返回结果animateTo方法是有返回结果的,类型为AnimationResult,通过返回结果可以获取到动画结束时的状态和原因,AnimationResult源码如下:
classAnimationResultT,V:AnimationVector(
//结束状态
valendState:AnimationStateT,V,
//结束原因
valendReason:AnimationEndReason
)
只有两个属性endState和endReason分别代表动画结束时的状态和原因
endState是AnimationState类型,通过其可以获取到动画结束时的值、速度、时间等数据,源码定义如下:
classAnimationStateT,V:AnimationVector(
valtypeConverter:TwoWayConverterT,V,
initialValue:T,
initialVelocityVector:V?=null,
lastFrameTimeNanos:Long=AnimationConstants.UnspecifiedTime,
finishedTimeNanos:Long=AnimationConstants.UnspecifiedTime,
isRunning:Boolean=false
):State{
//动画值
overridevarvalue:TbymutableStateOf(initialValue)
internalset
//动画速度矢量
varvelocityVector:V=
initialVelocityVector?.copy()?:typeConverter.createZeroVectorFrom(initialValue)
internalset
//最后一帧时间(纳秒)
@get:Suppress("MethodNameUnits")
varlastFrameTimeNanos:Long=lastFrameTimeNanos
internalset
//结束时的时间(纳秒)
@get:Suppress("MethodNameUnits")
varfinishedTimeNanos:Long=finishedTimeNanos
internalset
//是否正在运行
varisRunning:Boolean=isRunning
internalset
//动画速度
valvelocity:T
get()=typeConverter.convertFromVector(velocityVector)
}
注意这里的lastFrameTimeNanos和finishedTimeNanos是基于System.nanoTime获取到的纳秒值,不是系统时间。
endReason是一个AnimationEndReason类型的枚举,只有两个枚举值:
enumclassAnimationEndReason{
//动画运行到边界时停止结束
BoundReached,
//动画正常结束
Finished
}
Finished很好理解,就是动画正常执行完成;那BoundReached到达边界停止是什么意思呢?Animatable是可以为动画设置边界的,当动画运行到边界时就会立即停止,此时返回结果的停止原因就是BoundReached,关于动画的边界设置以及动画停止的更多内容会在后续文章中进行详细介绍。
那么返回值在哪些情况下会用到呢?比如一个动画被打断时另一个动画需要依赖上一个动画的值、速度等继续执行,或者动画遇到边界停止时需要重新进行动画此时就可以通过上一个动画的返回值获取到需要的数据后进行相关处理。
snapTo除了animateTo,Animatable还提供了snapTo执行动画,看到snapTo我们自然想到了前面介绍动画配置时的快闪动画SnapSpec,即动画时长为 0 瞬间执行完成,snapTo也是同样的作用,可以让动画瞬间达到目标值,方法定义如下:
suspendfunsnapTo(targetValue:T)
同样是一个被suspend修饰的挂起函数,即必须在协程里执行;参数只有一个targetValue即目标值,使用如下:
valanimatable=remember{Animatable(10.dp,Dp.VectorConverter)}
valscope=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,代码如下:
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实体类:
dataclassUploadData(
valbackgroundColor:Color,
valtextAlpha:Float,
valboxWidth:Dp,
valprogress:Int,
valprogressAlpha:Float
)
然后自定义animateUploadAsStateapi:
@Composable
funanimateUploadAsState(
//上传按钮动画数据
value:UploadData,
//状态
state:Any,
):UploadData{
//创建对应值的Animatable实例
valbgColorAnimatable=remember{
Animatable(
value.backgroundColor,
Color.VectorConverter(value.backgroundColor.colorSpace)
)
}
valtextAlphaAnimatable=remember{Animatable(value.textAlpha)}
valboxWidthAnimatable=remember{Animatable(value.boxWidth,Dp.VectorConverter)}
valprogressAnimatable=remember{Animatable(value.progress,Int.VectorConverter)}
valprogressAlphaAnimatable=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)
}
//返回最新数据
returnUploadData(
bgColorAnimatable.value,
textAlphaAnimatable.value,
boxWidthAnimatable.value,
progressAnimatable.value,
progressAlphaAnimatable.value
)
}
使用:
valoriginWidth=180.dp
valcircleSize=48.dp
//上传状态
varuploadStatebyremember{mutableStateOf(UploadState.Normal)}
//按钮文字
vartextbyremember{mutableStateOf("Upload")}
//根据状态创建目标动画数据
valuploadValue=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创建动画
valupload=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("改变上传状态")
}
}
运行效果如下:
最后本篇介绍了更底层动画 apiAnimatable的创建以及animateTo和snapTo的使用,并通过一个简单的实战实例完成了如何通过Animatable实现自定义动画完成与animateXxxAsState同样的效果。除此之外Animatable还有animateDecayapi 、边界值的设置以及停止动画等,由于篇幅问题我们将在后续文章中进行详细介绍,请持续关注本专栏了解更多 Compose 动画内容。
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线