Compose动画初探

873 阅读4分钟

我正在参加「掘金·启航计划」。今天我们来说说动画,个人认为动画是客户端交互中十分重要的部分,好的用户体验,很多时候都依赖于有效且合理的动画设计。从今天开始,一起来学习下 Compose 系统下的动画实现吧。

概览

Compose 系统的动画 API,多数都采用高阶函数提供,而且,这些函数都使用了 @Composable 限制 —— 这表明,这些动画函数只能在 Compose 环境下调用。

image.png

上图是官方给的 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
) {
// ......
}

如果我们要在如下场景下加上动画:

  1. 布局改变时
  2. 作用目标为布局的显示/消失

那么按照概览图的说明,我们就应该使用 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 用于控制它的显示与隐藏。

device-2022-10-12-103604.gif

如预期,组件的消失和隐藏,果然有了动画效果,如果没有 AnimatedVisibility 的加持,那它就只有很原始很纯粹的隐藏和消失了,如下:

output.gif

回头再来看这个默认动画,它看起来是由两个动画组成的:渐显 + 顶部扩展进入,这就是前面说的「渐显+扩展进入」吗?

拿 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)) {
// ...
}

134839.gif

仔细对比修改后的动画与默认的动画,可以发现:文本组件确实变成了从顶部开始「扩展显示」了 —— 当然,对比之下,我们也更好理解了什么叫做「扩展进入」以及方向的意义:从A方向扩展进入,就是说,要最先显示A方向

既然有 ColumnScope 的下 AnimatedVisibility,自然也有 RowScope 的版本,对应的,默认情况下,它的扩展用的则是 expandHorizontally(),因为它需要的是横向的动画。

如果要更清楚地看看动画区别,可自行延长动画时间来观察

小结

今天初窥了下 Compose 的动画,并实验了一个最简单的「布局可见性」动画。这中间也忽略了很多的细节,改天咱继续讨论吧。