chapter[11] · part 2 / 程序员的界面美学

Android 设计:Material Design

我们没有发明一种风格,我们发明了一种材料——它像纸和墨一样有物理规则,但足够智能,不受物理世界的限制。 Google Material Design 创始文档对"量子纸"隐喻的阐述(意译)

读完 HIG 再读 Material Design,最有趣的是气质差异:Apple 给你一套礼仪,Google 给你一套物理学。HIG 说"应当谦逊、应当清晰",像一位品味极好的主人在立家规;Material 则从一个第一性隐喻出发,把所有规则当定理推导出来。对程序员来说,Material 是更"对胃口"的读物——它是公理化的。本章看两样东西:这个公理系统怎么运转,以及它和 iOS 的方言差异如何处理。

11.1公理:一种会魔法的纸

Material 的全部规则源自一个隐喻:界面由一种虚构的材料构成——量子纸(quantum paper)。它像真纸一样:有厚度(1dp)、有海拔、会投影、不可相互穿透;又比真纸多几条魔法:可以瞬间伸缩、分裂、合并、响应触摸泛起涟漪。一旦接受这个公理,大量设计决定就不再需要死记:

同一个屏幕的侧视图:海拔是真实的 z 坐标 0dp 背景 卡片 · 1dp 顶栏 · 4dp FAB · 6dp 对话框 · 24dp 海拔越高 → 投影越大越软 → 越接近用户 → 越重要(5.2 的物理引擎化)
图 11-1 · Material 海拔模型侧视图 · 本书绘制(示意,非 Google 官方素材) 把第五章的光影做成了物理引擎 为什么对话框的影子必须比卡片大?不用背——对话框海拔更高,离"纸面"更远,影子自然更大更软。为什么两张卡片不能交叉重叠?纸不可穿透。为什么点按钮要泛涟漪?墨水落在纸上会晕开。隐喻是设计系统的公理:一致性不再靠规则清单维持,靠世界观维持。这正是 8.2 节"语义层"的极致形态——连语义都从一个统一隐喻里派生。

11.2动态取色:第三章 + 第八章的全自动化

Material 3(Material You)做了一件把本书前文全部串起来的事:动态取色(dynamic color)。用户换一张壁纸,整个系统和所有适配的 App 随之换装。它的管线值得程序员细看,因为每一步都是你学过的章节:

① 壁纸 提取主导色 ② 源色 进入 HCT 色彩空间 ③ 色调阶 0–100 同一色相的明度标尺(3.1) primary ← tone 40 on-primary ← tone 100 surface ← tone 98 on-surface ← tone 10 ④ 语义槽位 保证对比度的配对(8.2) 用户提供审美输入(壁纸),系统执行配色纪律(色调与对比度)——品味被工程化接管
图 11-2 · Material You 动态取色管线 · 本书绘制 一条把配色变成纯函数的流水线 关键在第③步:HCT 空间的"色调(tone)"轴就是感知均匀的明度——系统不直接用壁纸色,而是先把它展开成 0–100 的明度标尺,再按固定规则取格子(on-primaryprimary 永远隔开足够的 tone 距离)。这就是为什么任何壁纸进去,出来的界面对比度都达标:美学判断(选色)交给用户,工程纪律(可读性)锁死在系统里。每个 on-XX 的命名模式,是 8.2 语义层最工整的工业实现。

M3 还顺手解决了 9.4 节的暗色模式海拔问题:投影在深底上失效,于是海拔改用色调表面(tonal surface)表达——海拔越高,表面叠加越多主色调的"色调提升"。光的语法从"影子"换成了"染色",但层级语义一字未变。

11.3方言速查:同一句话的两种说法

双平台开发者最常踩的坑不是大原则(两家高度一致),而是方言细节。一张对照表:

维度iOS(HIG)Android(Material)
返回模型局部:左上角 ‹ + 屏幕左缘右滑全局:系统级返回手势/按钮,App 必须响应
系统字体San Francisco(光学尺寸)Roboto / Google Sans
主导航底部 Tab Bar(3–5 个)底部 Navigation Bar(M3 同样收敛到底部)
首要动作导航栏右上角文字按钮FAB——悬浮在内容上的实体按钮
触摸反馈高亮/透明度变化涟漪(ripple)从触点扩散
模态风格底部卡片上滑,压暗退后对话框居中,或 bottom sheet
尺寸单位ptdp(与 pt 概念等价)· 触达最小 48dp
气质克制、内容优先、隐形表达、色彩大胆、组件在场

其中 FAB 值得单独一句:它是第七章"克制"原则的实体化——整屏唯一一个高海拔、高饱和的圆钮,等于把"本屏最重要的动作"用海拔和颜色同时声明出来。也因此 FAB 的滥用方式只有一种:一屏放两个。

11.4跨平台策略:一套品牌,两种礼仪

