Compose中的动画

12/11/2021 AndroidCompose动画

# 可见性动画

可见性动画指的是:当View的可见性发生变化时,有一个过渡效果。

@Composable
fun ColumnScope.AnimatedVisibility(
    visible: Boolean, // 当前是否可见
    modifier: Modifier = Modifier, // 布局修饰符
    enter: EnterTransition = fadeIn() + expandVertically(), // 不可见变为可见时的动画,默认展开并淡入
    exit: ExitTransition = fadeOut() + shrinkVertically(), // 可见变为不可见时的动画,默认折叠并淡出
    content: @Composable AnimatedVisibilityScope.() -> Unit // 要展示的内容
) 
1
2
3
4
5
6
7
8

其中EnterTransitionExitTransition重载了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))
        }
    }
}
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

效果如下所示:

默认动画

水平滑入滑出

# 布局大小动画

布局大小动画指的是:当布局的大小反正改变时,有一个过渡效果,我们先来看下不加动画的效果:

不加动画

代码如下:

@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 "展开")
        }
    }
}
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

然后看添加动画的,我们打开这行注释来让动画执行:

.animateContentSize(), // 关键属性,给内容变化添加动画
1

效果如下:

加布局动画

animateContentSize的API如下:

fun Modifier.animateContentSize(
    animationSpec: FiniteAnimationSpec<IntSize> = spring(), // 动画属性
    finishedListener: ((initialValue: IntSize, targetValue: IntSize) -> Unit)? = null // 
)
1
2
3
4

这里我们不去深究FiniteAnimationSpec的API,直接看Compose提供的spring()函数,因为它足够应对我们的需求。

@Stable
fun <T> spring(
    dampingRatio: Float = Spring.DampingRatioNoBouncy, // 阻尼系数,值越小,弹力越大,弹簧效果越明显,默认无弹力
    stiffness: Float = Spring.StiffnessMedium, // 衰减系数,值越大,衰减的越快,也就是折叠的速度越快,默认衰减系数为"中"
    visibilityThreshold: T? = null // 可见性阀值(不懂!)
)
1
2
3
4
5
6

现在,让我们来改变阻尼的值,我们修改animateContentSize为如下代码:

.animateContentSize(
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy, // 修改为弹性为强
    )
), // 关键属性,给内容变化添加动画
1
2
3
4
5

效果如下:

提高弹性系数

我们修改了弹性系数为强,也就是阻尼系数为最低,发现弹的更有劲儿了

接着,让我们来改变衰减系数的值,我们来修改animateContentSize为如下代码:

.animateContentSize(
    animationSpec = spring(
        stiffness = Spring.StiffnessVeryLow // 修改衰减系数为最低
    )
), // 关键属性,给内容变化添加动画
1
2
3
4
5

修改衰减系数为最小

修改衰减系数为最低,会发现衰减的慢了,也就是动画事件变长了。

如果我们将上述两个改变合并呢?如下:

.animateContentSize(
    animationSpec = spring(
        dampingRatio = Spring.DampingRatioHighBouncy, // 修改弹性系数为高
        stiffness = Spring.StiffnessVeryLow // 修改衰减系数为最低
    )
), // 关键属性,给内容变化添加动画
1
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 = "这是其他屏幕")
    }
}
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

在代码中,直接通过一个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()
            }
        }
    }
}
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

添加动画后的效果:

切换布局-添加动画

我们只是使用Crossfade来包括切换屏幕的代码,就实现了切换的动画效果,这要比原生的xml布局方面很多。Crossfade的API如下:

@Composable
fun <T> Crossfade(
    targetState: T, // 状态值
    modifier: Modifier = Modifier,
    animationSpec: FiniteAnimationSpec<Float> = tween(), // 动画值
    content: @Composable (T) -> Unit
)
1
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)
1
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
    )
}
1
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
    ),
)
1
2
3
4
5
6
7

效果如下:

可以看到,现在颜色的切换有个渐变的效果了。其中animateColorAsState就是添加值动画的API,如下:

fun animateColorAsState(
    targetValue: Color, // 目标颜色值
    animationSpec: AnimationSpec<Color> = colorDefaultSpring, // 动画
    finishedListener: ((Color) -> Unit)? = null // 动画完成的回调
)
1
2
3
4
5

其中的重点是animationSpec,通过这个来指定动画效果,默认就是我们前面说的布局大小动画里面的spring

animateXXAsState一个系列,可以根据不同场景选择不同的函数。

Last Updated: 1/28/2022, 4:13:43 PM