Compose中的动画
# 可见性动画
可见性动画指的是:当View的可见性发生变化时,有一个过渡效果。
@Composable
fun ColumnScope.AnimatedVisibility(
visible: Boolean, // 当前是否可见
modifier: Modifier = Modifier, // 布局修饰符
enter: EnterTransition = fadeIn() + expandVertically(), // 不可见变为可见时的动画,默认展开并淡入
exit: ExitTransition = fadeOut() + shrinkVertically(), // 可见变为不可见时的动画,默认折叠并淡出
content: @Composable AnimatedVisibilityScope.() -> Unit // 要展示的内容
)
2
3
4
5
6
7
8
其中EnterTransition
和ExitTransition
重载了plus()
运算符,可以直接使用+
来进行多个动画的合并,比如上面的展开+
淡入。
大部分情况下,我们不需要去手写各种复杂的动画,Compose为我们提供了几种内置的动画效果,我们可以按需使用。大家可以在EnterExitTransition.kt
这个文件中看到Compose默认提供的入场和出场动画。
现在让我们来使用下:
@Composable
fun VisibilityAnimation() {
val visible = remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
Button(onClick = { visible.value = !visible.value }) {
Text(text = "可见性动画")
}
// 1 默认动画,淡入淡出
AnimatedVisibility(visible = visible.value) {
Text(text = "豫章故郡,洪都新府,星分翼珍,地接衡庐,襟三江而带五湖,控蛮荆而引瓯越。", modifier = Modifier.size(150.dp))
}
// 2 水平滑入滑出
AnimatedVisibility(
visible = visible.value,
enter = slideInHorizontally(initialOffsetX = { -it }), //入场动画
exit = slideOutHorizontally(targetOffsetX = { -it })//出场动画
) {
Text(text = "豫章故郡,洪都新府,星分翼珍,地接衡庐,襟三江而带五湖,控蛮荆而引瓯越。", modifier = Modifier.size(150.dp))
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
效果如下所示:
# 布局大小动画
布局大小动画指的是:当布局的大小反正改变时,有一个过渡效果,我们先来看下不加动画的效果:
代码如下:
@Composable
fun LayoutChangeAnimation() {
val expand = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(10.dp)
) {
Text(
// 来一段长文本
text = "豫章故郡,洪都新府。星分翼轸,地接衡庐。襟三江而带五湖,控蛮荆而引瓯越。物华天宝,龙光射牛斗之墟;" +
"人杰地灵,徐孺下陈蕃之榻。雄州雾列,俊采星驰。台隍枕夷夏之交,宾主尽东南之美。都督阎公之雅望,棨戟遥临;" +
"宇文新州之懿范,襜帷暂驻。十旬休假,胜友如云;千里逢迎,高朋满座。腾蛟起凤,孟学士之词宗;紫电青霜,王将军之武库。" +
"家君作宰,路出名区;童子何知,躬逢胜饯",
modifier = Modifier
.padding(10.dp),
//.animateContentSize(), // 关键属性,给内容变化添加动画
maxLines = if (expand.value) Int.MAX_VALUE else 1 // 根据是否展开设置最大行数
)
// 点击 "展开"/"折叠"
Button(onClick = { expand.value = !expand.value }) {
Text(if (expand.value) "折叠" else "展开")
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
然后看添加动画的,我们打开这行注释来让动画执行:
.animateContentSize(), // 关键属性,给内容变化添加动画
效果如下:
animateContentSize
的API如下:
fun Modifier.animateContentSize(
animationSpec: FiniteAnimationSpec<IntSize> = spring(), // 动画属性
finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null //
)
2
3
4
这里我们不去深究FiniteAnimationSpec
的API,直接看Compose提供的spring()
函数,因为它足够应对我们的需求。
@Stable
fun <T> spring(
dampingRatio: Float = Spring.DampingRatioNoBouncy, // 阻尼系数,值越小,弹力越大,弹簧效果越明显,默认无弹力
stiffness: Float = Spring.StiffnessMedium, // 衰减系数,值越大,衰减的越快,也就是折叠的速度越快,默认衰减系数为"中"
visibilityThreshold: T? = null // 可见性阀值(不懂!)
)
2
3
4
5
6
现在,让我们来改变阻尼的值,我们修改animateContentSize
为如下代码:
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy, // 修改为弹性为强
)
), // 关键属性,给内容变化添加动画
2
3
4
5
效果如下:
我们修改了弹性系数为强,也就是阻尼系数为最低,发现弹的更有劲儿了。
接着,让我们来改变衰减系数的值,我们来修改animateContentSize
为如下代码:
.animateContentSize(
animationSpec = spring(
stiffness = Spring.StiffnessVeryLow // 修改衰减系数为最低
)
), // 关键属性,给内容变化添加动画
2
3
4
5
修改衰减系数为最低,会发现衰减的慢了,也就是动画事件变长了。
如果我们将上述两个改变合并呢?如下:
.animateContentSize(
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy, // 修改弹性系数为高
stiffness = Spring.StiffnessVeryLow // 修改衰减系数为最低
)
), // 关键属性,给内容变化添加动画
2
3
4
5
6
应该会弹性大了,并且衰减慢了,我们来看下效果:
确实如我们所料:弹性大了,衰减慢了。这里弹回的卡顿是因为振幅太大了,导致互相挤压,实际应用中可以调整弹性值来解决的,这里不必在意。
# 布局切换动画
布局切换动画是指:在布局切换的时候,添加一个过渡效果。我们先来看无动画的切换效果:
我们可以看到,无动画时,切换的非常生硬,就是一闪而过的感觉,对应的代码如下所示:
/**
* 布局切换动画
*/
@Composable
fun LayoutSwitchAnimation() {
var first by remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Button(onClick = { first = !first }, modifier = Modifier.padding(bottom = 10.dp)) { // 点击切换first的指,进而改变展示的布局
Text(text = "切换")
}
// 根据boolean值选择是否展示第一个屏幕
if (first) {
FirstScreen()
} else {
OtherScreen()
}
}
}
// 第一个屏幕内容
@Composable
fun FirstScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Yellow),
contentAlignment = Alignment.Center,
) {
Text(text = "这是第一个屏幕")
}
}
// 第二个屏幕内容
@Composable
fun OtherScreen() {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red),
contentAlignment = Alignment.Center
) {
Text(text = "这是其他屏幕")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
在代码中,直接通过一个boolean值first
来判断并切换布局,没有任何动画效果,看起来非常生硬。
现在我们来修改上述的LayoutSwitchAnimation()
函数,我们用Crossfade()
来包括切换布局的代码,如下:
@Composable
fun LayoutSwitchAnimation() {
var first by remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
Button(onClick = { first = !first }, modifier = Modifier.padding(bottom = 10.dp)) {
Text(text = "切换")
}
// 使用Crossfade来添加切换的动画效果
Crossfade(
targetState = first, // 状态值,根据这个来判断切换到哪个屏幕
animationSpec = tween(durationMillis = 2000) // 动画值
) { first ->
if (first) {
FirstScreen()
} else {
OtherScreen()
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
添加动画后的效果:
我们只是使用Crossfade
来包括切换屏幕的代码,就实现了切换的动画效果,这要比原生的xml布局方面很多。Crossfade
的API如下:
@Composable
fun <T> Crossfade(
targetState: T, // 状态值
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(), // 动画值
content: @Composable (T) -> Unit
)
2
3
4
5
6
7
其中只有两个参数需要特别注意: targetState
是状态值,我们可以根据这个值来决定使用哪个布局;animationSpec
是动画值,表示要用什么动画进行切换,默认是tween()
,也就是一个渐变动画,如下:
@Stable
fun <T> tween(
durationMillis: Int = DefaultDurationMillis, // 动画时长,默认300ms
delayMillis: Int = 0, // 延迟时间
easing: Easing = FastOutSlowInEasing // 动画效果,默认快出慢进
): TweenSpec<T> = TweenSpec(durationMillis, delayMillis, easing)
2
3
4
5
6
代码也很简单,看注释就行,其中easing
是关键点,我们不用自己去写各种特效,因为Compose已经给我们提供了多个默认的实现效果,在Easing.kt
这个文件下就能找到,感兴趣的可以自己试试,这里不再废话。
# 属性动画
值动画会对数值的改变加入一个过渡效果。比如我们想要点击一个Button后修改它的背景色。可以这样写:
@Composable
fun ValueChangeAnimation() {
var first by remember { mutableStateOf(true) }
val bgColor = if (first) Color.Green else Color.Red // 在绿色和红色之间切换
Text(
text = "切换",
Modifier
.padding(32.dp) // 外边距,等价于margin
.background(bgColor) // 背景色
.clickable { first = !first } // 点击就反转first的值
.padding(8.dp) // 内边距,等价于padding
)
}
2
3
4
5
6
7
8
9
10
11
12
13
效果如下:
现在让我们来添加动画,我们将改变颜色的代码改为:
val bgColor by animateColorAsState(
targetValue = if (first) Color.Green else Color.Red,
animationSpec = spring(
dampingRatio = Spring.DampingRatioLowBouncy,
stiffness = Spring.StiffnessVeryLow
),
)
2
3
4
5
6
7
效果如下:
可以看到,现在颜色的切换有个渐变的效果了。其中animateColorAsState
就是添加值动画的API,如下:
fun animateColorAsState(
targetValue: Color, // 目标颜色值
animationSpec: AnimationSpec<Color> = colorDefaultSpring, // 动画
finishedListener: ((Color) -> Unit)? = null // 动画完成的回调
)
2
3
4
5
其中的重点是animationSpec
,通过这个来指定动画效果,默认就是我们前面说的布局大小动画里面的spring
。
animateXXAsState一个系列,可以根据不同场景选择不同的函数。