该做两套 UI 还是一套?把选项按成本排开:

A

全原生双轨

两个团队、两套设计语言,各自纯正。体验上限最高,成本两倍。大厂核心 App 的选择。

B

一套设计 + 平台礼仪适配(推荐默认)

品牌层(色彩、字阶、插画、语气)两端统一;结构层(返回模型、导航位置、模态风格、反馈动效)入乡随俗——正是上表那几行。Flutter/React Native/Compose Multiplatform 时代的务实解。

C

完全自定义

两端长得一模一样、且都不像系统。等于自建平台:10.6 的打破成本公式 × 2。除非品牌强到用户为它学习(大型游戏、超级 App),否则慎选。

程序员视角

策略 B 翻译成架构语言:品牌是抽象接口,平台礼仪是两个实现。把 design token(第八章)按这条线切开——brand.*(色、字、圆角、插画风格)共享一份;platform.*(导航结构、返回行为、反馈动效)各自绑定系统组件。在 Flutter 里这对应同一套主题 token 喂给 Material 与 Cupertino 两套组件;切错这条线(比如把 iOS 的返回箭头硬编码进共享组件),就是把实现细节泄漏进了接口。

11.5自适应布局:当一块屏会变形

Android 生态比 iOS 更早被一个问题逼到墙角:屏幕尺寸不是几种,是连续的——手机、折叠屏(合起来是手机、展开是平板)、平板、ChromeOS、桌面。第九章 Web 那套"为规则设计而非为版面设计"的弹性思维,在原生端有了官方框架:窗口尺寸类(window size class),把连续的宽度切成三档——Compact(手机竖屏)、Medium(折叠展开/小平板)、Expanded(平板/桌面/大折叠)。关键不在档位本身,而在每一档该重排成什么样:

Compact <600dp 手机 单栏 · 底部导航 列表→点开→详情(新屏) Medium 600–840dp 折叠/小平板 导航升级为侧栏 rail 底栏的图标移到左侧 Expanded >840dp 平板/桌面/大折叠 列表 + 详情 = 双栏并置 原本两屏的跳转,压成一屏
图 11-2 · 同一个 App 在三个窗口尺寸类下的重排 · 本书绘制 大屏不是"放大的手机",是"少一次跳转" 盯住两处变化:① 导航从底部栏(Compact)升到左侧 rail(Medium/Expanded)——拇指热区在大屏上不再适用(回链 10.4),手要么扶着平板边、要么在用鼠标,导航该贴边竖排。② 列表-详情(list-detail):手机上是"点列表→跳到详情新屏"两步;到 Expanded 直接并置成双栏,选中即在右侧显示——多出来的宽度没有拿去拉伸或留白,而是拿去消灭了一次导航跳转。这就是大屏设计的第一性原理:多出来的空间应该用来同时呈现更多上下文,而不是把手机布局抻宽。一个 1000px 宽、中间杵着一根窄列按钮的"抻大的手机",是大屏头号反模式。
程序员视角

这正是第九章弹性思维在原生端的复刻,工具也已就位:Compose 的 WindowSizeClass、SwiftUI 的 NavigationSplitView——一个组件根据当前尺寸类切换布局,而不是把"手机假设"(如单栏导航、列表必跳详情)硬编码进去。折叠屏还多一个维度叫姿态(posture):合起、半开(像笔记本支起)、完全摊平,以及跨折痕的内容连续性——但底层心法不变,仍是 9.1 的 content-out:不是"为 Pixel Fold 写一个分支",而是问"内容到了这个宽度,怎样重排才最省用户的力"。按设备型号分支,是追不完的;按尺寸类与内容重排,才收敛。

核心概念

HIG 与 Material 在所有表层差异之下共享同一个深层结构——都是把第一部分的美学原理编译成平台规则:层级(7.1)、光影(5.2)、字阶(4.3)、克制(7.7)。所以双平台设计的正确心法不是记两套规则,而是认出同一条原理在两地的不同方言。会一门"普通话"(第一部分),方言只是口音。

鉴赏练习 11

找一个你同时在 iPhone 和 Android 手机上用过的 App(或借朋友手机对比):① 用 11.3 的表逐行检查,它在每个维度上是"入乡随俗"还是"一套到底"?② 判断它选了 11.4 的哪种策略,找出一处礼仪适配做得好的细节和一处穿帮(iOS 风格的返回箭头出现在 Android 上是最常见的穿帮);③ 如果是 Android 12+ 设备,换一张壁纸,看哪些 App 跟着变色——那些就是接入了动态取色管线的 App。④ 变形测试:找一个平板或把手机横过来(或用模拟器拉宽窗口),看你常用的 App 怎么应对大屏——它是优雅地重排成双栏/侧栏(11.5),还是把手机布局直接抻宽、中间留一大片尴尬的空白?后者就是"放大的手机"反模式,记一例进 swipe file。