动画基础:让界面“活”起来

说实话,动画在 Android 开发里一直是个让人又爱又恨的东西。爱的是它能让界面变得丝滑流畅,恨的是以前用 View 系统写动画,代码量真不小。不过到了 Jetpack Compose 时代,动画的门槛被大大降低了。

我个人习惯把动画分成两类:一类是“值变了,我跟着变”,比如按钮按下时透明度从 1 变成 0.5;另一类是“组件出现了或消失了”,比如弹窗的淡入淡出。今天要讲的这几个 API,正好覆盖了这两种场景。

核心知识点一览:

  • animateFloatAsState —— 数值型属性的平滑过渡
  • animateColorAsState —— 颜色变化的丝滑切换
  • AnimatedVisibility —— 组件的出现/消失动画
  • AnimatedContent —— 内容切换时的过渡效果
  • Crossfade —— 两个布局之间的交叉淡入淡出
Compose 动画基础体系 动画基础 API 值变化动画 animateFloatAsState animateColorAsState 组件过渡动画 AnimatedVisibility AnimatedContent Crossfade

animateFloatAsState:数值变化的“润滑剂”

这个 API 是我用得最频繁的。说白了,它就是把一个 float 值的变化过程“拉长”,让 UI 不是瞬间跳变,而是平滑过渡。

举个例子,你有个进度条,进度从 0 直接跳到 100,用户看着会觉得很突兀。用 animateFloatAsState 包一下,进度条就会“走”过去。

@Composable
fun ProgressBar(progress: Float) {
    val animatedProgress by animateFloatAsState(
        targetValue = progress,
        animationSpec = tween(durationMillis = 500)
    )
    // 用 animatedProgress 绘制进度条
    LinearProgressIndicator(progress = animatedProgress)
}

小技巧:animationSpec 参数可以控制动画的时长和插值器。我个人习惯用 tween() 做线性过渡,用 spring() 做弹性效果。如果你想要那种“撞到边界再弹回来”的感觉,spring 很合适。

我在项目中遇到过一个问题:当 targetValue 频繁变化时,动画会不断重新启动。嗯,这里要注意,如果你在滚动列表里用 animateFloatAsState,每个 item 的动画可能会互相干扰。解决办法是给每个 item 一个稳定的 key。

animateColorAsState:颜色也能“流动”

颜色变化如果直接赋值,会看到明显的闪烁。用 animateColorAsState 之后,颜色会从红色慢慢过渡到蓝色,中间经过紫色、靛色……用户体验瞬间提升一个档次。

@Composable
fun ThemeButton(isDark: Boolean) {
    val bgColor by animateColorAsState(
        targetValue = if (isDark) Color(0xFF1A1A2E) else Color(0xFFF5F5F5),
        animationSpec = tween(600)
    )
    Box(modifier = Modifier.fillMaxSize().background(bgColor))
}

我曾经在做一个夜间模式切换时,直接用了 Color 的硬切换,结果用户反馈“眼睛被闪了一下”。后来换成 animateColorAsState,配合缓动曲线,切换过程变得非常柔和。

避坑指南:animateColorAsState 默认使用 Color 的 ARGB 通道做插值。如果你用 Material 的 Color 对象(比如 MaterialTheme.colors.primary),要注意它内部可能包含动态主题信息,直接做动画有时会出奇怪的颜色。建议先转成标准 Color。

AnimatedVisibility:组件的“出场退场”

这个 API 解决了一个很常见的需求:某个组件什么时候显示,什么时候隐藏,中间要有个过渡效果。

你想想看,一个弹窗突然出现,用户可能会吓一跳。但如果是淡入加轻微上移,感觉就自然多了。

@Composable
fun NotificationBanner(visible: Boolean) {
    AnimatedVisibility(
        visible = visible,
        enter = slideInVertically() + fadeIn(),
        exit = slideOutVertically() + fadeOut()
    ) {
        Text("你有新的消息", modifier = Modifier.padding(16.dp))
    }
}

这里有个细节:enter 和 exit 参数支持组合多个动画效果。用 + 号连接就行,比如 slideIn + fadeIn 表示“边滑入边淡入”。

常用动画效果速查:

效果说明
fadeIn / fadeOut透明度变化
slideInVertically / slideOutVertically垂直方向滑入/滑出
slideInHorizontally / slideOutHorizontally水平方向滑入/滑出
expandIn / shrinkOut从中心展开/收缩
scaleIn / scaleOut缩放效果

我记得有一次做消息列表,新消息出现时用 AnimatedVisibility 配合 slideInVertically,用户反馈“感觉消息是从底部冒出来的,很自然”。

AnimatedContent:内容切换的“转场大师”

当你的 Composable 内容需要根据某个状态切换时,AnimatedContent 就派上用场了。它比 AnimatedVisibility 更强大——后者只管显示/隐藏,前者可以管理多个不同内容之间的过渡。

@Composable
fun StepIndicator(step: Int) {
    AnimatedContent(targetState = step) { currentStep ->
        when (currentStep) {
            1 -> Text("第一步:填写信息")
            2 -> Text("第二步:确认订单")
            3 -> Text("第三步:支付完成")
        }
    }
}

默认情况下,AnimatedContent 使用 fadeIn + fadeOut 做过渡。你可以通过 transitionSpec 参数自定义,比如让旧内容向左滑出,新内容从右侧滑入。

AnimatedContent(
    targetState = step,
    transitionSpec = {
        slideInHorizontally { width -> width } + fadeIn() togetherWith
        slideOutHorizontally { width -> -width } + fadeOut()
    }
) { ... }

个人经验:transitionSpec 里的 lambda 返回的是动画方向。{ width -> width } 表示从右侧滑入,{ width -> -width } 表示向左滑出。这个方向逻辑一开始容易搞混,我建议你写个 demo 跑一遍,看看效果就明白了。

Crossfade:两个布局之间的“温柔切换”

Crossfade 和 AnimatedContent 有点像,但更专一——它只做交叉淡入淡出。适合的场景是:两个完全不同的布局,切换时希望旧布局慢慢消失,新布局慢慢出现。

@Composable
fun LoginOrRegister(isLogin: Boolean) {
    Crossfade(targetState = isLogin) { loginMode ->
        if (loginMode) {
            LoginForm()
        } else {
            RegisterForm()
        }
    }
}

Crossfade 的好处是代码极其简洁。你不需要关心 enter 和 exit 的细节,它自动帮你做了淡入淡出。缺点是只能做淡入淡出,不能做滑动或缩放。如果你需要更丰富的效果,还是用 AnimatedContent。

我曾在项目里用 Crossfade 做“登录/注册”页面的切换。用户点击切换按钮时,两个表单会优雅地交叉淡入淡出,比直接跳转舒服多了。

总结一下这几个 API 的适用场景:

  • animateFloatAsState:单个数值属性的平滑变化,比如进度、透明度、偏移量
  • animateColorAsState:颜色属性的过渡,比如主题切换、状态颜色变化
  • AnimatedVisibility:单个组件的显示/隐藏,支持多种进入退出效果
  • AnimatedContent:多个内容之间的切换,支持自定义转场方向
  • Crossfade:两个布局之间的简单淡入淡出,代码最简洁

好了,动画基础就聊到这里。这些 API 是 Compose 动画体系的基石,掌握了它们,你已经能做出大部分常见的交互动效了。下一章我们会深入动画的“高级玩法”,比如无限循环动画、关键帧动画,还有怎么用 Animatable 做更精细的控制。


公众号:蓝海资料掘金营,微信deep3321