AnimatedVisibility
对单个控件做显示和隐藏的动画效果
在上一个章节我们学习了 Transition 动画,这一节学习它更加上层的 Api,但其实内部本质上使用的也是 Transition, 先看一下 AnimatedVisibility 的源码, 先看看里面的一些参数配置, 了解了一些参数配置, 就很简单了
1
2
3
4
5
6
7
8
9
10
11
12
|
@Composable
fun ColumnScope.AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandVertically(),
exit: ExitTransition = fadeOut() + shrinkVertically(),
label: String = "AnimatedVisibility",
content: @Composable AnimatedVisibilityScope.() -> Unit
) {
val transition = updateTransition(visible, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
|
简单分析一下这几个方法参数, 基本上就知道怎么使用了,
- 第一个参数是 visible 是控制是否显示
- 第二个参数是 modifier 修饰符
- 第三个参数是配置的入场动画, 默认参数是 fadeIn() + expandVertically(), 是一个淡入 + 垂直方向展开的一个动画
- 第四个参数是退场动画, 默认参数是 fadeOut() + shrinkVertically() 是一个淡出 + 垂直方向收缩动画
- 第五个参数可以当做是起的一个名称
- 最后一个参数的类型是一个带有
@Composable
注解的函数类型参数, 是我们编写组件的代码
在入场动画和出场动画配置中, 我们发现他的返回值是 EnterTransition 和 ExitTransition, 可以看看 fadeIn 和 fadeOut,淡入淡出的动画
1
2
3
4
5
6
7
8
9
10
11
12
13
|
fun fadeIn(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
initialAlpha: Float = 0f
): EnterTransition {
return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}
fun fadeOut(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
targetAlpha: Float = 0f,
): ExitTransition {
return ExitTransitionImpl(TransitionData(fade = Fade(targetAlpha, animationSpec)))
}
|
这里可以发现他都是调用 EnterTransitionImpl 构造,并传入了 TransitionData 对象, 其实对动画渐变效果设置都在这个类里面了
1
2
3
4
5
6
|
internal data class TransitionData(
val fade: Fade? = null, // 淡入淡出动画
val slide: Slide? = null, // 滑动
val changeSize: ChangeSize? = null, // 大小的裁切 (会改变画面大小的动画都是这个参数,比如 shrinkOut)
val scale: Scale? = null // 缩放动画
)
|
所以, 我们不管配置的入场还是出场动画效果, 最终都会保存在这个类中
还有一个问题就是我们看到配置动画的方式是用 “ + ” 加号来完成的, 那么他是如何做到的呢, 其实他使用的也是中缀函数, 上几个篇章我们已经看到它很多身影了, 源码是这样的
1
2
3
4
5
6
7
8
9
10
|
operator fun plus(enter: EnterTransition): EnterTransition {
return EnterTransitionImpl(
TransitionData(
fade = data.fade ?: enter.data.fade,
slide = data.slide ?: enter.data.slide,
changeSize = data.changeSize ?: enter.data.changeSize,
scale = data.scale ?: enter.data.scale
)
)
}
|
如果以默认参数 fadeIn() + expandVertically() 调用的来看的话, 最终赋值的参数就是 fade 和 changeSize , 其实就是配置 TransitionData 的 4 个值, 但是如果相同的动画的画,比如 fade + fade 那么会被覆盖掉
最后可以看到内部也是调用的 updateTransition 方法, 这个上节我们已经学过了, 最后一个小例子看看效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
fun TransitionPro() {
var show by remember { mutableStateOf(true) }
Column(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility( visible = show ) {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
}
Button(modifier = Modifier.alpha(0.3f), onClick = {
show = !show
}) {
Text(text = "测试")
}
}
}
|
Transition 入场和出场动画
- fadeln:淡入动画。该动画会使对象逐渐从不可见到可见,产生渐变效果
- fadeOut:淡出动画。该动画会使对象逐渐从可见到不可见,产生渐变效果。
- slideOut:滑出动画。该动画会使对象在给定的方向上滑动离开屏幕。
- scaleln:缩放进入动画。该动画会使对象逐渐从较小的大小缩放到正常大小。
- scaleOut:缩放离开动画。该动画会使对象逐渐从正常大小缩放到较小的大小。
- expandln:展开动画。该动画会使对象从一个较小的尺寸逐渐展开到一个较大的尺寸。
- shrinkOut:收缩动画。该动画会使对象从一个较大的尺寸逐渐收缩到一个较小的尺寸。
- expandHorizontally:水平展开动画。该动画会使对象从一个较窄的宽度逐渐展开到一个较宽的宽度。
- expandVertically:垂直展开动画。该动画会使对象从一个较短的高度逐渐展开到一个较高的高度。
- shrinkHorizontally:水平收缩动画。该动画会使对象从一个较短的宽度逐渐收缩到一个较窄的宽度。
- shrinkVertically:垂直收缩动画。该动画会使对象从一个较高的高度逐渐收缩到一个较短的高度。
- slidelnHorizontally:水平滑入动画。该动画会使对象从给定方向的屏幕外滑入,直到达到正常位置。
- slidelnVertically:垂直滑入动画。该动画会使对象从给定方向的屏幕外滑入,直到达到正常位置。
- slideOutHorizontally:水平滑出动画。该动画会使对象从正常位置滑出至给定方向的屏幕外。
- slideOutVertically:垂直滑出动画。该动画会使对象从正常位置滑出至给定方向的屏幕外。
在 AnimatedVisibility 的入场和出场动画中, 一共可以配置这些参数, 下面我们一个一个场景分析, 介绍一些区别
fadeIn
1
2
3
4
5
6
|
fun fadeIn(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
initialAlpha: Float = 0f // 初始的透明度
): EnterTransition {
return EnterTransitionImpl(TransitionData(fade = Fade(initialAlpha, animationSpec)))
}
|
slideIn
1
2
3
4
5
6
7
8
9
10
11
|
@Stable
fun slideIn(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
initialOffset: (fullSize: IntSize) -> IntOffset, // 初始的偏移值, 其中 fullSize 是整个控件的大小
): EnterTransition {
return EnterTransitionImpl(TransitionData(slide = Slide(initialOffset, animationSpec)))
}
|
fadeOut
1
2
3
4
5
6
7
|
// 基本和 fadeIn 一致, 唯一 targetAlpha 不同, targetAlpha 表示的是目标状态的时候透明度
fun fadeOut(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
targetAlpha: Float = 0f,
): ExitTransition {
return ExitTransitionImpl(TransitionData(fade = Fade(targetAlpha, animationSpec)))
}
|
scaleOut
1
2
3
4
5
6
7
8
9
|
fun scaleOut(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
targetScale: Float = 0f, // 目标状态的缩放值
transformOrigin: TransformOrigin = TransformOrigin.Center // 从中心点缩放
): ExitTransition {
return ExitTransitionImpl(
TransitionData(scale = Scale(targetScale, transformOrigin, animationSpec))
)
}
|
场景一
其中一些参数说明都写在注释上了, 至于 animationSpec 的配置, 不再说明, 在之前的文章都有描述, 最终我们使用一些上面的几个动画, 看看效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
fun TransitionPro() {
var show by remember { mutableStateOf(true) }
Column(modifier = Modifier.fillMaxSize()) {
AnimatedVisibility(
visible = show, enter = fadeIn(animationSpec = tween(durationMillis = 2000)) + slideIn(animationSpec = tween(durationMillis = 2000)) {fullsize->
IntOffset(fullsize.width / 2, 0)
}, exit = fadeOut(animationSpec = tween(4000)) + scaleOut(animationSpec = tween(durationMillis = 2000),targetScale = 0.5f)
) {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Red)
)
}
Button(modifier = Modifier.alpha(0.3f), onClick = {
show = !show
}) {
Text(text = "测试")
}
}
}
|
这段代码的意思是
- 入场动画是一个淡入的动画, 并且配置的是 tween spec 设置的时间为 5000 毫秒, 加一个从 x 轴方向平移, fullsize.width / 2, 也就是控件的一半, 以初始状态控件的一半从右往左边滑入目标状态的过程
- 出场动画是淡出的动画, 并且配置的是 tween spec 设置的时间为 4000 毫秒, 加一个缩放动画, 配置的目标缩放状态是 0.5f, 也就是控件的一半
最终运行代码后, 效果如下
shrinkOut
shrinkOut 是一个收缩动画,会改变控件大小,它对应 TransitionData 中的属性是 changeSize , 首先看看 shrinkOut 的源码, 他的一些参数配置, 后面再看怎么使用的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
fun shrinkOut(
animationSpec: FiniteAnimationSpec<IntSize> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntSize.VisibilityThreshold
),
shrinkTowards: Alignment = Alignment.BottomEnd,
clip: Boolean = true,
targetSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
): ExitTransition {
return ExitTransitionImpl(
TransitionData(
changeSize = ChangeSize(shrinkTowards, targetSize, animationSpec, clip)
)
)
}
|
shrinkOut 中有不同的地方是 shrinkTowards 和 clip
- clip : 是否剪裁动画边界外的内容,默认为 true 会对控件进行裁切,false 不会对控件进行裁切, 只做动画的位移效果
- shrinkTowards :表示收缩的目标方向,也就是当前位置往哪个方向收缩, 和 expand 相反的作用 默认 Alignment.BottomEnd, 也就是右下角
场景二
这里我们先设置 clip 都为 true 的情况, 也就是默认情况下的效果
1
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
@Preview
@Composable
fun ShrinkExample() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
var isVisibleCenter by remember { mutableStateOf(true) }
var isVisibleBottomEnd by remember { mutableStateOf(true) }
var isVisibleTopStart by remember { mutableStateOf(true) }
AnimatedVisibility(
visible = isVisibleCenter,
enter = expandIn(),
exit = shrinkOut(animationSpec = tween(3000),shrinkTowards = Alignment.Center, clip = true )
) {
Card(
modifier = Modifier
.size(150.dp)
.padding(16.dp)
.clickable { isVisibleCenter = !isVisibleCenter }
) {
Text(
text = "Center with clip",
modifier = Modifier.align(Alignment.Start),
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.height(16.dp))
AnimatedVisibility(
visible = isVisibleBottomEnd,
enter = expandIn(),
exit = shrinkOut(animationSpec = tween(3000), shrinkTowards = Alignment.BottomEnd,clip = true)
) {
Card(
modifier = Modifier
.size(150.dp)
.padding(16.dp)
.clickable { isVisibleBottomEnd = !isVisibleBottomEnd }
) {
Text(
text = "Bottom End(右下角)",
modifier = Modifier.align(Alignment.Start),
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.height(16.dp))
AnimatedVisibility(
visible = isVisibleTopStart,
enter = expandIn(),
exit = shrinkOut(animationSpec = tween(3000), shrinkTowards = Alignment.TopStart,clip = true)
) {
Card(
modifier = Modifier
.size(150.dp)
.padding(16.dp)
.clickable { isVisibleTopStart = !isVisibleTopStart }
) {
Text(
text = "Top Start(左上角)",
modifier = Modifier.align(Alignment.Start),
textAlign = TextAlign.Center
)
}
}
}
}
|
代码运行后如下
这是分别设置为中心点, 右下角和左上角的收缩的目标方向, 并且是裁切画面的效果, 接下来, 这3种动画其余的代码都不变化, 只是修改一个参数 clip 为 false 再看看效果
可以看得出, 唯一的区别是, 收缩的时候, 对画面没有进行裁切, 在动画结束后,才隐藏
slide
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
fun slideIn(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
initialOffset: (fullSize: IntSize) -> IntOffset, // 初始状态
): EnterTransition {
return EnterTransitionImpl(TransitionData(slide = Slide(initialOffset, animationSpec)))
}
fun slideOut(
animationSpec: FiniteAnimationSpec<IntOffset> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntOffset.VisibilityThreshold
),
targetOffset: (fullSize: IntSize) -> IntOffset, // 目标终点状态
): ExitTransition {
return ExitTransitionImpl(TransitionData(slide = Slide(targetOffset, animationSpec)))
}
|
slideIn 是一个滑入动画, slideOut 是一个滑出动画,设置的参数都不多,一个是动画的 animationSpec,还是一个就是滑入或者滑出的起点或者终点位置,接下来看看这个例子
场景三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
AnimatedVisibility(visible = show, enter = slideIn(animationSpec = tween(durationMillis = 1500)) {
/** 从初始状态 --it.width, -it.height 到 fullSize 的宽度 **/
IntOffset(-it.width, -it.height)
}, exit = fadeOut(animationSpec = tween(durationMillis = 1500)) + shrinkOut(
animationSpec = tween(durationMillis = 1500), shrinkTowards = Alignment.CenterStart, clip = false
) {
/** 从初始状态切换到目标状态, 也就是 fullzise 切换到 0 的大小 **/
IntSize(0, 0)
}) {
Box(
modifier = Modifier
.size(200.dp)
.clip(RoundedCornerShape(40.dp))
.background(Color.Green)
)
}
|
上面的代码就是动画出现的时候是以 -it.width, -it.height 的起始坐标点,也就是左上角屏幕外的位置,然后以 tween 1500 毫秒的时间滑入到 fullSize 的位置,控件隐藏的动画是淡出的效果加收缩动画,并且设置了 clip 为不裁切画面的方式,运行后效果如下
expandIn
expandIn 是一个展开动画,和 shrinkOut 刚好相反,其中他也是个会改变控件大小的动画,参数基本也和 shrinkOut 一样,唯一不同的是 expandFrom ,表示的是从哪里展开,也就是起始开始位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
fun expandIn(
animationSpec: FiniteAnimationSpec<IntSize> =
spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntSize.VisibilityThreshold
),
expandFrom: Alignment = Alignment.BottomEnd,
clip: Boolean = true,
initialSize: (fullSize: IntSize) -> IntSize = { IntSize(0, 0) },
): EnterTransition {
return EnterTransitionImpl(
TransitionData(
changeSize = ChangeSize(expandFrom, initialSize, animationSpec, clip)
)
)
}
|
场景四
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
AnimatedVisibility(visible = show, exit = shrinkOut(
animationSpec = tween(durationMillis = 5000),
shrinkTowards = Alignment.BottomEnd, clip = true
), enter = expandIn(
animationSpec = tween(durationMillis = 5000), expandFrom = Alignment.TopStart, clip = true
) {
IntSize(0, 0)
}) {
Box(
modifier = Modifier
.size(156.dp)
.clip(RoundedCornerShape(40.dp))
.background(Color.Red)
)
}
|
这段代码配置的是入场是一个展开动画,且是从 TopStart 左上角扩大的,退场动画是从 BottomEnd 右下角收缩的一个动画,运行后效果如下
scaleIn scaleOut
scaleIn 和 scaleOut 是一个缩放动画,scaleIn 是代表入场,scaleOut 代表的是退场,需要注意的是,他和 expandIn 和 shrinkOut 不一样,这两个会改变控件的大小,会对控件进行裁切,而 scaleIn scaleOut 不会,scaleIn scaleOut 只是对控件本身进行大小缩放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
fun scaleIn(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
initialScale: Float = 0f, // 初始状态缩放大小
transformOrigin: TransformOrigin = TransformOrigin.Center, // 缩放的相对位置 (Center 代表的是中心点)
): EnterTransition {
return EnterTransitionImpl(
TransitionData(scale = Scale(initialScale, transformOrigin, animationSpec))
)
}
fun scaleOut(
animationSpec: FiniteAnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
targetScale: Float = 0f, // 目标状态缩放大小
transformOrigin: TransformOrigin = TransformOrigin.Center // 缩放的相对位置 (Center 代表的是中心点)
): ExitTransition {
return ExitTransitionImpl(
TransitionData(scale = Scale(targetScale, transformOrigin, animationSpec))
)
}
|
这里的参数区别就是 initialScale 和 targetScale 配置的区别了,然后他们都是一个 scale 动画,配置在 TransitionData 对象中的 scale 属性中
1
2
3
4
5
6
7
8
9
10
11
12
|
AnimatedVisibility(
visible = show, exit = scaleOut(animationSpec = tween(durationMillis = 1000)), enter = scaleIn(
animationSpec = tween(4000), initialScale = 0.3f, transformOrigin = TransformOrigin(0f, 0f)
)
) {
Box(
modifier = Modifier
.size(156.dp)
.clip(RoundedCornerShape(40.dp))
.background(Color.Red)
)
}
|
这段代码配置的入场动画是初始缩放大小为 0.3f,缩放的相对位置为 0f,0f 也就是左上角的位置,退场动画为 tween 为1000 毫秒,而相对位置默认参数的话,就是中间位置,目前状态缩放比例是 0f, 也就是完全消失,最后运行代码看看效果
AnimatedVisibility 总结
好了,AnimatedVisibility 的使用和配置基本上都讲完了,本质上它是对单个控件的显示和隐藏做渐变效果,也就是入场和出场的动画,而入场和出场的动画都是对 TransitionData 中的 fade slide changeSize scale 这4个属性做配置,他们的对应关系是这样的
- fade 淡入淡出效果
- fadeln 淡入动画,该动画会使对象逐渐从不可见到可见,产生渐变效果
- fadeOut 淡出动画,该动画会使对象逐渐从可见到不可见,产生渐变效果
- slide 滑入滑出效果
- slideOut 滑出动画
- slideIn 滑入动画
- slidelnHorizontally 水平滑入动画
- slidelnVertically 垂直滑入动画
- slideOutHorizontally 水平滑出动画
- slideOutVertically 垂直滑出动画
- changeSize 收缩动画效果 (会对控件大小进行裁切)
- expandIn 展开动画
- shrinkOut 收缩动画
- expandHorizontally 水平展开动画
- expandVertically 垂直展开动画
- shrinkHorizontally 水平收缩动画
- shrinkVertically 垂直收缩动画
- scale 缩放动画效果
- scaleln:缩放进入动画
- scaleOut 缩放退场动画
好了,基本上 AnimatedVisibility 能使用的动画就是这些,在本文中对于 xxxVertically 和 xxxHorizontally 并没有讲,是因为他们本质上就是再上一层的封装,比如 slideIn 动画,而对于 slidelnVertically 只是针对一个方向上的,而 slideIn 可以完全自由配置你想要的坐标偏移,而你学会了 slideIn 的动画,对于单个方向的自然也就会了
CrossFade
对于 AnimatedVisibility 动画而言,他是作用于单个控件的显示和隐藏做渐变动画的,而 CrossFade 就是两个控件,一个消失另一个显示出来,动画效果是淡入淡出,并对尺寸进行处理 (瞬间改变)。Crossfade 只支持这一种动画效果,是对于 AnimatedContent 的简化版本,AnimatedContent 稍后进行介绍
照例看看 Crossfade 源码
1
2
3
4
5
6
7
8
9
10
11
|
@Composable
fun <T> Crossfade(
targetState: T, // 初始状态
modifier: Modifier = Modifier,
animationSpec: FiniteAnimationSpec<Float> = tween(),
label: String = "Crossfade",
content: @Composable (T) -> Unit
) {
val transition = updateTransition(targetState, label)
transition.Crossfade(modifier, animationSpec, content = content)
}
|
Crossfade 的源码也比较简单,animationSpec 的配置只能是 FiniteAnimationSpec 的子类,也就是 TweenSpec, SpringSpec, KeyframesSpec, RepeatableSpec, SnapSpec 这些,如果你再往 Crossfade 里面看的话,可以看到他对 alpha 做渐变效果而已,也就是看到的淡入淡出
1
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
|
fun test22() {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
var showPicture by remember { mutableStateOf(false) }
Crossfade(targetState = showPicture, label = "") {
if (it) {
Image(
contentScale = ContentScale.Crop,
painter = painterResource(id = R.mipmap.aaa),
modifier = Modifier.width(300.dp),
contentDescription = null
)
} else {
Box(
Modifier
.height(300.dp * 9 / 16)
.width(300.dp)
.background(Color.Blue)
)
}
}
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = { showPicture = !showPicture }) {
Text(text = "切换")
}
}
}
|
代码也很简单,使用 showPicture 控制图片的显示和隐藏,运行效果如下
CrossFade 尺寸不一样
如果切换的两个组件,尺寸不一致,就会出现如下情况
如果转场前是大尺寸,转场后是小尺寸,会等转场快结束的时候,变成小尺寸
如果转场前是小尺寸,转场后是大尺寸,会在转场刚开始的时候,变成大尺寸
1
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
|
@Preview
@Composable
fun test222() {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
var showPicture by remember { mutableStateOf(false) }
Crossfade(targetState = showPicture, label = "", animationSpec = tween(2000)) {
if (it) {
Image(
contentScale = ContentScale.Crop,
painter = painterResource(id = R.mipmap.aaa),
modifier = Modifier.width(100.dp),
contentDescription = null
)
} else {
Box(
Modifier
.height(300.dp * 9 / 16)
.width(300.dp)
.background(Color.Blue)
)
}
}
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = { showPicture = !showPicture }) {
Text(text = "切换")
}
}
}
|
AnimatedContent
上面说到 crossFade 是作用于两个控件的,而 AnimatedContent 用来控制多个组件的入场和出场,同时还能对入场和出场效果做定制,AnimatedContent 出入场动画效果的尺寸是渐变的,这是区别与 crossFade 的一点,AnimatedContent 的源码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Composable
fun <S> AnimatedContent(
targetState: S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentTransitionScope<S>.() -> ContentTransform = {
(fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)))
.togetherWith(fadeOut(animationSpec = tween(90)))
},
contentAlignment: Alignment = Alignment.TopStart,
label: String = "AnimatedContent",
contentKey: (targetState: S) -> Any? = { it },
content: @Composable() AnimatedContentScope.(targetState: S) -> Unit
)
|
这里重要的参数就是 transitionSpec 了,transitionSpec 是一个高阶函数,他可以配置入场和出场的动画,同时提供了一个 AnimatedContentTransitionScope 的上下文环境,返回的对象是一个 ContentTransform 对象,其中 AnimatedContentTransitionScope 内部有一个 using 方法,可以配置大小的改变,而 ContentTransform 是用来配置入场和出场的动画的
1
2
3
4
5
6
7
8
9
|
class ContentTransform(
val targetContentEnter: EnterTransition, // 入场动画
val initialContentExit: ExitTransition, // 退场动画
targetContentZIndex: Float = 0f, // 配置轴, 控件上下的顺序
sizeTransform: SizeTransform? = SizeTransform() // 控制大小
)
// togetherWith 中缀函数,用来配置退场动画
infix fun EnterTransition.togetherWith(exit: ExitTransition) = ContentTransform(this, exit)
|
而上面的默认参数的意思就是入场:淡入 + 缩放动画,退场使用 togetherWith 配置了一个淡出的退场动画,togetherWith 是一个中缀函数,它其实就是配置了 initialContentExit 属性,基本参数配置就是这些,回到之前的例子看看效果
大小尺寸的变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Composable
fun test232() {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
var showPicture by remember { mutableStateOf(false) }
AnimatedContent(showPicture) {
if (it) {
Image(
contentScale = ContentScale.Crop,
painter = painterResource(id = R.mipmap.aaa),
modifier = Modifier.width(100.dp),
contentDescription = null
)
} else {
Box(
Modifier.height(300.dp * 9 / 16).width(300.dp).background(Color.Blue)
)
}
}
Spacer(modifier = Modifier.height(10.dp))
Button(onClick = { showPicture = !showPicture }) {
Text(text = "切换")
}
}
}
|
入场和出场
配置入场和退场的动画效果,模拟 activity 的转场动画
1
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
|
fun AnimContent() {
var show by remember { mutableStateOf(true) }
Column(modifier = Modifier.fillMaxSize()) {
AnimatedContent(
targetState = show, transitionSpec = {
/** 配置一个 activity 的转场动画 **/
slideIn(
animationSpec = tween(durationMillis = 1000)
) {
/** 配置初始状态值 **/
IntOffset(it.width, 0)
} togetherWith slideOut(animationSpec = tween(1000)) {
/** 目标状态,移除屏幕外 **/
IntOffset(-it.width, 0)
}
}, label = ""
) {
if (it) square() else round()
}
Button(onClick = { show = !show }) {
Text(text = "测试")
}
}
}
|
裁切画面
配置状态的切换
1
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
|
@Composable
fun TimeTest() {
var count by remember { mutableIntStateOf(0) }
Column(modifier = Modifier.fillMaxSize()) {
Button(onClick = { count-- }) {
Text(text = "-")
}
Spacer(modifier = Modifier.padding(20.dp))
AnimatedContent(targetState = count, label = "", transitionSpec = {
/** 使用 SizeTransform clip false 可以看到画面不裁切的效果,更加明显 **/
if (targetState > initialState) { // 等价于 targetState isTransitioningTo initialState
// 递增
(slideInVertically { it } + fadeIn() togetherWith slideOutVertically { -it } + fadeOut()) using SizeTransform(clip = true)
} else {
// 递减
(slideInVertically(animationSpec = tween(3000)) { -it } + fadeIn() togetherWith slideOutVertically(animationSpec = tween(3000)) { it } + fadeOut()) using SizeTransform(
clip = true
)
}
}) {
Box(
modifier = Modifier
.background(Color.Green)
.padding(5.dp)
) {
Text(text = "count $it", fontSize = 28.sp)
}
}
Spacer(modifier = Modifier.padding(20.dp))
Button(onClick = { count++ }) {
Text(text = "+")
}
}
}
|
这段代码配置了一个 count 递增和递减的一个动画
- count 递增的时候
- 入场:初始状态以 fullHeight 的位置开始垂直方向滑入,且是一个淡入的效果
- 出场:以初始状态垂直方向滑入到目标状态为 -fullHeight 的位置,且是一个淡出的效果
- count 递减的时候:
- 入场:初始状态以 - fullHeight 的位置开始垂直方向滑入,且是一个淡入的效果
- 出场:以初始状态垂直方向滑入到目标状态为 fullHeight 的位置,且是一个淡出的效果
同时在递增和递减的过程中,都用 using 配置了 SizeTransform ,他可以理解为在这个入场和出场过程中,额外需要配置尺寸变化的,那么就使用它,这里我们配置了 clip 为 true,会对画面进行裁切,其实也就是默认情况下的效果,运行后看看效果
接下来我们再修改为 clip 为 false,也就是对画面不进行裁切,看看效果
设置为 clip 为 false,不裁切画面,可以发现可以看到动画位移的效果
targetContentZIndex
主要是用于控制组件的层级关系,默认值是 0f,数值较大的控件会覆盖较小的控件,如果数值相同,目标状态控件在最上面
它的场景,一般是用于想让动画在切换过程中,想让某一个控件无论是入场或者是退场动画中,控件一直在最上层显示
1
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
|
@Composable
fun test2() {
var show by remember { mutableStateOf(true) }
/** 控制内容显示 **/
Column(modifier = Modifier.fillMaxSize()) {
// 修改 targetContentZIndex 的情况
AnimatedContent(
targetState = show, transitionSpec = {
/** 目标状态是 true 的时候,这里动画的目标状态其实就是 square() 显示 **/
if (targetState) {
(fadeIn(tween(3000)) togetherWith fadeOut(tween(6000))).apply {
/** 配置出场的动画 **/
/** 绘制顺序,小的数值后绘制,大的先绘制,那么大的就会被盖在小的上面 **/
/** 这样配置,就是 square 显示的时候,square 后绘制,square 一只在最底层,所以红色的会一直盖在上面 **/
targetContentZIndex = -1f
}
} else fadeIn(tween(durationMillis = 3000)) togetherWith fadeOut(tween(3000))
}, label = ""
) {
if (it) square() else round()
}
Spacer(modifier = Modifier.height(20.dp))
// 默认情况下
AnimatedContent(
targetState = show, transitionSpec = {
/** 目标状态是 true 的时候,这里动画的目标状态其实就是 square() 显示 **/
if (targetState) {
(fadeIn(tween(durationMillis = 3000)) togetherWith fadeOut(tween(3000)))
} else (fadeIn(tween(durationMillis = 3000)) togetherWith fadeOut(tween(3000))) using SizeTransform()
}, label = ""
) {
if (it) square() else round()
}
Button(onClick = { show = !show }) {
Text(text = "测试$show")
}
}
}
|
这段代码是使用 show 控制正方形和圆角矩形的切换效果的, true 状态的时候显示的是正方形,false 的时候显示圆角矩形, 所以正常情况下
- true 到 false 的状态转变是,正方形慢慢消失,圆角矩形慢慢显示,且圆角矩形慢慢的盖在正方形上面
- false 到 true 的状态转变是,圆角矩形慢慢消失,正方形慢慢显示,且正方形慢慢的盖在圆角矩形上面
而如果我想让正方形出现的时候,圆角矩形一直盖在正方形的上面,直到动画结束才消失,那么就可以修改正方形的 targetContentZIndex 比圆角矩形的小,这种状态的转变也就是 false 到 true 的转变
Modifier.animateEnterExit
为子项单独设置进入和退出的动画, animateEnterExit 外层必须是 AnimatedVisibility
1
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
|
@Composable
fun anim1() {
var visible by remember {
mutableStateOf(false)
}
AnimatedVisibility(
// visible = visible, enter = fadeIn(tween(durationMillis = 2000)), exit = fadeOut(tween(durationMillis = 2000))
visible = visible, enter = EnterTransition.None, exit = ExitTransition.None
) {
Box(
Modifier
.fillMaxSize()
.background(Color.Gray)
) {
Box(
Modifier
.align(Alignment.Center)
.animateEnterExit(
enter = slideInVertically(tween(durationMillis = 2000)), exit = slideOutVertically(tween(durationMillis = 2000))
)
.sizeIn(minWidth = 64.dp, minHeight = 64.dp)
.background(Color.Green)
)
}
}
LaunchedEffect(key1 = Unit, block = {
delay(2500)
visible = true
})
}
|
这个代码在控件显示的时候,同时子组件 box 设置了入场动画垂直滑入, 效果是这样的
如果不想要 AnimatedVisibility 的默认动画,就可以设置 NONE,这样就是单独使用子组件动画了
Modifier.animateContentSize
使用 animateContentSize,可让 Compose 组件大小发生变化的时候,具备动画的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@Composable
fun anim2() {
var maxLines by remember { mutableIntStateOf(2) }
Column {
Box(
modifier = Modifier
.padding(20.dp)
.fillMaxWidth()
.background(Color.Gray)
.animateContentSize(animationSpec = spring(dampingRatio = Spring.DampingRatioHighBouncy))
) {
Text(
text = "111...",
maxLines = maxLines
)
}
Button(onClick = { maxLines = 5 }) {
Text(text = "展开")
}
}
}
|
总结
AnimatedVisibility 是对单个控件做显示和隐藏的动画效果, CrossFade 是两个控件,而 AnimatedContent 是多个控件,其中 AnimatedContent 功能更加强大,可以对入场和出场动画做单独的配置。而它们其实也都是 Transition 动画,至此,关于 compose 所有关于动画的设置都基本讲完了