我正在参加「掘金·启航计划」。今天我们来说说动画,个人认为动画是客户端交互中十分重要的部分,好的用户体验,很多时候都依赖于有效且合理的动画设计。从今天开始,一起来学习下 Compose 系统下的动画实现吧。
概览
Compose 系统的动画 API,多数都采用高阶函数提供,而且,这些函数都使用了 @Composable 限制 —— 这表明,这些动画函数只能在 Compose 环境下调用。
上图是官方给的 Compose 动画接口概览,主要说明了在不同场景和需求下,应该如何选择动画的问题。其中,红框就是前面提到的高阶动画函数。举两个实现例子:
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
// ......
}
@Composable
fun <S> AnimatedContent(
targetState: S,
modifier: Modifier = Modifier,
transitionSpec: AnimatedContentScope<S>.() -> ContentTransform = {
fadeIn(animationSpec = tween(220, delayMillis = 90)) +
scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) with
fadeOut(animationSpec = tween(90))
},
contentAlignment: Alignment = Alignment.TopStart,
content: @Composable() AnimatedVisibilityScope.(targetState: S) -> Unit
) {
// ......
}
如果我们要在如下场景下加上动画:
- 布局改变时
- 作用目标为布局的显示/消失
那么按照概览图的说明,我们就应该使用 AnimatedVisibility 来实现。这和 View 中的 animateLayoutChanges 实现的效果类似,但是,可控性却强很多。animateLayoutChanges 属性只是一个开关,打开后,布局变化时,系统默认给一个动画,而 Compose 的 AnimatedVisibility 函数却能添加很多的自定义控制,比如动画效果、时长等等。
AnimatedVisibility
AnimatedVisibility 动画,顾名思义,就是关注组件的显示和隐藏的,其函数原型如下:
@Composable
fun AnimatedVisibility(
visible: Boolean,
modifier: Modifier = Modifier,
enter: EnterTransition = fadeIn() + expandIn(),
exit: ExitTransition = shrinkOut() + fadeOut(),
label: String = "AnimatedVisibility",
content: @Composable() AnimatedVisibilityScope.() -> Unit
) {
val transition = updateTransition(visible, label)
AnimatedEnterExitImpl(transition, { it }, modifier, enter, exit, content)
}
关键参数说明如下:
- visible - 组件是否可见
- enter - 组件进入过渡动画,默认是「渐显+扩展进入」
- exit - 组件退出过渡动画, 默认是「渐隐+缩小退出」
- content - 动画内容
参数的作用很明了了,现在直接来一个例子,看看效果吧。
@Composable
fun Greeting(name: String) {
val visible = remember { mutableStateOf(false) }
Column {
Button(onClick = { visible.value = !visible.value }) {
Text(text = if (visible.value) "隐藏" else "显示")
}
AnimatedVisibility(visible.value) {
Text(text = "Hello $name!", modifier = Modifier
.size(100.dp)
.background(Color.Gray), textAlign = TextAlign.Center)
}
}
}
很简单,动画目标是一个 Text 组件,进入和消失的动画,直接用默认的。Button 用于控制它的显示与隐藏。
如预期,组件的消失和隐藏,果然有了动画效果,如果没有 AnimatedVisibility 的加持,那它就只有很原始很纯粹的隐藏和消失了,如下:
回头再来看这个默认动画,它看起来是由两个动画组成的:渐显 + 顶部扩展进入,这就是前面说的「渐显+扩展进入」吗?
拿 enter 来说,它实际上是 fadeIn() + expandIn() 实现的,前者是「渐显」,后者是「扩展进入」,加起来的意思是:渐显的同时,从右下角开始扩展(函数原型定义的「从右下角」,什么意思呢?后文再说)。而这里我们实现的动画,确实是渐显的,但是扩展好像是从底部开始的?
一查原码,果然,这里的组件放到了 Column 里,调用的是 ColumnScope 下的 AnimatedVisibility,动画是有区别的:
@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)
}
如上,fadeIn() 之上叠加的是 expandVertically() 而非 expandIn(), expandVertically() 确实就是从底部开始扩展显示的。expandVertically() 还可以自定义,我们甚至可以把「扩展」改成从顶部开始:
// ...
AnimatedVisibility(visible.value, enter = fadeIn() + expandVertically(expandFrom = Alignment.Top)) {
// ...
}
仔细对比修改后的动画与默认的动画,可以发现:文本组件确实变成了从顶部开始「扩展显示」了 —— 当然,对比之下,我们也更好理解了什么叫做「扩展进入」以及方向的意义:从A方向扩展进入,就是说,要最先显示A方向。
既然有 ColumnScope 的下 AnimatedVisibility,自然也有 RowScope 的版本,对应的,默认情况下,它的扩展用的则是 expandHorizontally(),因为它需要的是横向的动画。
如果要更清楚地看看动画区别,可自行延长动画时间来观察
小结
今天初窥了下 Compose 的动画,并实验了一个最简单的「布局可见性」动画。这中间也忽略了很多的细节,改天咱继续讨论吧。