0. 前言
前段时间对于Android中的Compose动画做了系统性的学习,相关文章发布在 Compose 动画
专栏里。系统性学完Compose动画后,又对此做了系统性的回顾,抽取其比较重要的部分,希望能帮助大家快速入门Compose动画,所以有了本篇文章 : 一个小时入门 Android Compose 动画
1. Compose中的动画API
我们先来看下官网上的图,看上去很复杂,东西很多
对此,我进行了重新的整理。其实需要重点关注的Compose
动画API
,有这些
接下来我们一个个的来看
2. AnimateXxxAsState
AnimateXxxAsState
用来实现单个值变化(大小、位置、颜色等)的动画
Xxx
代指很多单位,比如Dp
、Color
、Float
、Int
、Rect
等。
我们这里以Dp
和Color
为例
animateDpAsState
: 大小变化animateColorAsState
: 颜色变化
2.1 代码示例
下面这段代码点击后,Box
的尺寸和颜色会发生动画过渡的变化
var big by remember {
mutableStateOf(false)
}
//var size = if (big) 300.dp else 50.dp
val size by animateDpAsState(targetValue = if (big) 300.dp else 50.dp)
val color by animateColorAsState(targetValue = if (big) Color.Red else Color.Blue)
Box(
Modifier
.size(size)
.background(color)
.clickable {
big = !big
}
)
效果如下所示
3. AnimatedVisibility
控制UI组件的显示/隐藏,并可以自定义入场和出场时候的动画效果
3.1 入场和出场动画
AnimatedVisibility
通过enter
/exit
可以配置入场和出场动画效果,比如以下几个
- 淡入 : fadeIn / fadeout
- 缩放 : scaleIn / scaleOut
- 滑动 : slideIn / slideOut
- 展开 : expandIn / shrinkOut
3.1 代码示例
这段代码点击按钮后,会 显示/隐藏
图片
Column(horizontalAlignment = Alignment.CenterHorizontally) {
var visible by remember {
mutableStateOf(true)
}
AnimatedVisibility(visible = visible, enter = expandIn(), exit = shrinkOut()) {
Image(painter = painterResource(id = R.drawable.photot1), contentDescription = null)
}
Button(onClick = {
visible = !visible
}) {
Text(text = "显示/隐藏")
}
}
效果如下所示
4. Transition
用来管理多个动画,通过updateTransition()
来创建一个Transition
,再通过Transition
创建具体的动画
4.1 代码示例
这段使用Transition
实现的代码,效果和使用animateXxxAsState
实现的效果一样
var big by remember {
mutableStateOf(false)
}
val transition = updateTransition(targetState = big, label = "myTransition")
val size by transition.animateDp(label = "mySize") {
if (it) 300.dp else 50.dp
}
val color by transition.animateColor(label = "myColor") {
if (it) Color.Red else Color.Blue
}
//val size by animateDpAsState(targetValue = if (big) 300.dp else 50.dp)
//val color by animateColorAsState(targetValue = if (big) Color.Red else Color.Blue)
Box(
Modifier
.size(size)
.background(color)
.clickable {
big = !big
}
)
效果如下所示
4.2 为什么要有Transition?
animateXxAsState
可以实现一样的效果,那为什么还要有 Transition
这个API呢 ?
- 使用Transition可以对动画做很方便的管理
animateXxAsState
是面向值的,在多个动画多个状态的情况下存在不便于管理的问题Transition
是面向状态的,多个动画可以共用一个状态,能够做到统一的管理
Transition
支持Compose
动画预览- Transition支持Compose动画预览功能
- animateXxAsState当前不支持Compose动画预览
4.3 如何进入Compose动画预览界面 ?
点击Start Animation Preview
按钮即可进入
4.4 封装并复用Transition
在简单的场景下,在同一个页面中使用updateTransition
创建Transition
并直接操作它完成动画是没问题的。然而,如果需要处理一个具有许多动画属性的复杂场景,可以把Transition
动画的实现与用户界面分开,从而提升代码复用率和可维护性。
class TransitionBean(size: State<Dp>, color: State<Color>) {
val size by size
val color by color
}
@Composable
private fun updateMyTransition(big: Boolean): TransitionBean {
val transition = updateTransition(targetState = big, label = "myTransition")
val size = transition.animateDp(label = "mySize") {
if (it) 300.dp else 50.dp
}
val color = transition.animateColor(label = "myColor") {
if (it) Color.Red else Color.Blue
}
return TransitionBean(size, color)
}
@Composable
private fun TransitionTest() {
var big by remember {
mutableStateOf(false)
}
val transitionBean = updateMyTransition(big)
Box(
Modifier
.size(transitionBean.size)
.background(transitionBean.color)
.clickable {
big = !big
}
)
}
5. AnimationSpec
自定义动画规格 AnimationSpec
,类似于传统View体系中的差值器Interpolator
,但是比起差值器,又提供了更多的功能。
5.1 SpringSpec
基于弹簧的物理动画效果,通过spring()
进行调用。
- 很多动画内部AnimationSpec使用的默认值都是spring,比如animateXXXAsState以及updateTransition等
- 基于物理规律,使动画更真实自然
- 因为是基于物理规律的,所以无法指定动画执行时长,而是会基于物理规律来确定动画执行时长
5.1.1 SpringSpec的参数
dampingRatio
:弹簧的阻尼比,这个值越大,阻尼越大stiffness
:弹簧的刚度值,弹簧有多想回弹回去,这个值越大,回弹的越快visibilityThreshold
:当动画到达这个阈值会立即停止
5.1.2 代码示例
var big by remember {
mutableStateOf(false)
}
val size by animateDpAsState(
targetValue = if (big) 300.dp else 50.dp,
animationSpec = spring(Spring.DampingRatioMediumBouncy, Spring.StiffnessHigh, 0.1.dp)
)
Box(
Modifier
.size(size)
.background(Color.Blue)
.clickable {
big = !big
}
)
效果如下所示
5.2 TweenSpec
可指定规定时间完成动画,通过tween
进行调用。
可以使用Easing
来可以控制动画的节奏。
5.2.1 TweenSpec的参数
-
durationMillis : 动画执行时长
-
delayMillis : 动画延迟多久执行
-
easing : 用来控制动画的节奏
5.2.2 Easing
5.2.3 代码示例
这段代码指定了在2秒时间匀速线性地完成动画
var big by remember {
mutableStateOf(false)
}
val size by animateDpAsState(
targetValue = if (big) 300.dp else 50.dp,
animationSpec = tween(2000, easing = LinearEasing)
)
Box(
Modifier
.size(size)
.background(Color.Blue)
.clickable {
big = !big
}
)
效果如下所示
5.3 RepeatableSpec / InfiniteRepeatableSpec
RepeatableSpec
: 可循环播放的动画,需要包裹其他AnimateSpec
InfiniteRepeatableSpec
: 无限循环动画,需要包裹其他AnimateSpec
repeatable/infiniteRepeatable不支持spring,因为一个循环运动的弹簧是违背物理规律的
5.3.1 代码示例
这段代码使用RepeatableSpec
,循环播放动画5
次
val size by animateDpAsState(
targetValue = if (big) 300.dp else 50.dp,
animationSpec = repeatable(
iterations = 5,
tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
这段代码使用InfiniteRepeatableSpec
,无限循环播放动画
val size by animateDpAsState(
targetValue = if (big) 300.dp else 50.dp,
animationSpec = infiniteRepeatable(
tween(2000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
)
)
6. Animatable
Animatable
是Android Compose
动画的底层API
animateDpAsState
内部使用Animatable
实现Animatable
具有更高的可定制性- 如果
animateDpAsState
能够满足需求,就用animateDpAsState
就行了,否则才去使用Animatable
6.1 Animatable的参数
- initialValue : 初始值
- typeConverter : 转换到什么单位 (比如Dp)
- visibilityThreshold : 当动画到达这个阈值会立即停止
6.2 代码示例
通过调用animatable.animateTo
实现动画的过渡,animateTo
必须要在LaunchedEffect
内部执行
这段代码的动画效果,和使用AnimateDpAsState
的动画效果一致。
@Composable
private fun AnimatableTest() {
var big by remember {
mutableStateOf(false)
}
var size = if (big) 300.dp else 50.dp
val animatable = remember {
Animatable(size, Dp.VectorConverter)
}
LaunchedEffect(key1 = big) {
animatable.animateTo(size)
}
Box(
Modifier
.size(animatable.value)
.background(Color.Blue)
.clickable {
big = !big
}
)
}
效果如下所示
6.3 Compose里的协程 : LaunchedEffect
LaunchedEffect
是Compose
里的协程
为什么要专门为Compose
出一个专门的协程呢 ?
因为Compose中每次状态改变,Compose进行重组更改UI的时候,就会去重新执行相应的代码块
- 如果使用原本的协程,每次Compose的重组都会执行该代码,这肯定是不行的
- 而
LaunchedEffect
专门针对Compose
重组的这个特定,做了特定的处理,只有其传入的key
值发生变化的时候,才会去执行
LaunchedEffect(key1 = big) {
animatable.animateTo(size)
}
7. 为什么使用animateDpAsState,就可以实现动画渐变的效果 ?
其实我们点击AnimateDpAsState
内部的代码,可以发现其也是调用Animatable
来实现的。
val animatable = remember {
Animatable(size, Dp.VectorConverter)
}
LaunchedEffect(key1 = targetValue) {
animatable.animateTo(size)
}
也就是说,animateDpAsState
内部会去启动一个协程,通过animatable.animateTo
会在某一个时间段内,自动完成从当前值到目标值的计算,并且实时将该值通知给Compose
重组,从而实现页面界面的更新,达到动画渐变的效果。
而什么时候触发协程呢 ? 就是当协程传入的这个key
值targetValue
发生变化的时候。
8. 小结
到这里,我们就把Compose
的重点API
都讲完了,但是要想真正入门Compose
动画,还需要上手亲自上手去敲一下代码,真正实践下才能真的掌握。在此基础上,如果想要看更详细的Compose动画,欢迎看我的Compose动画 专栏