Android Compose 属性动画使用探索详解
作者:loongwind 发布时间:2022-08-07 11:06:57
前言
Jetpack Compose
(简称 Compose )是 Google 官方推出的基于 Kotlin 语言的 Android 新一代 UI 开发框架,其采用声明式的 UI 编程特性使得 Android 应用界面的编写和维护变得更加简单。
本专栏将详细介绍在使用 Compose 进行 UI 开发中如何实现炫酷的动画效果。动画效果在 App 使用中至关重要,它使得 App 的交互更加自然流畅,用户使用体验更加良好。
在传统的 Android 开发中有古老的 View 动画和目前流行的属性动画,如今 View 动画几乎已被广大开发者所抛弃,属性动画因其可以作用于任何对象的灵活和强大特性而被开发者所拥抱。既然属性动画这么强大,那么它是否能用在 Compose 开发中呢?如果能那跟传统 UI 开发中使用又有什么区别呢?本篇就带领你来探索一下在 Compose 中属性动画的使用。
使用探索
在传统 Android 开发中,属性动画使用得最多的是 ObjectAnimator
和 ValueAnimator
,接下来就探索一下在 Compose 中如何使用它们来实现动画效果。
ObjectAnimator 使用探索
首先看一下在传统 Android 开发中如何使用属性动画,比如使用属性动画实现竖直方向向下移动的动画:
val animator = ObjectAnimator.ofFloat(view, "translationY", 10f, 100f)
animator.start()
通过 ObjectAnimator
作用于 View 的 translationY
属性,不断改变 translationY 的值从而实现动画效果,一个很简单的属性动画,这里就不贴运行效果了。
那在 Compose 中能否使用 ObjectAnimator 呢?
下面使用 Compose 在界面上显示一个 100dp*100dp 的蓝色正方形方块,代码如下:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Box(Modifier.padding(start = 10.dp, top = 10.dp)
.size(100.dp)
.background(Color.Blue)
)
}
}
}
运行效果如下:
现在要同样实现一个竖直方向移动的动画效果,让方块从上往下移动。在上面的属性动画实现中 ObjectAnimator
是作用于 View 组件上的,按照这个思路在这里 ObjectAnimator 就应该作用于 Box 上,但实际上我们这里压根拿不到 Box 的实例,因为这里的 Box 实际是一个函数且没有返回值,看一下 Box 的源码:
@Composable
fun Box(modifier: Modifier) {
Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier)
}
既然作用于 Box 上不行,那能不能作用于 State 上呢,Compose 是数据驱动 UI 刷新,通过数据状态改变重组 UI 实现界面的刷新,把上面的 top 提取为一个 State 再通过 ObjectAnimator 去改变是否可行呢?改造代码实验一下:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val topPadding:MutableState<Int> = mutableStateOf(10)
val animator = ObjectAnimator.ofInt(topPadding, "value", 10, 100)
animator.duration = 1000
setContent {
Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
.size(100.dp)
.background(Color.Blue)
// 添加点击事件
.clickable {
// 启动动画
animator.start()
}
)
}
}
}
改造如下:
将之前 top 的固定值提取成了一个 State 变量 topPadding,当 topPadding 的值发生改变时会重组界面从而让界面刷新
声明了 ObjectAnimator 的 animator 变量,作用于 topPadding 的 value 属性上,并设置动画值从 10 到 100,动画时长 1000ms
给 Box 添加点击监听事件启动动画
实际上写完这段代码,编辑器就已经有报错提示了,提示如下:
说没有找到带 Int 参数的 setValue
方法,那来看看 MutableState
是否有 setValue 方法:
interface MutableState<T> : State<T> {
override var value: T
operator fun component1(): T
operator fun component2(): (T) -> Unit
}
可以发现 MutableState 中是有一个 var 修饰的 value 变量的,说明是有 setValue 方法的,但是错误提示是找不到带 Int 参数的 setValue 方法,实际上 MutableState 的 setValue 的定义应该是这样的:
fun setValue(value:T){
this.value = value
}
这里参数类型是泛型 T
,而 ObjectAnimator 找的是明确的 Int 类型参数的方法,所以找不到。那怎么办呢?是不是就意味着在 Compose 中无法使用 ObjectAnimator 了呢?
直接使用确实是不行,那我们能不能对其进行封装,不是找不到对应的 setValue 方法嘛,那我封装一下提供一个 setValue 方法不就行了。定义一个 IntState
类,再提供一个 mutableIntStateOf
方法:
class IntState(private val state: MutableState<Int>){
var value : Int = state.value
get() = state.value
set(value) {
field = value
state.value = value
}
}
fun mutableIntStateOf(value: Int, policy: SnapshotMutationPolicy<Int> = structuralEqualityPolicy()) : IntState{
val state = mutableStateOf(value, policy)
return IntState(state)
}
IntState
构造方法传入一个 MutableState 类型的 state 参数,然后提供一个 value 变量,get 方法返回 state.value ,set 方法将传入值设置给 state.value,这样 IntState
就有了一个明确的 setValue(value:Int) 的方法。
为了便于使用,封装一个 mutableIntStateOf
方法,实现里先采用 Compose 提供的 mutableStateOf 方法获取一个 MutableState ,然后用其构建一个 IntState 进行返回。
再改造一下上面动画实现代码将 mutableStateOf
替换成 mutableIntStateOf
:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 替换为 mutableIntStateOf
val topPadding = mutableIntStateOf(10)
// 创建 ObjectAnimator 目标为 topPadding,作用属性为 value,值从 10 变化到 100
val animator = ObjectAnimator.ofInt(topPadding, "value", 10, 100)
// 设置动画时长 1s
animator.duration = 1000
setContent {
Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
.size(100.dp)
.background(Color.Blue)
// 添加点击事件
.clickable {
// 启动动画
animator.start()
}
)
}
}
}
现在不报错了,运行一下看看是否有动画效果:
效果符合预期,说明这种办法是可行,也说明 ObjectAnimator 在 Compose 中也是可以使用的,只是不能像传统 Android 开发那样直接作用于 View 组件上,而是需要进行二次封装后使用。
ValueAnimator 使用探索
ObjectAnimator 使用探索完了,那么 ValueAnimator
能否使用呢?Compose 以声明式的方式通过数据驱动界面刷新,而ValueAnimator
主要用于数据的改变,好像很契合的样子,使用 ValueAnimator 不断改变 State 的值理论上就可以实现动画效果。还是上面的例子,改造成使用 ValueAnimator
来实现:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 使用 mutableStateOf 创建 topPadding 的 State
var topPadding by mutableStateOf(10)
// 创建 ValueAnimator 从 10 变化到 100
val animator = ValueAnimator.ofInt(10, 100)
// 动画时长 1s
animator.duration = 1000
// 设置监听,当动画改变时动态修改 topPadding 的值
animator.addUpdateListener {
topPadding = it.animatedValue as Int
}
setContent {
Box(Modifier.padding(start = 10.dp, top = topPadding.dp)
.size(100.dp)
.background(Color.Blue)
.clickable {
animator.start()
}
)
}
}
}
是否有效果呢?运行一下看看效果:
跟上面使用 ObjectAnimator 实现的效果一致,说明 ValueAnimator 在 Compose 中实现动画是可行的,只是需要手动去监听 ValueAnimator 值的变化然后去动态更新 State 的值,稍微麻烦了一点,实际上我们也可以对其进行封装简化其使用。
通过上面的代码发现,如果要在 Compose 中使用 ValueAnimator 来实现动画,对动画数值的改变进行监听并动态更新 State 的值是必不可少的一步,那么我们就可以将其提取进行封装。
/**
* @param state 动画作用的目标 State
* @param values 动画的变化值,可变参数
*/
fun animatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
// 创建 ValueAnimator ,参数为传入的 values
val animator = ValueAnimator.ofInt(*values)
// 添加监听
animator.addUpdateListener {
// 更新 state 的 value 值
state.value = it.animatedValue as Int
}
return animator
}
然后将上面的创建动画替换成使用 animatorOfInt 创建:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val topPadding = mutableStateOf(10)
// 使用封装的 animatorOfInt 方法创建动画
val animator = animatorOfInt(topPadding, 10, 100)
animator.duration = 1000
setContent {
Box(Modifier.padding(start = 10.dp, top = topPadding.value.dp)
.size(100.dp)
.background(Color.Blue)
.clickable {
animator.start()
}
)
}
}
}
使用是不是要简单很多,不需要手动去处理动画值变化的监听了,有点使用 ObjectAnimator 的感觉,只是不需要指定目标属性。运行效果跟上面一致就不贴图了。
Compose 函数中使用属性动画
前面在 Compose 中使用的动画都是创建在 Compose 函数外面的,如果我们想把这个组件封装成一个独立的 Compose 组件就需要将动画的创建放到 Compose 函数里面,比如将上面的效果封装成一个 AnimationBox
组件:
@Composable
fun AnimationBox(){
val topPadding = mutableStateOf(10)
val animator = animatorOfInt(topPadding, 10, 100)
animator.duration = 1000
Box(modifier = Modifier.padding(start = 10.dp, top = topPadding.value.dp)
.size(100.dp)
.background(Color.Blue)
.clickable {
animator.start()
})
}
首先 mutableStateOf 会报错:
意思是在组合过程中创建 state 需要使用 remember
,原因是当 state 里的值发生变化时 Compose 会进行重组导致函数重新执行,如果 mutableStateOf 不加 remember
则会每次重组都重新创建 state,导致 UI 上使用的值每次都是初始值而得不到刷新。
既然报错那就给他加上 remember
:
@Composable
fun AnimationBox(){
val topPadding = remember { mutableStateOf(10) }
...
}
然后在使用的地方直接使用 AnimationBox() 即可:
setContent {
AnimationBox()
}
运行后发现效果跟之前一样,那是不是就可以了呢?
实际上上面的代码是还存在问题的,前面说在 Compose 重组时会重新执行 Compose 组件的代码,也就是在界面刷新时会多次重复创建动画对象,我们在 animatorOfInt 函数里添加一个日志再看看运行时的日志输出:
fun animatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
println("-------call animatorOfInt--------")
...
}
输出结果:
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
I/System.out: -------call animatorOfInt--------
日志确实输出了多次,意味着动画确实创建了多次,那怎么解决呢?
前面说了 remember
可以解决重组时重复创建的问题,所以只需在创建动画上套上 remember
即可,如下:
val animator = remember { animatorOfInt(topPadding, 10, 100) }
修改后再看日志,发现就只在第一次进行了创建,动画执行过程中并没有再次创建。
为了方便使用,可以再封装一个 rememberAnimatorOfInt
方法:
@Composable
fun rememberAnimatorOfInt(state:MutableState<Int>, vararg values: Int) : ValueAnimator{
return remember { animatorOfInt(state, *values) }
}
在 animatorOfInt 上套了一个 remember,这样使用时就可以直接使用 rememberAnimatorOfInt 方法:
val animator = rememberAnimatorOfInt(topPadding, 10, 100)
remember 是 Compose 提供的在 Compose 函数中缓存状态的方法,解决在 Compose 重组时重复创建的问题,关于 remember 更多使用大家可以自行查询相关资料,本专栏主要讲解动画的使用就不过多赘述。
实战
前面介绍了属性动画在 Compose 中的运用,那在实际开发中到底好不好用呢?接下来我们通过一个实例来看看。
先看一下最终实现的效果:
一个上传按钮的动画效果,动画主要分为三阶段:
上传开始
按钮从圆角矩形变成圆形
按钮颜色从蓝色变成中间白色,边框灰色
文字逐渐消失
上传进度
边框根据进度变为蓝色
上传完成
按钮从圆形变成圆角矩形
按钮颜色变成红色
文字逐渐显示,且文字变为 “Success”
上传开始动画
先把按钮的初始状态使用 Compose 实现:
@Composable
fun UploadButton() {
Box(
modifier = Modifier
.padding(start = 10.dp, top = 10.dp)
.width(180.dp),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(24.dp))
.background(Color.Blue)
.size(180.dp, 48.dp),
contentAlignment = Alignment.Center,
) {
Text("Upload", color = Color.White)
}
}
}
运行效果如下:
下面就为这按钮添加动画,前面讲了动画主要作用于 State 上,所以需要先将使用到的数据提取成对应的状态:
@Composable
fun UploadButton() {
val originWidth = 180.dp
val circleSize = 48.dp
var text by remember { mutableStateOf("Upload") }
val textAlpha = remember { mutableStateOf(1.0f) }
val backgroundColor = remember { mutableStateOf(Color.Blue) }
val boxWidth = remember { mutableStateOf(originWidth) }
Box(
modifier = Modifier
.padding(start = 10.dp, top = 10.dp)
.width(originWidth),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(height/2))
.background(backgroundColor.value)
.size(boxWidth.value, height),
contentAlignment = Alignment.Center,
) {
Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha.value))
}
}
}
创建开始上传的动画:
@Composable
fun UploadButton() {
...
val uploadStartAnimator = remember {
// 创建 AnimatorSet
val animatorSet = AnimatorSet()
// 按钮宽度变化动画
val widthAnimator = animatorOfDp(boxWidth, arrayOf(originWidth, circleSize))
// 文字消失动画
val textAnimator = animatorOfFloat(textAlpha, 1f, 0.0f)
// 按钮颜色动画
val colorAnimator = animatorOfColor(backgroundColor, arrayOf(Color.Blue, Color.Gray))
// 动画添加到 AnimatorSet
animatorSet.playTogether(widthAnimator, textAnimator, colorAnimator)
animatorSet
}
Box(...) {
Box(
modifier = Modifier
...
.clickable {
// 点击执行动画
uploadStartAnimator.start()
},
...
)
}
}
分别创建按钮宽度、按钮颜色和文字 alpha 值变化的动画,因需同时执行多个动画,这里使用 AnimatorSet 进行同时执行,然后在按钮上添加点击事件进行动画执行。
上面的 animatorOfDp
、animatorOfFloat
、animatorOfColor
都是自定义封装的函数,封装方法与上面介绍的 animatorOfInt
基本相同,源码可通过文章最后附的源码地址进行查看。
运行效果如下:
好像还差点,中间应该是白色的,在 Box 下再添加一个白色圆形的 Box,默认 alpha 是 0,上传开始时 alpha 从 0 变成 1 :
@Composable
fun UploadButton() {
...
val progressAlpha = remember { mutableStateOf(0.0f) }
val uploadStartAnimator = remember {
...
// 中间白色透明度变化动画
val centerAlphaAnimator = animatorOfFloat(progressAlpha, 0.0f, 1f)
animatorSet.playTogether(widthAnimator, textAnimator, colorAnimator, centerAlphaAnimator)
animatorSet
}
Box(...) {
Box(...) {
// 白色圆形
Box(
modifier = Modifier.size(40.dp).clip(RoundedCornerShape(20.dp))
.alpha(progressAlpha.value).background(Color.White)
)
Text(text, color = Color.White, modifier = Modifier.alpha(textAlpha.value))
}
}
}
运行效果如下:
上传进度动画
这里通过自定义 clip 的一个弧形的 shape 来实现进度,自定义代码如下:
class ArcShape(private val progress: Int) : Shape {
override fun createOutline(
size: Size,
layoutDirection: LayoutDirection,
density: Density
): Outline {
val path = Path().apply {
moveTo(size.width / 2f, size.height / 2f)
arcTo(Rect(0f, 0f, size.width, size.height), -90f, progress / 100f * 360f, false)
close()
}
return Outline.Generic(path)
}
}
传入一个进度值(0-100),然后根据进度值算出一个绘制的弧度,使用这个自定义的 ArcShape 代码如下:
Box(Modifier.size(48.dp).clip(ArcShape(30)).background(Color.Blue))
效果:
所以只需动态改变 ArcShape 的 progress 参数的值就能实现上传进度效果,修改代码如下:
@Composable
fun PreviewUploadButton() {
...
val progress = remember { mutableStateOf(0) }
//上传进度动画
val progressAnimator = remember {
val animator = animatorOfInt(progress, 0, 100)
animator.duration = 1000
animator
}
val uploadStartAnimator = remember {
...
// 添加动画监听,完成后执行进度动画
animatorSet.addListener(onEnd = {
progressAnimator.start()
})
animatorSet
}
Box(...) {
Box(...) {
// 进度 Box
Box(
modifier = Modifier.size(height).clip(ArcShape(progress.value))
.alpha(progressAlpha.value).background(Color.Blue)
)
...
}
}
}
运行效果:
上传完成动画
最后是上传完成动画就很简单了,基本就是开始动画的反向,只是按钮颜色从蓝色变成了红色,动画在上传进度动画完成时执行:
@Composable
fun PreviewUploadButton() {
...
val endAnimatorSet = remember {
val animatorSet = AnimatorSet()
val widthAnimator = animatorOfDp(boxWidth, arrayOf(circleSize, originWidth))
val centerAnimator = animatorOfFloat(progressAlpha, 1f, 0f)
val textAnimator = animatorOfFloat(textAlpha, 0f, 1f)
val colorAnimator = animatorOfColor(backgroundColor, arrayOf(Color.Blue, Color.Red))
animatorSet.playTogether(widthAnimator, centerAnimator, textAnimator, colorAnimator)
animatorSet.addListener(onStart = {
text = "Success"
})
animatorSet
}
val progressAnimator = remember {
val animator = animatorOfInt(progress, 0, 100)
animator.duration = 1000
animator.addListener(onEnd = {
endAnimatorSet.start()
})
animator
}
...
}
最终效果:
最后
通过本篇文章的探索可以发现属性动画在 Compose 中确实是可以使用的,虽然跟传统 UI 开发中使用属性动画有所区别,但确实能用,而且通过一个简单的实战示例发现好像还挺好用的。好了,我已经学会 Compose 的动画开发了,什么?Compose 还单独提供了一套动画 API ?
属性动画这不是挺好使的么,这不是多此一举么,难道 Compose 的动画 API 比属性动画还好用、还强大?如果感兴趣请关注本专栏,从下一篇开始带你真正走进 Compose 的动画世界。
源码地址:ComposeAnimationDemo
来源:https://juejin.cn/post/7147323811824664590


