
2021年Google I/O大会上Material3下面统一叫M3跟着Android 12一起登场到现在已经三年多了。我是在M2时代做了两三个大型App之后才转到M3的这中间把官方设计指南翻了好几遍也在真实项目里验证过不少方案。这篇笔记就从色彩、形状、字体、状态层、主题定制这几个核心模块展开把我整理出来的设计规范理解、落地方式以及实际迁移中踩过的坑一并记录下来。适合正在做组件库升级、新项目技术选型或者被设计师追着问“M3到底怎么用”的Android开发同学。先说一个感受M3不是M2的换皮它把整个设计系统的底层逻辑改了。如果你还抱着M2的思路去套M3的颜色变量和圆角值很快就会发现怎么配都不对劲。这篇笔记不会机械地罗列官方文档里的参数表而是会解释每个规范背后的原因以及你在真实项目里应该怎么取舍。1. M3的设计底层逻辑从“纸片隐喻”到“个性化表达”1.1 为什么M2那一套在现在行不通了M2时代的核心隐喻是“纸片”。整个界面系统被设计成一张张带有厚度的纸Z轴上的阴影、抬升关系、涟漪效果都是围绕纸质物理感展开的。这个思路在2014年刚推出的时候确实很超前官方甚至给所有组件都定义了静止高度和抬升高度。但随着产品形态越来越多样主题定制需求越来越多纸片隐喻开始显得僵硬。纸质隐喻绑定了一套固定的层级关系比如FAB默认抬升6dp、顶部应用栏保持4dp。这些值在通用场景里挺好用可一旦产品需要更激进的视觉表达比如音乐App想要全沉浸式背景或者学习类App想要极简低干扰的卡片这套固定规则就变成束缚了。M3把底层逻辑从“物理纸片”转向了“现实材质和个性化”。官方的说法叫expressive强调设计系统应该能表达产品自身的品牌气质而不是让所有App看起来都是同一个模子。同时M3引入了一个非常关键的基础设施——设计令牌Design Tokens。你可以把设计令牌理解成一组有名字的设计决策变量比如color-primary、shape-corner-large、type-scale-title-large。代码里不直接写具体的十六进制色值或圆角px值而是引用这些令牌换一套令牌值就等于换了一套主题。这套思路和前端界流行的Design Tokens标准是对齐的。组件库和业务代码解耦设计师改一套令牌就能全局生效不用再一个个找控件去改。实际做项目的时候这个特性的价值比想象的更大换肤、不同品牌子App复用组件库、无障碍模式适配都是靠这一层抽象才能优雅落地。1.2 M3解决了什么实际问题从工程角度看M3解决的最大痛点是主题定制成本。M2时期做个深色模式要覆盖一堆属性稍不留神就会漏掉某个控件的底色导致深浅色切换时出现白底白字的尴尬。M3把颜色收敛到固定的色彩角色上深色模式下系统会自动根据明暗背景计算角色颜色你只需要搭好框架颜色切换是自动发生的。从产品角度看M3提供了更强的品牌表达能力。你可以基于品牌色生成完整的色调系让按钮、图标、渐变、图表保持同一套色彩基因而不是靠设计师手动挑几个顺眼的色值丢给你。后面讲动态色彩的部分这个能力会体现得特别明显。2. 色彩体系读懂HCT色彩空间与颜色角色的分配逻辑2.1 HCT色彩空间到底解决了什么问题M3最核心的技术变化就是引入了新的色彩空间HCT这是M2的HSL/ARGB方案替代品。H代表色相HueC代表色度ChromaT代表色调Tone。前两个和HSL类似关键是这个Tone。HSL里的L代表Lightness亮度是为“显示在屏幕上”设计的但它不是为“人眼感知均匀变化”设计的。举个实际对比在HSL里从L80调到L40你肉眼看会觉得颜色变暗了不少但再往下从L40调到L20视觉变化幅度就不一样了。这就导致设计师控制颜色明暗变化时只能凭感觉微调。HCT里的Tone是基于人眼感知的亮度模型计算出来的同样的Tone差值人眼感知到的明暗变化是一致的。M3在生成一套色调板时就是固定色相和色度然后在Tone轴均匀采样生成11个色阶0到100每10一个台阶。这就是你看到M3的Primary色调板里从Primary-0到Primary-100的11个色值的来历。这个均匀感知的特性直接支撑了M3的对比度保证机制。系统可以在任何背景色上自动计算前景内容应该用哪个Tone的文本才能满足WCAG对比度要求而不需要开发者和设计师手动去试色。2.2 五大色彩角色与一套默认取值M3的颜色角色比M2丰富核心的是Primary、Secondary、Tertiary、Neutral、Error这几类。每个角色衍生出Container容器色和On色内容色。比如Primary是在强调态下频繁使用的角色对应按钮通常用它而PrimaryContainer是它同色系下的浅色容器Swift风格的次级操作按钮常用它。我整理了最常用的一组色彩角色表新项目里照着这张表配主题就行角色名典型用途说明Primary按钮、选中态、FAB主品牌色的体现On PrimaryPrimary上的内容颜色通常是白色或深色保证对比度Primary Container浅色容器底可容纳较大面积色块On Primary Container该容器上的内容颜色和容器色保持高对比度Secondary次要操作、图标强调M3鼓励用色轮上相邻颜色区分主次Tertiary表达品牌特质、数据可视化用色轮上相距较远的颜色制造焦点Surface页面背景、卡片底中性色Surface Variant副背景、分割区域比Surface稍深Background页面最底层背景通常和Surface同色系Error错误提示语义色不建议轻易改这些角色的设计意图很简单一套色板要能覆盖页面里所有常见的视觉层级同时保证内容色和容器色的对比度达标。M3还提供了一组现成的Baseline配色值如果你没有品牌强色可以直接用默认的紫罗兰色方案快速跑起来。2.3 动态色彩Android 12的秘密武器与适配限制动态色彩Dynamic Color应该是M3最吸引眼球的功能。简单说系统会从用户的壁纸里提取颜色生成一套个性化的主题色桌面、设置、通知栏、第三方应用全部跟着变。实现原理是系统壁纸提取源色在HCT色彩空间里量化出接近壁纸的种子色再基于这个种子色通过算法生成一组符合Material规范的色板最终映射到Primary、Secondary、Tertiary等角色上。在代码里你只需要一行代码绑定系统生成的动态主题注意这只能在Android 12及以上使用// 仅 Android 12 支持动态取色 dynamicLightColorScheme(context) dynamicDarkColorScheme(context)但这里有个容易踩的坑Android 12以下的设备没有动态取色能力你需要提供fallback方案。另外动态色不是让开发者和设计师真的用“壁纸的颜色”做视觉设计它只是让系统根据壁纸生成色调板你的页面结构、留白、形状仍然是由你自己控制的。动效设计和品牌信息想让用户识别出来靠的还是排版和布局而不是靠某个颜色值。我在实际项目里还发现一个问题动态色彩方案下品牌色的占比会明显降低。如果你的产品强依赖品牌识别度比如金融类App的绿色、运动类App的橙色全量启用动态色彩可能会稀释品牌感。折中的方案是允许用户主动开关“动态主题”同时提供静态品牌主题作为默认选项这样既照顾了系统级一致性又保住了品牌的视觉权重。3. 形状与状态层M3在交互反馈上的精细打磨3.1 Shape系统用圆角表达品牌气质M3把形状从M2的“四个角都一样”升级成了可独立控制每个角的系统。它定义了一套完整的形状令牌包括shape-corner-none、shape-corner-extra-small、shape-corner-small、shape-corner-medium、shape-corner-large、shape-corner-extra-large、shape-corner-full。默认基础单位是4dpSmall系列是8dpMedium是12dpLarge是16dpExtraLarge是28dpFull则是完全圆角比如圆形FAB。每个组件在官方规范里都对应一组推荐形状比如Card推荐Medium12dpDialog推荐ExtraLarge28dpFAB推荐Full。这么做是为了保持视觉层级的一致性越大的浮动元素圆角应该越明显这样才符合“大组件更柔和”的视觉直觉。实际做品牌定制时设计师可以单独调整某个形状令牌让整套组件的圆角气质统一变化。比如做儿童教育类App想要更可爱亲和的风格就把shape-corner-large从16dp提到20dp所有引用这个令牌的组件会同步变化。这个特性和Design Tokens体系是配套的你在代码里不要直接给控件写死圆角dp值而是引用形状令牌后续调整才能一键生效。Compose里覆写形状令牌是这样的val MyShapes Shapes( extraSmall RoundedCornerShape(4.dp), small RoundedCornerShape(8.dp), medium RoundedCornerShape(12.dp), large RoundedCornerShape(16.dp), extraLarge RoundedCornerShape(28.dp) )3.2 State Layers点击反馈不是简单的改色M2里的点击反馈主要依赖ripple效果和控件自带的背景色切换。M3引入了State Layers的概念把hover悬停、focused聚焦、pressed按压、dragged拖拽这些状态统一抽象成“覆盖层”。思路是这样的每个状态的视觉反馈是由一个半透明黑色或白色覆盖层叠加在Surface上生成的透明度不同状态不同。比如默认状态透明度为0%hover是8%focused是10%pressed是10%到12%。你不需要为每个状态单独配一个背景色值只要把覆盖层透明度表维护好就行。这个设计对开发来说非常友好特别是做自定义组件的时候。你不需要再为每个状态想一套颜色只要在Surface上叠加对应的前景覆盖层就能得到各状态的视觉反馈。同时深色模式和浅色模式的反馈差异也自动处理好了深色背景用白色覆盖层浅色背景用黑色覆盖层透明度表是同一张算法自动适配。要注意的是State Layers是建立在“组件有明确的背景Surface”这个前提上的。如果是文本按钮这种本身没有背景的组件点击反馈一般通过ripple画在内容周围规则是相似的但实现路径不同。项目里经常出现按钮点击了没反馈或者反馈太弱的问题大多是覆盖层透明度没有按规范走或者自定义View时忘记了状态变化时重绘覆盖层这块细节值得排查一下。3.3 3D Elevation阴影也要分层M3沿用了M2的Z轴概念但对阴影实现做了规范化。它定义了一个Elevation层级表Rest状态静止下组件的基础高度比如顶部应用栏是0dp卡片是1dpFAB是6dp状态变化时才临时抬升比如导航抽屉展开时遮罩层和内容区会有抬升差。在较新的Android版本上系统还支持色调ElevationTint Elevation效果。简单说就是当两个同色Surface上下叠加时上层表面会因为高度产生轻微的颜色变化你会看到叠加区域有一层淡淡的阴影。这个效果通过Surface的tonalElevation参数控制默认值是0dp当你设置Surface的tonalElevation大于0时系统会自动在背景上叠加基于Primary色彩角色的色调产生柔和的分层感。这个设计的价值在于深色模式下阴影几乎不可见纯粹的黑色阴影表现力很差。色调Elevation用颜色变化替代或补足阴影能在深色模式里依然保持层次。做深色主题的时候建议优先使用tonalElevation来区分层级而不是依赖黑色阴影。我在深色模式适配中试过这个方案效果比加一圈描边干净多了。4. 字体排印与无障碍每个字号都要过对比度检查4.1 Type Scale一套完整的字体规范M3沿用并规范了M2的字体缩放体系分为Display、Headline、Title、Body、Label五大类每类又有Large、Medium、Small三档。Display最大用于大屏展示文案Label最小用于按钮、标签、辅助信息。官方给了一套默认的字号和行高对应关系Display Large是57sp/64spHeadline Medium是45sp/52spTitle Large是22sp/28spBody Medium是14sp/20spLabel Large是14sp/20sp但字重不同等等。设计上要注意的是这套Type Scale不只是字号阶梯还隐含了字重和字间距的建议。比如Display类适合用粗字重和宽松的行高Label类用更大的行高和小字重保证在小字号下依然清晰。Compose里通过Typography类来配置val MyTypography Typography( displayLarge TextStyle( fontSize 57.sp, lineHeight 64.sp, fontWeight FontWeight.Normal ), titleLarge TextStyle( fontSize 22.sp, lineHeight 28.sp, fontWeight FontWeight.Medium ), // ... 其他文本样式 )一个容易被忽略的点中文排版下同样的字号和行高在英文环境里没问题但在中文里经常偏挤。这是因为中文文字结构更方正无衬线笔画更粗。实际项目中我一般会把Body这类正文样式的lineHeight稍微调大2到4sp再让设计师确认效果。规范是起点不是终点排版最终要服务于实际的可读性。4.2 无障碍要求不是“加分项”而是“硬指标”M3对无障碍很强调尤其关注颜色对比度。它的目标是正文文本Normal Text要满足WCAG AA标准4.5:1大号文本和UI组件满足3:1。这在你定制主题时是硬约束不是设计建议。你在选Primary色时如果选了一个浅黄色当主色那PrimaryContainer和OnPrimaryContainer之间的对比度很可能过不了4.5:1这时候系统动态生成色板也不会救你因为色板是基于对比度算法生成的高对比组合。实操建议每次定制主题后把Primary、Secondary、Surface这几个关键角色的深浅组合导出一张对照表逐项检查对比度。可以用官方提供的M3色彩工具Material Theme Builder来验证出问题时它会直接提示哪个组合不达标。除了对比度还要注意触摸目标的最小尺寸。M3建议可点击元素最小不低于48x48dp间距不足的相邻点击区域至少留8dp的间隔。这部分在自定义控件时特别容易被忽略一个看起来很正常的图标按钮如果实际点击热区只有32dp在小屏设备上就很容易误触旁边的元素用户在低端安卓机上的反馈通常是“老是点错”。5. 实操从0到1搭一套M3主题5.1 不分框架先用Material Theme Builder生成基础色板不管你是用View体系还是Compose第一步建议都用Material Theme Builder这个工具。它是Google官方出的一个网页工具你上传品牌色后它能自动生成M3的完整色板并导出Android、Flutter、Web等多个平台的代码。我建议的搭建流程是在Material Theme Builder里上传品牌主色和辅助色选择浅色/深色模式顺便把动态色预览打开看看效果。导出生成的XML或Kotlin代码作为主题底稿。打开项目把生成的色彩令牌对照表放进资源文件。根据产品需要微调Secondary和Tertiary这两类角色官方建议从色轮上选择与Primary相邻或相距较远的颜色但很多品牌其实只有一个主色这时让系统自动生成即可。跑一遍对比度检查挑出不合格的组合手动修正。5.2 View体系values/themes.xml与values-night/themes.xml的双轨配置如果你还在用传统ViewXML体系M3主题的接入有两种方式。如果你的应用最低版本是Android 12可以直接继承Theme.Material3.DayNight如果需要兼容低版本可以借助Material Components for Android 1.5.0及以上版本它会提供向后兼容的M3主题样式。配置的核心是把之前M2的colorPrimary、colorPrimaryVariant等老属性替换成M3的角色属性style nameTheme.MyApp parentTheme.Material3.DayNight item namecolorPrimarycolor/m3_primary/item item namecolorOnPrimarycolor/m3_on_primary/item item namecolorPrimaryContainercolor/m3_primary_container/item item namecolorOnPrimaryContainercolor/m3_on_primary_container/item item namecolorSecondarycolor/m3_secondary/item item namecolorSecondaryContainercolor/m3_secondary_container/item item namecolorTertiarycolor/m3_tertiary/item item namecolorSurfacecolor/m3_surface/item item namecolorSurfaceVariantcolor/m3_surface_variant/item item nameandroid:colorBackgroundcolor/m3_background/item !-- 如果要支持动态取色 -- item namedynamicColorThemeOverlaystyle/ThemeOverlay.Material3.DynamicColors.DayNight/item /style注意这里的dynamicColorThemeOverlay就是前面说的动态色彩的开关。Android 12会动态取色低版本自动回退到你配的静态色不用写条件判断代码。这个机制很成熟直接用就好。5.3 Compose体系MaterialTheme三件套的完整配置Compose端配置M3主题的核心是MaterialTheme函数它接收colorScheme、typography、shapes三个参数这三个参数分别对应M3的三个设计子系统。一个完整的配置长这样private val LightColors lightColorScheme( primary Color(0xFF6750A4), onPrimary Color(0xFFFFFFFF), primaryContainer Color(0xFFEADDFF), secondary Color(0xFF625B71), surface Color(0xFFFEF7FF), background Color(0xFFFEF7FF) ) private val DarkColors darkColorScheme( primary Color(0xFFD0BCFF), onPrimary Color(0xFF381E72), primaryContainer Color(0xFF4F378B), secondary Color(0xFFCCC2DC), surface Color(0xFF141218), background Color(0xFF141218) ) Composable fun MyAppTheme( darkTheme: Boolean isSystemInDarkTheme(), dynamicColor: Boolean true, content: Composable () - Unit ) { val colorScheme when { dynamicColor Build.VERSION.SDK_INT Build.VERSION_CODES.S - { if (darkTheme) dynamicDarkColorScheme(LocalContext.current) else dynamicLightColorScheme(LocalContext.current) } darkTheme - DarkColors else - LightColors } MaterialTheme( colorScheme colorScheme, typography MyTypography, shapes MyShapes, content content ) }这段代码里的dynamicColor开关我建议做成可配置项方便在设置页给用户选择。另外动态色只在Android 12有效所以代码里必须判断SDK版本这是最容易漏的一个条件分支。5.4 自定义组件怎么接入M3令牌做组件库升级时最大的工作量其实不在官方控件而在自定义View和自定义Composable上。视图体系里的自定义View可以通过MaterialColors工具类来获取主题色从而保持与主题一致val primary MaterialColors.getColor(view, R.attr.colorPrimary) val surfaceVariant MaterialColors.getColor(view, R.attr.colorSurfaceVariant)Compose里更简单直接在Composable函数内读取MaterialTheme的colorSchemeComposable fun MyCustomBadge(modifier: Modifier Modifier) { val colors MaterialTheme.colorScheme Surface( color colors.tertiaryContainer, contentColor colors.onTertiaryContainer, shape MaterialTheme.shapes.small ) { Text(Beta, style MaterialTheme.typography.labelLarge) } }一个常见问题是开发者在自定义组件里写死了颜色比如Color(0xFFEADDFF)一旦主题切换或者动态色启用这个组件不会跟着变。记住一条铁律代码里不要出现魔法色值所有颜色都要从主题令牌取。6. 从M2迁移到M3的常见问题与避坑指南6.1 常见问题速查表我把团队实际迁移中遇到的高频问题整理成了一张表方便按图索骥问题现象原因分析解决方案控件颜色突然变灰/变淡旧属性名未被M3识别比如colorPrimaryVariant已废弃用colorPrimaryContainer替代检查是否迁移完整深色模式下文本看不清对比度不足或者自定义文本颜色没有适配深色用onPrimary/onSurface等On系列角色替代写死颜色FAB图标颜色不对FAB的contentColor没有绑定绑定contentColorcolorScheme.onPrimaryContainer点击反馈不明显State Layers透明度设置错误或未实现核对默认8%-12%的覆盖层透明度检查focused/pressed状态动态色不生效未加dynamicColorThemeOverlay或SDK版本判断错误Android 12添加dynamicColorThemeOverlay低版本自动回退阴影在深色模式完全看不见纯黑阴影在深色底上无表现力改用tonalElevation给Surface设置色调Elevation字体在中文下显得拥挤中文和英文的lineHeight需求不同在正文样式中把lineHeight调大2-4sp6.2 迁移实战中的三个容易翻车的点第一个翻车点是阴影的metrics变化。M2时期很多人习惯给按钮写死android:elevation到了M3组件库里按钮的抬升行为默认由组件自己管理直接设置elevation可能导致和状态层叠加出现异常。建议优先用组件自带的elevation配置或tonalElevation不要再手动给Material组件设置绝对elevation。第二个翻车点是深浅色模式切换。M3的深色色板不是简单地“把浅色反转”它有一套独立的角色值。比如Primary在浅色里的Container是浅紫色在深色里是深紫色OnPrimaryContainer也从深色字变成浅色字。如果迁移时只配了浅色主题忘了在values-night目录下配深色主题Compose端还好说因为darkColorScheme是显式配置的但XML体系下就会出现一半组件浅色一半组件深色的分裂状态。第三个翻车点是滚动边缘效果。M2时代部分App用到了自定义的滚动变色效果或系统默认的overScrollGlow。M3的主题下滚动边缘的颜色跟随Surface角色的变化如果你之前硬编码了滚动edge effect的颜色迁移后会很突兀。解决方式是让滚动边缘颜色透明或者跟随主题动态计算。6.3 一个小抄设计评审时可以快速走查的清单每次M3相关需求上线前我都会拿这张检查清单过一遍比写测试用例还快Primary和On Primary的对比度是否超过4.5:1所有自定义组件是否都从主题令牌取色而没有魔法色值深色模式下FAB和卡片是否还有层级区分检查tonalElevation可点击区域的尺寸是否都大于48x48dp动态色开关是否放进了设置页而不是写死开启或关闭中文字体排版是否调整了行高是否有文字被截断这张清单帮我挡住了好几次上线前的低级视觉Bug。建议团队里至少保存一份每次主题调整后跑一遍。回到开头那个感受M3真正升级的是“设计系统”这个概念本身。颜色不再是几个色值形状不再是几个圆角字体不再是几个字号它们都变成了有名字的令牌可以在全局被引用、被覆盖、被动态改变。理解了这层抽象逻辑再看官方文档里的那一堆参数表格你就知道哪些该背哪些只要知道存在就够了。我个人在实际开发里的体会是先搭好令牌层再谈视觉表达。这套方法论不只是Android和Compose适用它已经跑到了Flutter、Web、桌面端成为一种跨端的设计共识。后续如果你在Flutter里见到ColorScheme.fromSeed或者在Web前端看到design-tokens的风格变量你都会发现它们背后是同一套M3式的思考方式。