Android Compose状态改变动画animateXxxAsState使用详解
作者:loongwind
前言
上一篇文章我们探索了 Compose 中属性动画的使用,发现属性动画确实是可以在 Compose 中使用的,虽然使用方式跟传统 Android 开发中有所区别,但也不难用,甚至对于已经熟悉了属性动画的我们来说学习成本更低,那么为什么 Compose 又要单独搞一套 动画 Api 呢?为了搞清楚这个问题,首先我们得先学会 Compose 动画的使用。
实践是检验真理的唯一标准,我们将从这一篇开始一步步深入学习 Compose 动画的使用,看看它到底好不好用。本篇将首先从animateXxxAsState
这一组动画 Api 开始进入 Compose 的动画世界。
animateXxxAsState
在 Compose 中提供了一系列动画 API,其中有一类 API 跟属性动画非常类似,它就是 animateXxxAsState
,我翻译成状态改变动画,其中 Xxx
对应的是 Dp
、Float
、Int
、Size
、Offset
、Rect
、IntOffset
、IntSize
、Color
等数据类型,即当状态改变时触发对应数据类型的值的发生改变,从而执行数据从当前值到目标值变化的动画。
对应 Api 如图:
接下来就看看这些 Api 到底是如何使用的。
基础使用
我们首先以 animateDpAsState
为例来看一下 animateXxxAsState
动画到底如何使用。
Dp
是 Compose 提供的一个封装数据类型,作用跟在传统 Android 开发中 xml 使用的 dp
单位是一样的,是与屏幕像素密度相关的抽象单位。Compose 中为其提供了基础数据类型的扩展,可以直接使用数值.dp
进行使用,如:10.dp
、12.5.dp
等。
在 Compose 中跟长度相关的参数类型基本上都是 Dp
,如宽高、圆角、间距等等。
animateDpAsState
的定义如下:
fun animateDpAsState( targetValue: Dp, animationSpec: AnimationSpec<Dp> = dpDefaultSpring, finishedListener: ((Dp) -> Unit)? = null ): State<Dp>
参数说明:
- targetValue:目标值
- animationSpec:动画规格
- finishedListener:动画完成监听
返回值是一个 State
对象,即当其内部 value 值发生改变时会触发 Compose 的重组,从而刷新界面。
前面说了 animateXxxAsState
跟属性动画类似,但是好像不对呀,这里参数只有一个 targetValue
即目标值,熟悉属性动画的都知道,属性动画的数值参数是一个可变参数,当为 1 个的时候,初始值为属性当前值,目标值为传入参数值,多个参数时初始值为第一个参数值,那这里只有一个 targetValue
参数是不是也是初始值是从组件中获取呢?
我们来试试,创建一个 Box 通过改变其左边距实现向右移动的动画:
val startPadding = animateDpAsState(10.dp) Box(Modifier .padding(start = startPadding.value, top = 10.dp) .size(100.dp, 100.dp) .background(Color.Blue) )
因为需要对左边距进行改变,所以将 padding 的 start 提取为 startPadding 变量,如上面的代码,但是这样的话那初始值就是 animateDpAsState
传入的值,也就是这里的 10.dp ,先运行一下看看是不是这样:
运行效果确实是这样,那怎么实现动画效果呢?是修改 startPadding 的值么?我们给 Box 添加一个点击事件修改 startPadding 的值看看:
val startPadding = animateDpAsState(10.dp) Box(Modifier .padding(start = startPadding.value, top = 10.dp) .size(100.dp, 100.dp) .background(Color.Blue) // 添加点击事件 .clickable { // 修改值 报错 startPadding.value = 100.dp } )
这样写编辑器直接报错了,错误信息如下:
说 val 变量不能重新赋值,是因为 startPadding 变量定义成了 val 所以不能修改么?并不是,因为我们重新赋值的不是 startPadding 变量,而是其内部的 value,而 startPadding 是 State 类型,State 内部的 value 是 val 的,定义如下:
interface State<out T> { val value: T }
所以并不能通过重新赋值修改 animateDpAsState
创建的 State 的 value 值,那么怎么修改这个值让其产生动画呢?
前面说了 animateXxxAsState 是依赖状态改变而产生值的变化,所以实际上我们这里还需要定义一个额外的状态变量,targetValue 参数根据这个状态传入不同的值,修改上面代码如下:
@Composable fun DpAnimationBox(){ // 是否移动到右边 var moveToRight by remember { mutableStateOf(false) } //根据 moveToRight 变量传入参数,true 代表在右边则传入 100.dp,false 在左边则传入 10.dp val startPadding = animateDpAsState(if (moveToRight) 100.dp else 10.dp) Box(Modifier .padding(start = startPadding.value, top = 10.dp) .size(100.dp, 100.dp) .background(Color.Blue) .clickable { // 改变 moveToRight 状态,这里直接取反 moveToRight = !moveToRight } ) }
修改点如下:
- 使用 mutableStateOf 创建 moveToRight 变量,内部值为 Boolean 类型,即 MutableState,因为是在 Compose 函数中使用,需要用 remember 函数包裹,防止重组时重复创建
- 修改 animateDpAsState 传入参数的固定值为根据 moveToRight 传入,即
if (moveToRight) 100.dp else 10.dp
- 修改点击事件处理,修改 moveToRight 的值
运行看一下效果:
终于有效果了。所以实际是根据 moveToRight 的值改变导致传入 animateDpAsState 的 targetValue 参数的值发生改变,而动画执行的就是之前旧的值到当前设置最新值的动画效果。
上面的 moveToRight 是 MutableState 类型, 内部的 value 是 Boolean 类型,那是不是只能是 Boolean 类型呢,当然不是,可以是任何类型,只要在传入 animateDpAsState 的参数值时根据这个类型的值进行自定义条件判断传入不同的数据即可,比如定义一个枚举类型,根据不同类型传入不同的参数,如下:
enum class CustomState{ STATE1, STATE2, STATE3, } var customState by remember { mutableStateOf(CustomState.STATE1) } val paddingValue = when(customState){ CustomState.STATE1 -> 0.dp CustomState.STATE2 -> 100.dp CustomState.STATE3 -> 200.dp } val startPadding = animateDpAsState(paddingValue)
甚至你可以直接创建一个跟动画值相同的数据类型,比如这里可以直接创建一个 Dp 类型的状态变量,然后在点击时直接改变其值来驱动动画执行,如下:
@Composable fun DpAnimationBox(){ // 动画目标值 var startPaddingValue by remember { mutableStateOf(10.dp) } // 蒋其设置给 animateDpAsState val startPadding = animateDpAsState(startPaddingValue) Box(Modifier .padding(start = startPadding.value, top = 10.dp) .size(100.dp) .background(Color.Blue) .clickable { // 改变动画目标值 if(startPaddingValue == 10.dp){ startPaddingValue = 100.dp }else{ startPaddingValue = 10.dp } } ) }
上面代码同样能实现跟之前一样的效果。使用还是相当灵活的,开发中可以根据实际的需求定义不同的状态来完成我们想要的动画效果。
动画监听
animateXxxAsState
提供了动画完成时的监听 finishedListener
,可以通过监听动画完成进行自定义的业务处理,比如修改界面的显示状态或者开启下一个动画等。
比如 animateDpAsState
的finishedListener
定义如下:
(Dp) -> Unit
有一个 Dp 类型的参数,即动画完成时的目标值,使用如下:
val startPadding = animateDpAsState(if (moveToRight) 100.dp else 10.dp) { //TODO: do something }
比如我们想在上面的动画结束时再让方块移动回去,那我们可以这么写:
val startPadding = animateDpAsState(if (moveToRight) 100.dp else 10.dp) { if(it == 100.dp){ moveToRight = false } }
效果如下:
或者我们想让这个方块往返重复执行,可以这么写:
val startPadding = animateDpAsState(if (moveToRight) 100.dp else 10.dp) { moveToRight = !moveToRight }
效果如下:
通过对 animateXxxAsState
动画的监听我们可以实现界面状态的刷新或进行动画的组合等自定义操作。
使用示例
前面讲了 animateDpAsState
动画的使用,其他 animateXxxAsState
api 的使用基本一样,只是动画作用的数据类型不一样,下面将通过一个个简单示例来看看其他几个 api 的使用。
animateFloatAsState
animateFloatAsState
作用于 Float 类型数据的动画,比如 alpha 值,通过改变控件的 alpha 值可实现元素的显示与隐藏,使用示例如下:
@Composable fun FloatAnimationBox() { var show by remember { mutableStateOf(true) } val alpha by animateFloatAsState(if (show) 1f else 0f) Box(Modifier .padding(10.dp) .size(100.dp) .alpha(alpha) .background(Color.Blue) .clickable { show = !show } ) }
动画效果:
animateIntAsState
animateIntAsState
作用于 Int 数据类型,上面的 animateDpAsState
实现的动画也可以使用 animateIntAsState
实现,如下:
@Composable fun IntAnimationBox() { var moveToRight by remember { mutableStateOf(false) } val startPadding by animateIntAsState(if (moveToRight) 100 else 10) Box(Modifier .padding(start = startPadding.dp, top = 10.dp) .size(100.dp) .background(Color.Blue) .clickable { moveToRight = !moveToRight } ) }
效果跟使用 animateDpAsState
实现的一样:
animateColorAsState
animateColorAsState
是作用于 Color 上,可实现颜色的过渡动画,比如将上面的方块颜色从蓝色变为红色,代码如下:
@Composable fun ColorAnimationBox() { var toRed by remember { mutableStateOf(false) } val color by animateColorAsState(if (toRed) Color.Red else Color.Blue) Box(Modifier .padding(10.dp) .size(100.dp) .background(color) .clickable { toRed = !toRed } ) }
效果如下:
animateSizeAsState/animateIntSizeAsState
animateSizeAsState
作用于 Size 上,看到这个我们一下就想到了用于控件的 size 上,比如上面的 Modifier.size()
上,但实际上 Modifier.size()
的参数并不是 Size 类型,而是 Dp 类型或者 DpSize
,而 DpSize 并不是 Size 的子类,所以不能直接将 Size 类型的数据直接传入 Modifier.size()
中,而是需要转换一下:
@Composable fun SizeAnimationBox() { var changeSize by remember { mutableStateOf(false) } // 定义 Size 动画 val size by animateSizeAsState(if (changeSize) Size(200f, 50f) else Size(100f, 100f)) Box(Modifier .padding(10.dp) // 设置 Size 值 .size(size.width.dp, size.height.dp) .background(Color.Blue) .clickable { changeSize = !changeSize } ) }
效果如下:
animateIntSizeAsState
跟 animateSizeAsState
几乎一样,只是它作用于 IntSize
,跟 Size 的唯一区别就是参数是 Int 类型而不是 Float 类型,如下:
val size by animateIntSizeAsState(if (changeSize) IntSize(200, 50) else IntSize(100, 100))
animateOffsetAsState/animateIntOffsetAsState
animateOffsetAsState
作用于 Offset 类型数据,用于控制偏移量,同样的它不能直接用于 Modifier.offset()
上,因为 Modifier.offset()
接收的也是 Dp 类型参数,所以也需要进行转换,如下:
@Composable fun OffsetAnimationBox() { var changeOffset by remember { mutableStateOf(false) } // 定义 offset 动画 val offset by animateOffsetAsState(if (changeOffset) Offset(100f, 100f) else Offset(0f, 0f)) Box(Modifier // 设置 offset 数值 .offset(offset.x.dp, offset.y.dp) .padding(10.dp) .size(100.dp) .background(Color.Blue) .clickable { changeOffset = !changeOffset } ) }
效果如下:
animateIntOffsetAsState
则作用于 IntOffset
类型数据,使用方法与上面一致,只是将 Float 类型换成 Int 类型:
val intOffset by animateIntOffsetAsState(if (changeOffset) IntOffset(100, 100) else IntOffset(0, 0))
Modifier.offset()
提供了一个返回 IntOffset 的函数参数,可以如下使用:
Modifier.offset { intOffset }
animateRectAsState
animateRectAsState
作用于 Rect
数据,即可以同时控制位置和大小,通过 animateRectAsState
可实现上面方块的位置和大小变化的动画,使用如下:
@Composable fun RectAnimationBox() { var changeRect by remember { mutableStateOf(false) } // 定义 rect val rect by animateRectAsState(if (changeRect) Rect(100f, 100f, 310f, 150f) else Rect(10f, 10f, 110f, 110f)) Box(Modifier // 设置位置偏移 .offset(rect.left.dp, rect.top.dp) // 设置大小 .size(rect.width.dp, rect.height.dp) .background(Color.Blue) .clickable { changeRect = !changeRect } ) }
效果如下:
实战
上面讲了 animateXxxAsState
动画 api 的基本使用,下面就用这些 api 来完成一个实战效果,还是上一篇《Compose 中属性动画的使用》的效果:
前面说了animateXxxAsState
是依赖于状态的动画,分析上面的动画一共存在 4 个状态:
- 默认状态:显示蓝色矩形按钮,文字为 Upload
- 开始上传状态:按钮变为圆形且中间为白色,边框为灰色,文字消失
- 上传中状态:边框根据进度变为蓝色
- 上传完成状态:按钮从圆形回到圆角矩形,且颜色变为红色,文字变为 Success
实现原理如下:
首先通过一个枚举定义上述四种状态:
enum class UploadState { Normal, Start, Uploading, Success }
然后实现默认状态的界面展示:
@Composable fun UploadAnimation() { val originWidth = 180.dp val circleSize = 48.dp var uploadState by remember { mutableStateOf(UploadState.Normal) } var text by remember { mutableStateOf("Upload") } val textAlpha by animateFloatAsState(1f) val backgroundColor by animateColorAsState(Color.Blue) val boxWidth by animateDpAsState(originWidth) val progressAlpha by animateFloatAsState(0f) val progress by animateIntAsState(0) // 界面布局 Box( modifier = Modifier .padding(start = 10.dp, top = 10.dp) .width(originWidth), contentAlignment = Alignment.Center ) { // 按钮 Box( modifier = Modifier .clip(RoundedCornerShape(circleSize / 2)) .background(backgroundColor) .size(boxWidth, circleSize) .clickable { // 点击时修改状态为开始上传 uploadState = UploadState.Start }, contentAlignment = Alignment.Center, ) { // 进度 Box( modifier = Modifier.size(circleSize).clip(ArcShape(progress)) .alpha(progressAlpha).background(Color.Blue) ) // 白色蒙版 Box( modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp)) .alpha(progressAlpha).background(Color.White) ) // 文字 Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha)) } } }
然后根据上传按钮的状态定义不同状态时的数据值:
var textAlphaValue = 1f var backgroundColorValue = Color.Blue var boxWidthValue = originWidth var progressAlphaValue = 0f var progressValue = 0 when (uploadState) { // 默认状态不处理 UploadState.Normal -> {} // 开始上传 UploadState.Start -> { // 文字透明度变为0 textAlphaValue = 0f // 按钮背景颜色变为灰色 backgroundColorValue = Color.Gray // 按钮宽度变为圆的宽度 boxWidthValue = circleSize // 中间进度的透明度变为 1 progressAlphaValue = 1f } // 上传中状态 UploadState.Uploading -> { textAlphaValue = 0f backgroundColorValue = Color.Gray boxWidthValue = circleSize progressAlphaValue = 1f // 进度值变为 100 progressValue = 100 } // 上传完成 UploadState.Success -> { // 文字透明度变为 1 textAlphaValue = 1f // 颜色变为红色 backgroundColorValue = Color.Red // 按钮宽度变化默认时的原始宽度 boxWidthValue = originWidth // 进度透明度变为 0f progressAlphaValue = 0f } } val textAlpha by animateFloatAsState(textAlphaValue) val backgroundColor by animateColorAsState(backgroundColorValue) val boxWidth by animateDpAsState(boxWidthValue) val progressAlpha by animateFloatAsState(progressAlphaValue) val progress by animateIntAsState(progressValue)
此时运行后点击按钮效果如下:
点击后只有开始上传的动画,没有后续的动画效果,这是因为我们在点击的时候只是将状态变为了 UploadState.Start 而没有进行后续状态的改变,所以需要监听动画完成然后继续改变按钮的状态来实现完整的动画效果,代码修改如下:
val boxWidth by animateDpAsState(boxWidthValue){ // 按钮宽度变化完成监听,当状态为 Start 则修改为 Uploading if(uploadState == UploadState.Start){ uploadState = UploadState.Uploading } } val progress by animateIntAsState(progressValue){ // 进度完成监听,当状态为 Uploading 则修改为 Success if(uploadState == UploadState.Uploading){ uploadState = UploadState.Success // 文字内容修改为 Success text = "Success" } }
分别给按钮宽度变化动画和进度动画进行监听并修改其状态,这样就将整个动画串联起来了,最终效果如下:
最后
关于 animateXxxAsState
的基本使用就讲得差不多了,并通过一系列 api 完成了上一篇使用属性动画实现的效果,细心的同学会发现关于 animateXxxAsState
其实还有两个知识点是没有介绍到的:
animateXxxAsState
还有一个 apianimateValueAsState
animateXxxAsState
的参数animationSpec
参数
其中 animateValueAsState
是 animateXxxAsState
的底层 api,上面介绍的一系列 animateXxxAsState
最终都是调用 animateValueAsState
来实现,关于 animateValueAsState
我们将在下一篇进行详细介绍。animationSpec
是对动画进行更详细的配置,比如动画的时间、速度曲线等,将在后续文章中详细介绍
以上就是Android Compose状态改变动画animateXxxAsState使用详解的详细内容,更多关于Android Compose状态改变动画的资料请关注脚本之家其它相关文章!