猜你喜欢
- Spring简介1.什么是Springspring是分层的JavaSE及JavaEE应用于全栈的轻量级开源框架,以 IoC (Inverse
- 之前我们学习了如何使用Jpa访问关系型数据库。通过Jpa大大简化了我们对数据库的开发工作。但是,之前的例子中我们只提到了最简单的CRUD(增
- 一、内部类介绍1.定义:一个类内部又嵌套了一个类,被嵌套的类就是内部类(inner class),嵌套其他类的称为外部类(outer cla
- 前言Spring 的 JDBC Templet 是 Spring 对 JDBC 使用的一个基本的封装。他主要是帮助程序员实现了数据库连接的管
- we can custom min heap or max heap by override the method compare.pack
- jar文件包括java普通类、资源文件和普通文件,在maven中即是打包src/main/java和src/main/resources资源
- 今天来写一个通用的筛选栏的实现,也是因为之前项目中要好多地方用到筛选栏这么个东西,所以为了之后用起来比较方便,就简单的做了一些封装.废话不多
- 上周,公司的项目改版要求加上一个右滑返回上一个界面,于是就在网上找了一些开源库打算实现.但是在使用的时候遇见了许多的问题.试了两天用过 ht
- 一、cancel()无效当协程任务被取消的时候,它的内部是会产生一个 CancellationException 的。而协程的结构化并发,最
- ContentProvider是内容提供者,可以跨进程提供数据。大家都知道,ContentProvider的启动,是在Application
- 一、类加载器类加载器(ClassLoader),顾名思义,即加载类的东西。在我们使用一个类之前,JVM需要先将该类的字节码文件(.class
- 以前一直使用Hibernate,基本上没用过Mybatis,工作中需要做映射关系,简单的了解下Mybatis的映射。两者相差不多都支持一对一
- 前言我们常说的字符串为空,其实就是一个没有字符的空数组。比如:String a = "";a 就可以称为是一个空字符串。
- 最近要做一个java web项目,因为页面不是很多,所以就没有前后端分离,前后端写在一起,这时候就用到thymeleaf了,以下是不动脑式的
- 本文实例为大家分享了android UI绘制加减号按钮的具体代码,供大家参考,具体内容如下在项目中我们常常会用到这么一个view。这时候我们
- Java的外部类为什么不能使用private和protected进行修饰对于这个问题,一直没有仔细思考,今天整理一下:对于顶级类(外部类)来
- WPF的InkCanvas就是一个画板,可以在上面随意涂鸦,每写上一笔,InkCanvas的Strokes集合里就新增一个涂鸦对象,下面的代
- Long end,long num,File file,String charset4个参数说明end 相当于坐标 ,tail 向上的起点,
- 前言在一些日常业务中,会遇到一些琐碎文件需要统一打包到一个压缩包中上传,业务方在后台接收到压缩包后自行解压,然后解析相应文件。而且可能涉及安
- Spring Security简介:Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块