Material Design 3 这套设计语言从 2021 年正式对外公布到现在已经不算什么新鲜概念了但真正在 Android 项目里把它落地到“能看、能用、能维护”的程度我见过的团队并不多。大部分情况是设计稿给的是 M3 风格代码里还跑着 M2 的组件和一套自己写的颜色常量最后做出来的东西既不像 M2 也不像 M3配色割裂、组件行为不一致、深色模式一开就露馅。这篇内容就是把我自己在几个中大型 Android 项目里推行 M3 的完整过程拆开讲从设计令牌怎么映射到代码、动态取色怎么取舍、组件怎么迁移、主题怎么组织到实际踩过的坑和排查手法全部摊开说。适合已经有一定 Android 开发基础、正在或准备把项目往 M3 迁移的开发者参考也适合想搞清楚“M3 到底和 M2 差在哪、值不值得迁”的技术负责人。全文围绕 Material Design 3 实践展开不空谈设计理念只讲能直接抄作业的东西。1. 迁移前的整体设计与思路拆解1.1 为什么 M3 不能当成“换套颜色”来做很多人对 M3 的第一印象是“圆角更大了、颜色更柔和了”于是下意识把它理解成一次视觉换肤觉得改改colors.xml、调调shapeAppearance就完事。这个判断是错的而且错得很危险。M3 的本质变化在于它把颜色、字体、形状这三类视觉属性抽象成了设计令牌Design Token体系并且引入了语义化命名和动态取色两个机制。这意味着 M2 时代那套“primary、primaryDark、accent”的命名逻辑在 M3 里根本对不上号你如果硬套最后会得到一堆语义混乱、在深色模式下无法自洽的颜色引用。我举个具体的例子。M2 里colorPrimary通常被当成主品牌色按钮、标题、选中态都用它。到了 M3颜色被拆成了primary、onPrimary、primaryContainer、onPrimaryContainer四层分别对应“主色”“主色上的内容色”“主色容器”“容器上的内容色”。一个填充按钮用的是primary背景配onPrimary文字而一个 tonal 按钮用的是secondaryContainer配onSecondaryContainer。你要是还按 M2 的思路只维护一个主色那这些组件根本没法正确渲染。所以迁移的第一步不是改代码而是先把设计令牌和代码里的语义角色对齐这是整个实践的骨架。1.2 迁移策略的三种路线与取舍实际项目里推行 M3我总结下来无非三条路线各有适用场景选错了会给自己挖坑。路线做法优点代价适用场景全量替换直接升级到 Material Components 1.9 或 Compose Material3全面改用 M3 组件与主题一致性最好长期维护成本低一次性改动大回归测试范围广新项目、或组件数量可控的中型项目渐进共存保留 M2 主题局部页面用 M3 主题包裹风险可控可分批推进两套主题并存容易出现样式冲突大型存量项目、迭代节奏紧的团队仅换皮只替换颜色和形状组件仍用 M2改动最小组件行为与视觉不匹配长期是技术债不推荐仅作临时过渡我个人的建议是新项目一律走全量替换存量项目走渐进共存但要设定明确的收敛时间点。所谓“仅换皮”这条路我试过一次结果是半年后不得不推倒重来因为 M2 组件的内部实现和 M3 的视觉规范对不上比如 M2 的MaterialButton默认高度、内边距、状态层都和 M3 的 filled button 不一致你光改颜色只会得到一个“穿着 M3 衣服的 M2 按钮”越看越别扭。1.3 主题分层把颜色、字体、形状拆开管理M3 的主题体系天然是分层的我在项目里会严格按这个结构组织资源避免所有东西堆在一个themes.xml里。颜色层定义ColorScheme包含 primary/secondary/tertiary/error 四组主色及其 container、on 系列加上 surface 系列和 outline 系列。字体层定义TypographyM3 有 display/headline/title/body/label 五组每组三档large/medium/small共 15 个文本样式。形状层定义ShapesM3 有 extraSmall/small/medium/large/extraLarge 五档圆角。这三层各自独立组件只引用语义角色不直接引用具体色值。这个约束听起来简单但它是整个 M3 实践能不能长期维护的关键。我见过太多项目在colors.xml里写m3_primary #6750A4然后在布局里直接color/m3_primary一旦要做动态取色或者换主题全部失效。正确的做法是组件引用?attr/colorPrimary这类主题属性由主题在运行时决定具体值。1.4 动态取色的取舍不是所有 App 都该开动态取色Dynamic Color是 M3 最有辨识度的能力之一Android 12 及以上可以从用户壁纸提取色调生成配色方案。但我必须泼一盆冷水动态取色不是默认就该开的。它适合内容消费类、工具类、系统风格强的 App因为这类应用对品牌色的依赖弱跟随系统反而显得协调。但品牌属性强的应用比如有严格 VI 规范的金融、电商类开了动态取色会导致品牌色被壁纸色覆盖用户反而认不出你的 App。我的处理方式是做成可配置开关在设置里给用户一个“跟随系统主题色”的选项默认关闭品牌色作为兜底。代码层面通过DynamicColors.applyToActivitiesIfAvailable()控制并且要处理好 Android 12 以下的降级逻辑——低版本没有动态取色能力必须回落到静态配色方案这个回落方案要单独设计并测试不能想当然。2. 核心细节解析与实操要点2.1 颜色角色的映射关系与常见误用M3 的颜色角色数量比 M2 多得多初次接触容易懵。我把最常用的几组和它们的典型用途整理成表方便对照。颜色角色典型用途常见误用primary / onPrimary主要操作按钮、强调元素拿 primary 当大面积背景导致视觉过重primaryContainer / onPrimaryContainer次要强调、选中态背景、卡片高亮和 primary 混用层级不清secondaryContainer / onSecondaryContainertonal 按钮、筛选 chip 选中态误当成主色使用surface / onSurface页面背景、正文文字用纯白当 surface深色模式失效surfaceVariant / onSurfaceVariant次级背景、辅助文字和 surface 混用导致层次丢失outline / outlineVariant边框、分割线用 onSurface 画分割线对比过强error / onError错误提示、危险操作用红色硬编码替代 error 角色这里有个特别容易踩的坑surface 系列在 M3 里不是简单的“背景色”。M3 引入了 surface container 的五个层级lowest、low、default、high、highest用来表达界面元素的抬升关系。如果你还按 M2 的思路只用一个colorSurface那卡片、底部弹窗、对话框的层次感就出不来。我在项目里会明确约定页面根背景用surface卡片用surfaceContainerLow底部弹窗用surfaceContainerHigh对话框用surfaceContainerHighest形成稳定的抬升语义。2.2 字体系统的落地15 个样式怎么用不混乱M3 的 15 个文本样式如果无脑铺开团队里每个人都会按自己喜好选最后排版一片混乱。我的做法是在项目里再收一层把 15 个样式映射到实际业务场景形成团队约定。display 系列只在极少数的空状态大标题、引导页使用日常页面基本不用。headline 系列页面主标题用headlineSmall区块标题用headlineMedium的情况很少慎用。title 系列卡片标题、列表项主文本用titleMedium对话框标题用headlineSmall或titleLarge。body 系列正文用bodyMedium长文阅读用bodyLarge辅助说明用bodySmall。label 系列按钮文字用labelLarge标签、chip 用labelMedium极小标注用labelSmall。这个映射表要写进团队规范文档Code Review 时对照检查。我吃过亏早期没约定结果同一个列表页里列表项标题有人用titleMedium、有人用bodyLarge视觉上差一截用户看着就觉得“这 App 没做完”。2.3 形状系统的参数选择与视觉一致性M3 的五档圆角extraSmall 4dp、small 8dp、medium 12dp、large 16dp、extraLarge 28dp看起来简单但实际用起来要克制。我的经验是组件圆角必须从这五档里选不允许自定义中间值。一旦允许 10dp、14dp 这种“差不多”的值出现视觉一致性就崩了。具体到组件按钮用small或full胶囊形卡片用medium底部弹窗顶部圆角用extraLarge输入框用extraSmall或small。这里有个细节M3 的 filled button 默认是胶囊形完全圆角如果你项目里习惯方角按钮改成胶囊形会显得突兀这时候要么统一改要么在主题里覆盖 button 的 shape但覆盖后要确保所有按钮一致不能有的圆有的方。2.4 组件迁移的优先级排序存量项目迁移组件不可能一口气全换。我通常按“视觉影响大、改动成本低”的原则排序先做收益高的。按钮类MaterialButton换成 M3 的 filled/tonal/outlined/text 四种变体改动集中在样式风险低收益高。输入框TextInputLayout升级到 M3 样式注意 outlined 和 filled 两种形态的选择。卡片MaterialCardView调整圆角和 elevation配合 surface container 层级。顶部栏Toolbar迁移到MaterialToolbar或 Compose 的TopAppBar注意滚动行为和颜色。底部导航BottomNavigationView换成NavigationBar这是 M3 变化较大的组件需要重新设计交互。对话框与弹窗MaterialAlertDialog、BottomSheetDialog调整圆角和背景色。注意BottomNavigationView到NavigationBar的迁移不是简单替换类名M3 的 NavigationBar 在选中态指示器、图标与文字布局、高度上都有变化需要同步调整布局参数否则会出现图标被裁切或文字重叠。3. 实操过程与核心环节实现3.1 依赖与主题基础配置先确认依赖版本。View 体系用 Material Components 1.9.0 及以上Compose 体系用androidx.compose.material3的稳定版。这两个体系的 M3 实现是独立的不要混用主题资源。// View 体系 implementation com.google.android.material:material:1.11.0 // Compose 体系 implementation platform(androidx.compose:compose-bom:2024.02.00) implementation androidx.compose.material3:material3主题配置上View 体系继承Theme.Material3.DayNightCompose 体系用MaterialTheme包裹。我建议在themes.xml里只定义颜色和形状的覆盖字体通过fontFamily统一指定不要在主题里散落一堆textAppearance覆盖那样后期根本维护不动。style nameTheme.App parentTheme.Material3.DayNight item namecolorPrimarycolor/brand_primary/item item namecolorOnPrimarycolor/brand_on_primary/item item namecolorPrimaryContainercolor/brand_primary_container/item item namecolorOnPrimaryContainercolor/brand_on_primary_container/item !-- 其余角色同理务必成组定义 -- /style这里的关键是成组定义。primary 和 onPrimary 必须一起定义container 和 onContainer 必须一起定义缺一个就会导致某个组件在特定状态下文字看不清。我见过只定义了colorPrimary没定义colorOnPrimary的项目结果按钮文字用了默认的黑色在深色主色上几乎看不见。3.2 静态配色方案的生成与校验品牌色到 M3 配色方案的转换手工调是调不准的因为 M3 的色调系统基于 HCT 色彩空间涉及色调tone的精确映射。我的做法是用官方的 Material Theme Builder 工具输入品牌色导出完整的配色方案然后人工校验对比度。校验的重点是对比度。M3 规范要求正文文字与背景的对比度至少 4.5:1大号文字至少 3:1。工具导出的方案大部分情况下是合规的但品牌色偏亮或偏暗时可能出问题。我一般会用对比度检查工具抽查几组关键组合onPrimary对primary、onSurface对surface、onSurfaceVariant对surfaceVariant。实测下来品牌色明度在中间区间L 值 40 到 60时最稳太亮太暗都容易在某一组上翻车。深色模式的配色方案要单独生成不能简单把浅色方案反相。M3 的深色方案里primary 通常比浅色模式更亮、饱和度更低surface 是一系列带色调的深灰而不是纯黑。我见过直接把#FFFFFF换成#000000的做法结果深色模式下所有 elevation 层次全部消失卡片和背景糊成一片。3.3 动态取色的接入与降级处理动态取色的接入分两步判断可用性和应用配色。// 在 Application 或 BaseActivity 中 if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { DynamicColors.applyToActivitiesIfAvailable(this) }这段代码看起来简单但有几个细节必须处理。第一applyToActivitiesIfAvailable要在super.onCreate之前调用否则主题已经应用了静态配色动态取色不生效。第二如果用户关闭了“跟随系统主题色”开关就不能调用这个方法需要走静态配色。第三动态取色生成的配色方案里某些角色比如品牌强调色可能和你的品牌调性冲突这时候可以用DynamicColorsOptions做角色级别的覆盖。降级处理是重点。Android 12 以下没有动态取色必须保证静态配色方案完整且视觉过关。我的做法是把静态方案当成“主方案”来打磨动态取色当成“增强项”这样即使大部分用户用不上动态取色App 的视觉也不会差。反过来把动态取色当主方案、静态方案随便凑合的项目在低版本设备上会非常难看。3.4 组件样式的统一覆盖M3 组件的默认样式不一定符合项目需求需要在主题里统一覆盖。以按钮为例如果项目要求所有 filled button 高度统一为 48dp、圆角用 smallstyle nameWidget.App.Button.Filled parentWidget.Material3.Button item nameandroid:minHeight48dp/item item nameshapeAppearancestyle/ShapeAppearance.App.Small/item /style然后在布局里通过style属性引用或者在主题里用materialButtonStyle全局指定。全局指定是更推荐的做法因为它能保证所有按钮默认一致避免开发者忘记加 style。同理materialCardViewStyle、textInputStyle、bottomNavigationStyle都可以在主题里统一配置。这里有个实操心得覆盖组件样式时只覆盖必要的属性不要整个重写。我见过把Widget.Material3.Button的所有属性都抄一遍再改几个值的做法结果 Material Components 库升级后新增的属性没被继承组件行为出现诡异问题。正确的做法是继承官方样式只覆盖差异项。3.5 Compose 与 View 混合项目的主题同步现在很多项目是 Compose 和 View 混用的这时候主题同步是个大问题。Compose 的MaterialTheme和 View 的Theme.Material3是两套独立的主题系统颜色值需要手动对齐。我的做法是单一数据源在代码里定义一份颜色常量或者从资源读取同时喂给 Compose 的ColorScheme和 View 的主题属性。具体来说Compose 侧构建lightColorScheme()/darkColorScheme()时用的色值和colors.xml里的色值来自同一份定义避免两边各写一套导致不一致。val AppLightColorScheme lightColorScheme( primary BrandPrimary, onPrimary BrandOnPrimary, primaryContainer BrandPrimaryContainer, onPrimaryContainer BrandOnPrimaryContainer, // ... )View 侧则通过colors.xml引用同名色值。这个同步工作看起来繁琐但它是混合项目视觉一致的前提。我踩过的坑是Compose 页面和 View 页面并排显示时同一个按钮颜色差了一点点排查半天才发现是两边色值定义不同步。4. 常见问题与排查技巧实录4.1 深色模式下颜色错乱的排查路径深色模式是 M3 实践里问题最集中的地方。典型症状是切到深色模式后某些文字看不见、某些卡片背景和页面背景一个色、某些图标消失。排查我一般按这个顺序走。先查是否成组定义了颜色角色。如果只定义了colorSurface没定义colorOnSurface深色模式下 onSurface 会回落到默认值很可能和 surface 撞色。再查是否用了硬编码色值。布局里写android:textColor#000000的地方深色模式下必然是黑字黑底。最后查是否用了 M2 的旧属性。colorPrimaryDark在 M3 里已经废弃如果还在用深色模式行为不可预期。症状可能原因排查方法文字看不见on 系列颜色未定义或硬编码全局搜索硬编码色值检查主题属性完整性卡片与背景同色surface container 层级未区分检查卡片是否用了 surfaceContainerLow 等层级图标消失tint 用了固定色或未跟随主题检查 ImageView 的 tint 是否引用主题属性状态栏颜色不对未处理系统栏配色检查 WindowInsets 和状态栏图标明暗设置4.2 动态取色不生效的几种情况动态取色调了没反应我遇到过三种原因。第一种是调用时机不对applyToActivitiesIfAvailable在super.onCreate之后调用主题已应用自然不生效。第二种是设备或系统版本不支持Android 12 以下直接跳过这个要靠日志确认。第三种是主题本身覆盖了颜色属性动态取色生成的颜色被主题里的静态定义覆盖了。第三种最隐蔽因为代码逻辑没错但视觉上就是没变。排查第三种的方法是临时把主题里的颜色覆盖全部注释掉看动态取色是否生效。如果生效了说明是覆盖冲突需要调整覆盖策略——要么去掉覆盖要么用DynamicColorsOptions做角色级覆盖而不是主题级覆盖。4.3 组件升级后的布局错位问题从 M2 组件升级到 M3 组件布局错位几乎是必然的因为 M3 组件的默认尺寸、内边距、最小高度都变了。最典型的是按钮M2 的MaterialButton默认最小高度 48dpM3 的 filled button 也是 48dp但内边距和文字大小变了导致按钮宽度变化原本排在一行的按钮可能换行。我的处理方式是升级一个组件回归一个页面不要批量升级后统一测。批量升级看似高效但出问题时很难定位是哪个组件的改动导致的。逐个升级、逐个页面回归虽然慢但问题定位快整体返工少。另外升级前先把关键页面的截图存档升级后对比能快速发现视觉差异。4.4 主题属性引用失效的隐蔽原因有时候明明主题里定义了colorPrimary组件却拿不到显示成默认色。这种情况我遇到过两次原因都很隐蔽。一次是主题继承链断了自定义主题继承了Theme.Material3.DayNight但中间某一层又继承了别的主题导致属性被覆盖。另一次是values-night目录下的主题定义不完整只覆盖了部分属性深色模式下缺失的属性回落到默认值。排查这类问题我习惯用adb shell dumpsys activity top看当前 Activity 实际应用的主题或者临时在代码里打印theme.resolveAttribute()的结果确认属性是否真的解析到了预期值。这个方法比盲猜快得多。4.5 常见问题速查表问题排查方向解决要点按钮颜色不对检查 primary/onPrimary 是否成组定义补齐颜色角色避免硬编码深色模式文字看不清检查 on 系列和硬编码色值全部改用主题属性引用动态取色无效检查调用时机和主题覆盖调整调用顺序用角色级覆盖组件圆角不一致检查是否用了非标准圆角值统一从五档形状里选字体层级混乱检查是否按映射表使用建立团队文本样式约定混合项目颜色不一致检查 Compose 与 View 色值来源统一单一数据源5. 落地后的维护与团队协作建议5.1 建立设计令牌的单一数据源M3 实践能不能长期维护核心在于有没有单一数据源。我的做法是在项目里建一个designsystem模块或者至少一个专门的包集中管理颜色、字体、形状的定义。所有页面、组件都从这里引用不允许在业务代码里出现色值字面量。这个约束要靠工具保障不能只靠自觉。我会在 CI 里加一条静态检查扫描业务代码里是否有#开头的色值字面量有就报错。这条规则一开始会让团队不适应但坚持下来后换主题、调色值的成本会大幅降低。实测下来一个中等规模项目从“色值散落”收敛到“单一数据源”大概需要两到三轮迭代之后维护效率提升非常明显。5.2 组件封装与业务解耦M3 组件直接用业务代码里会散落大量样式配置。我的建议是对高频组件做一层薄封装比如AppButton、AppCard、AppTextField内部固定 M3 的样式选择对外只暴露业务需要的参数。这样业务代码不用关心用的是 filled 还是 tonal视觉一致性由封装层保证。封装要克制不要过度设计。我见过把 M3 组件封装成一套自研组件库的做法结果 Material Components 升级后封装层和底层组件行为不一致维护成本翻倍。薄封装的原则是只封装样式和常用组合不改变组件行为不隐藏组件能力。5.3 渐进迁移的节奏控制存量项目迁移 M3节奏控制比技术方案更重要。我的经验是按“主题先行、组件分批、页面收尾”的顺序推进。先把主题和设计令牌搭好这是地基然后按组件优先级分批迁移每批迁移后做回归最后按页面收尾确保每个页面都符合 M3 规范。每个批次要有明确的验收标准比如“本批次迁移的按钮在所有页面视觉一致、深色模式正常、动态取色开关切换无异常”。没有验收标准的迁移最后一定会留下半成品页面。我在一个项目里就因为赶进度跳过了验收结果三个月后用户反馈某个页面的按钮在深色模式下是白底白字排查发现是迁移时漏了那个页面的样式覆盖。5.4 设计稿与代码的对照机制设计和开发之间的 M3 理解偏差是实践里最常见的摩擦点。设计师给的是 Figma 里的 M3 组件开发实现的是 Material Components 的 M3 组件两者在细节上可能有差异比如圆角、内边距、状态层透明度。我的做法是建立一份对照表把设计稿里的关键组件和代码里的实现一一对应标注差异点双方确认后再开发。这份对照表不需要很正式一个共享文档就行但必须维护。我见过设计和开发各干各的最后验收时发现按钮圆角差了 2dp、卡片阴影深浅不一返工成本很高。提前对照这些问题在开发阶段就能消除。6. 我个人在实际操作中的几点体会M3 这套体系刚上手时确实比 M2 繁琐颜色角色多、组件变体多、约束多但用熟之后会发现它的约束恰恰是它的价值——语义化的颜色角色让深色模式和动态取色成为可能分层的主题结构让换肤和品牌定制变得可控。我最大的体会是不要试图绕过 M3 的约束去“简化”它那些看起来麻烦的成组定义、语义引用恰恰是后期维护省心的前提。另一个体会是关于动态取色。它很吸引眼球但不要为了用而用。先想清楚你的 App 是不是真的适合跟随系统配色想不清楚就先关着静态方案打磨好一样能做出漂亮的 M3 界面。我见过为了展示动态取色而强行开启、结果品牌识别度下降的项目得不偿失。最后分享一个小技巧迁移过程中把每个页面的浅色和深色截图都存一份按页面归档。这个截图库在回归测试、设计验收、问题排查时都能派上用场尤其是深色模式的问题光看代码很难发现对比截图一目了然。这个习惯我从第一个 M3 项目保持到现在省下的排查时间远超维护截图库的成本。