卷 V · 手· CH 17· 有无对照 深度 17/28

动效不是装饰,是因果

这一卷讲界面上唯一会「回话」的部分。而动效是全书最容易做成噪音的一项——因为它是唯一一个「加了就能看见效果」的维度,于是所有人都忍不住多加一点。

它从哪来、去哪了四种该做四种不该做

一个当场能感觉到的对照

下面这个 Demo 里,两种模式的内容完全一样。点一行,再切换模式点一次:

没有动效的那次,你会有大约半秒钟的空白:这个面板是从哪来的?关掉之后我会回到哪?

有动效的那次,这两个问题根本不会出现——因为那个面板是从你刚点的那一行长出来的,你的眼睛全程跟着它。

动效在回答一个具体的问题

这个新出现的东西,和我刚才那个动作是什么关系。

这就是判断「该不该加动效」的唯一标准:它有没有在解释一个因果关系

有 —— 加。
没有 —— 那它就是延迟。而延迟是纯粹的成本,用户每次都要付。

四种该做

场合它在解释什么典型时长
从哪来 / 去哪了 新界面和你刚点的那个东西的空间关系 300–500ms
点了有反应 「我收到了」——涟漪、按下的凹陷、状态层 100ms 以内
东西变了 列表里插入/删除了一项,数字从 3 变成 4 200–300ms
我在忙 加载中——它在说「没死机,还在跑」 循环
第二条最容易被低估

「点了有反应」是所有动效里最重要的一个,而且它的时长要求和别的完全不同:必须在 100ms 以内

因为人对「我的动作引起了这个结果」的感知窗口大约就是 100ms。超过之后,因果感开始断裂;超过 300ms,用户会以为自己没点到,然后再点一次。

这也是第 4 章那个「硬」的主要来源——不是动效不够花哨,是压根没有按下反馈

四种不该做

  • 入场动画——页面加载完,元素一个个淡入、依次上滑。它解释了什么因果?没有。用户已经在等加载了,你又让他多等 400ms。
  • 装饰性循环——一直在呼吸的按钮、一直在飘的图标。它们持续消耗注意力,而注意力是这个界面最稀缺的资源。
  • 过长的转场——超过 500ms 的转场,第一次觉得高级,第五次觉得慢,第五十次想卸载。你的用户会用这个 App 五百次。
  • 让人等的确认动画——点了「保存」之后一个 800ms 的对勾动画。保存已经成功了,为什么还要拦着我?
一条很硬的自查

你自己在开发时,会不会想把这个动画跳过?

你每天要看这个界面几十次。如果你已经开始觉得某个动画烦了——用户在第十次的时候就会有同样的感觉,只是他们不会告诉你,他们会直接觉得这 App「有点慢」。

顺带:「感觉慢」和「实际慢」是两件事,而动效能同时影响两者。一个 300ms 的转场会让操作真的慢 300ms;但一个恰当的加载动画能让 2 秒的等待感觉像 1 秒。区别在于前者是在你等的时候挡着你,后者是在你本来就得等的时候陪着你

尊重「减弱动态效果」

一件很多人不知道的事:有些人会因为界面动效而头晕、恶心。前庭功能障碍者在系统里打开了「减弱动态效果」,你必须听。

// 读系统设置。这不是可选项 —— 这是无障碍要求。
@Composable
fun rememberReduceMotion(): Boolean {
    val ctx = LocalContext.current
    return remember {
        Settings.Global.getFloat(
            ctx.contentResolver,
            Settings.Global.ANIMATOR_DURATION_SCALE, 1f
        ) == 0f
    }
}

@Composable
fun SheetTransition(content: @Composable () -> Unit) {
    val reduce = rememberReduceMotion()
    AnimatedVisibility(
        visible = true,
        // 减弱模式下:保留淡入(不会引起眩晕),去掉位移和缩放
        enter = if (reduce) fadeIn(tween(100))
                else slideInVertically { it / 2 } + fadeIn(tween(300))
    ) { content() }
}

注意做法不是「关掉所有动画」——那会丢掉因果信息。而是把「位移和缩放」换成「淡入淡出」:因果关系还在,眩晕的来源(大面积运动)没了。

动效的层次

最后给一个很实用的排序,用来决定「有限的精力先花在哪」:

① 按下反馈必做。没有它界面就是坏的
② 加载 / 空 / 错的状态转换必做。不然会闪一下白
③ 列表增删该做。Compose 一行 animateItem()
④ 页面转场该做,但用系统默认的就行
⑤ 共享元素转场锦上添花。做之前先把 ①–④ 做完
⑥ 其他一切默认不做

大多数人的精力分配正好是倒过来的:花两天做一个共享元素转场,而按钮至今没有 pressed 态。

本章关掉的自由度本章 2 条 · 累计 48 / 78
  • 「这儿要不要加个动画」——问它有没有在解释因果。没有就不加。入场动画、装饰性循环一律不做。
  • 「动效要不要考虑无障碍」——要,而且是必须。读系统的动画缩放设置,减弱模式下把位移换成淡入淡出。