先做灰度,颜色最后加
最后一章不给新知识,给一个顺序。而这个顺序会把前面二十七章的规则,从「你知道」变成「你做得到」。
一个反直觉的建议
《Refactoring UI》里有一条工作法,我把它放在全书最后,因为它能强制你执行前面所有的章节:
理由很硬:颜色是最容易的手段,所以它会掩盖你在别处偷的懒。
标题不够突出?染个蓝色,好像行了。
两个状态分不开?一个红一个绿,好像行了。
按钮不够明显?填个饱和色,好像行了。
三个「好像行了」,实际上一个问题都没解决——你只是把层级问题换成了颜色噪音。而这正是第 4 章那个「花」的生产过程。
对比一下
按这个顺序玩一遍:
- 灰度 + 未修——问题全暴露:标题和副标题几乎一样重,两个状态标签分不开,主次按钮的差别只剩一个边框。
- 上色 + 未修——「好多了」。但你只是用颜色盖住了问题,转回灰度它原样还在。
- 灰度 + 修过——一个颜色都没加,层级就出来了。标题放大加粗、分组之间拉开间距,全是卷 II 和卷 III 的手段。
- 上色 + 修过——这才对。颜色是锦上添花,不是救命。
换句话说:颜色只负责表达「是什么」,不负责表达「有多重要」。
「有多重要」由大小、间距、字重、位置负责——它们在灰度下依然有效。
它顺带解决了两件事
① 色觉自查。第 16 章那个「把界面转成灰度看看还分不分得开」的检查,在这个流程里是免费的——因为你本来就是从灰度开始做的。灰度下能分开的东西,所有色觉类型的人都能分开。
② 暗色模式。如果你的层级完全建立在颜色上,做暗色模式时你要重新调一遍所有关系。如果层级建立在大小和间距上,暗色模式只需要换一套颜色角色(第 16 章)——布局一行不动。
Compose 里实施起来很轻,一个 Preview 参数就够:
// 在开发期给自己一个「抽色」开关
@Composable
fun GrayscaleMode(enabled: Boolean, content: @Composable () -> Unit) {
if (!enabled) { content(); return }
val matrix = ColorMatrix().apply { setToSaturation(0f) }
Box(
Modifier.graphicsLayer {
renderEffect = RenderEffect
.createColorFilterEffect(ColorMatrixColorFilter(matrix.values))
.asComposeRenderEffect()
}
) { content() }
}
// 每个 Preview 配一对 —— 灰度那个是你真正该看的
@Preview(name = "1 灰度") @Composable
private fun PreviewGray() = AppTheme { GrayscaleMode(true) { OrderScreen(sample) } }
@Preview(name = "2 上色") @Composable
private fun PreviewColor() = AppTheme { GrayscaleMode(false) { OrderScreen(sample) } }
更土的做法一样有效:截图,用任何工具去饱和度。不需要写一行代码。
卷 VII 收尾
补卷四章,关掉了 11 个自由度:
加上主线的 67 条,全书一共 78 个你以后不用再临场决定的问题。
最后
这本书从第一句话开始就是同一件事:
「难看」不是一种感受,是一个可以数出来的量——你这一屏上有多少个当时随手定的数。
设计师不是每次都选得比你好,是他们不每次都选。
而你有一样他们没有的东西:你能把规则写成代码。他们靠眼睛和纪律维持一致,你可以靠 lint、类型系统和 CI。纪律会随时间衰减,CI 不会。
最后再邀请你做一次那件事:按 M 键。
这一页会浮出红线,把它自己的每一个间距、字号、行高、对比度都量给你看——数字是当场从 DOM 读的,不是写死的。
如果你现在能看着那些红线,说出「这里的间距是 s-6」「这段正文的对比度是 9.44:1,过 AAA」「这两个区块之间落在 8 的格子上」——
那这本书的事就办完了。
- 「什么时候加颜色」——最后。先用灰度把层级、分组、对齐、状态全做完,颜色只负责表达语义。
- 「怎么确认层级不是靠颜色撑的」——抽掉饱和度再看一遍。这一步顺带做完了色觉自查和暗色模式的准备。