打造 Material 颜色主题 | 实现篇
Material 主题 https://material.io/design/material-theming/overview.html#material-theming Material 组件 https://material.io/components Material 颜色 https://material.io/design/color/ Material 字体 https://material.io/design/typography/ Material 形状 https://material.io/design/shape/
在 Android 上可使用 1.1.0 版本及以上的 Material 组件 (Material Design Components, MDC) 库来实现 Material 主题。如果您希望从 Design Support Library 或 MDC 1.0.0 迁移到新版本的 MDC,请参阅我们的迁移指南。
Material 组件 https://github.com/material-components/material-components-android 迁移指南 https://medium.com/androiddevelopers/migrating-to-material-components-for-android-ec6757795351
颜色属性
Material Design 提供可供填充的 12 个颜色 "槽 (slots)",这些色值构成应用的整体调色板。每个 "槽" 都有一个设计术语 (如 Primary),该术语则对应一个可在应用主题中覆盖的颜色属性 (如 Primary 这个术语对应 colorPrimary 这个颜色属性)。这些是您的浅色和深色主题默认的基准色值。
△ 浅色主题的基准 MDC 颜色属性
△ 深色主题的基准 MDC 颜色属性
Material 组件使用这些颜色属性为各个 widget 着色。
app:backgroundTint=”?attr/colorSecondary”
您可能会认得下表中的一些颜色属性,如 colorPrimary。这些因为一些颜色属性继承自 AppCompat 和平台本身,而其余属性则来由 MDC。每个颜色属性的完整来源见下表。
挑选颜色
colorPrimary 和 colorSecondary 是用于呈现品牌的颜色 colorPrimaryVariant 和 colorSecondaryVariant 是品牌颜色较浅或较深的阴影色 colorSurface 用于表单或表面 (如卡片颜色和应用的底部弹出菜单颜色) android:colorBackground 是应用的窗口背景颜色 顾名思义,colorError 用于错误和警告 各种各样的 "On" 颜色 (colorOnPrimary、colorOnSecondary、colorOnSurface 等) 用于为显示在其他颜色上方的前景内容 (如文本和图标) 进行着色。这些颜色需要满足无障碍功能要求并与所在表面的颜色有足够的对比度。
颜色工具
Material 颜色工具: 获得主色和辅色的浅色/深色变体以及合适的 "On" 颜色。并能在示例界面中预览这些颜色的效果。 https://material.io/resources/color/ Material 调色板生成器: 生成完整的色调调色板 (即包含 Material 色值编号 50 - 900)。另外您还能获得互补色、近似色和三等分配色的建议。 https://material.io/design/color/the-color-system.html#tools-for-picking-colors
△ Material 颜色工具 (左) 和 Material 调色板生成器 (右)
注意事项
除非您的品牌恰巧使用和基准 Material 主题色完全相同的紫色和蓝绿色,否则请您务必覆盖 colorPrimary、colorSecondary 及其变体色。 您无需覆盖所有颜色。一些颜色 (如 colorSurface) 默认使用的就是中性颜色,因此使用默认值不会有什么问题。 如果您的品牌没有定义任何种类的辅色或强调色,那么可以将一种颜色同时用于 colorPrimary 和 colorSecondary。变体色也可以和主色相同 (即 colorPrimary 和 colorPrimaryVariant 可以相同)。 一个颜色和其变体颜色以及 "On" 颜色尽管是三个单独的颜色属性 (如 colorPrimary、colorPrimaryVariant 和 colorOnPrimary),但它们之间依然存在紧密的联系。因此如果您覆盖了其中一个,请检查其他颜色属性,以查看是否可行且满足无障碍功能要求。
其他颜色槽
除 Material 主题指定的 12 个颜色槽以外,您的设计系统可能还会用到其他颜色槽。幸运的是,在 Android 上声明颜色属性的操作很简单:
<!-- In res/values/attrs.xml -->
<attr name="colorCustom" format="color" />
<!-- In res/values/themes.xml -->
<style name="Theme.App" parent="Theme.MaterialComponents.*">
...
<item name="colorCustom">@color/...</item>
</style>
颜色资源
将浅色和深色主题用到的全部 <color> 存储在一个 res/values/colors.xml 文件中 <color> 色值使用字面名称命名 (而不是基于使用方式命名):
如此一来,便可以在使用颜色时更自然地用到 ?attr/ 语法。支持深色主题时也推荐采用此种方法 使用 green_500 或 brand_name_yellow 等名称 避免使用语义名称,如 color_primary
<!-- In res/values/colors.xml -->
<color name="navy_500">#64869B</color>
<color name="navy_700">#37596D</color>
<color name="navy_900">#073042</color>
<color name="green_300">#3DDC84</color>
<color name="green_500">#00A956</color>
覆盖应用主题中的颜色
让我们了解一下如何通过覆盖相关属性将您自己的调色板应用到主题中。
首先,您的主题需要在妥善处理浅色和深色调色板的同时减少与基础主题的重复。有关这方面的更多信息,请参阅 Chris Banes 关于深色主题的文章以及 Chris Banes 和 Nick Butcher 的 "如何正确开发外观样式" 演讲。
设置好主题后,覆盖您希望在浅色和深色主题中更改的颜色属性即可:
<!-- In res/values/themes.xml -->
<style name="Theme.App" parent="Theme.App.Base">
...
<item name="colorPrimary">@color/navy_700</item>
<item name="colorPrimaryVariant">@color/navy_900</item>
<item name="colorSecondary">@color/green_300</item>
<item name="colorSecondaryVariant">@color/green_900</item>
<!-- Using default values for colorOnPrimary, colorSurface, colorError, etc. -->
</style>
<!-- In res/values-night/themes.xml -->
<style name="Theme.App" parent="Theme.App.Base">
...
<item name="colorPrimary">@color/navy_500</item>
<item name="colorPrimaryVariant">@color/navy_900</item>
<item name="colorSecondary">@color/green_300</item>
<item name="colorSecondaryVariant">@color/green_300</item>
<!-- Using default values for colorOnPrimary, colorSurface, colorError, etc. -->
</style>
Material 组件将根据主题全局应用您覆盖好的颜色:
颜色复用性和最佳实践
在许多情况下,都要在布局、可绘制对象、样式和其他位置使用颜色。我们将介绍一些可尽量复用代码的方法,而且不会影响您在应用主题中指定的色值。
<Button
...
- app:backgroundTint="@color/green_300"
+ app:backgroundTint="?attr/colorPrimary"
- android:textColor="@color/black"
+ android:textColor="?attr/colorOnPrimary"
/>
请参阅 Nick Butcher 的《Android 样式系统 | 主题背景属性》,以了解更多说明和此规则的一些例外情况。
有时,您可能希望使用 MDC 主题中的一种颜色,但带上 alpha 值 (例如 60% 的 colorPrimary)。例如,触发点击时的波纹动画和项目被选中的状态。
Android <color> 资源支持 alpha 通道:
<!-- 60% alpha = 99 -->
<color name=”navy_700_alpha_60”>#9937596D</color>
<!-- In res/color/primary_60.xml -->
<selector ...>
<item android:alpha="0.6" android:color="?attr/colorPrimary" />
</selector>
每种状态的颜色和主题叠加
更为常见的情况是根据视图状态使用 ColorStateList 切换颜色 (和 alpha 值)。MDC widget 大量将其用于禁用状态、悬停状态和按压状态等。比如下面是一个 MDC 按钮的背景色源代码:
<!-- In button/res/color/mtrl_btn_bg_color_selector.xml -->
<selector ...>
<item android:color="?attr/colorPrimary" android:state_enabled="true" />
<item android:alpha="0.12" android:color="?attr/colorOnSurface" />
</selector>
完整源代码 https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/button/res/color/mtrl_btn_bg_color_selector.xml
和上面这个按钮的示例类似,假设您希望将主背景色从主色改为辅色:
您当然可以将以上源文件复制一份,然后将 colorPrimary 改为 colorSecondary,但如果源代码恰巧发生更改,那么此操作会很繁琐且会出现问题。
一种更好的方式是使用主题叠加。Nick Butcher 在他的《Android 样式系统 | 主题背景覆盖》一文中对此有详细的介绍。基本上,我们可以替换 View 或 ViewGroup 的主题属性值 (在我们例子中为 colorPrimary) 以及所有依赖它的项目 (在我们的例子中为按钮)。
下面是一个主题叠加的简单例子。请注意 parent 为空,其作用是确保我们仅覆盖希望更改的属性:
<!-- In res/values/themes.xml -->
<style name="ThemeOverlay.App.PrimarySecondary" parent="">
<item name="colorPrimary">?attr/colorSecondary</item>
<item name="colorOnPrimary">?attr/colorOnSecondary</item>
</style>
android:theme - 用于所有 widget,不会以默认样式工作 app:materialThemeOverlay - 仅用于 MDC widget,以默认样式工作
<Button
...
<!-- Alternatively apply with android:theme -->
+ app:materialThemeOverlay="@style/ThemeOverlay.App.PrimarySecondary"
/>
API 兼容性
平台是在 API 23 才开始在 CSL 等处增加对 ?attr/ 语法的支持。如果您的 minSdk 是更早的版本,也不要担心: 有兼容性类!事实上,MDC 和 AppCompat widget 都有在底层使用这些兼容性类,因此在使用时无需其他操作。
对于需要以编程方式使用 CSL 的场景,请使用 AppCompatResources:
val primary60 = AppCompatResources#getColorStateList(
context, R.color.primary60
)
AppCompatResources https://developer.android.google.cn/reference/androidx/appcompat/content/res/AppCompatResources
MDC widget 中的颜色
之前我们曾说过,MDC widget 会响应主题级别的颜色属性覆盖。但是,您是如何知道按钮会将 colorPrimary 用作其背景着色并将 colorOnPrimary 用于其图标和文本呢?让我们来看一下几个选项。
复制这个项目并在 Android Studio 中运行应用 调整 res/values/color.xml 以及 res/values/themes.xml 和 res/values-night/themes.xml 中的色值 重新运行应用,观察视觉变化
构建 Material 主题 https://material.io/resources/build-a-material-theme 项目源代码 https://github.com/material-components/material-components-android-examples/tree/develop/MaterialThemeBuilder
△ 构建 Material 主题中的色值变化
按钮文档 https://github.com/material-components/material-components-android/blob/master/docs/components/Button.md
源代码
查看 MDC 源代码是了解其工作机制最可靠的方法。MDC 使用默认样式实现 Material 主题,因此看一看这些样式以及所有可设置样式的属性和 java 文件是个不错的办法。比如您可以去看看 MaterialButton 的样式、属性和 java 源文件。
MaterialButton 样式 https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/button/res/values/styles.xml MaterialButton 属性 https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/button/res/values/attrs.xml MaterialButton 源文件 https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/button/MaterialButton.java
△ MDC 按钮的默认样式和色值
自定义视图中的颜色
您的应用可能包含您自己构建或从现有库中获得的自定义 widget。在和 MDC 一起使用时,使这些视图对 Material 主题作出响应非常必要。让我们看一看在将颜色主题用于自定义 widget 时需要牢记的事项。
为自定义视图设置样式需要使用 <declare-styleable>。在保持一致性方面,复用 MDC 中的属性名称非常有用。使用 <declare-styleable> 的默认样式还可以使用 MDC 主题颜色属性:
<!-- In res/values/attrs.xml -->
<declare-styleable name="AppCustomView">
<attr name="backgroundTint" />
<attr name="titleTextColor" />
...
</declare-styleable>
<!-- In res/values/styles.xml -->
<style name="Widget.App.CustomView" parent="android:Widget">
<item name="backgroundTint">?attr/colorSurface</item>
<item name="titleTextColor">
@color/material_on_surface_emphasis_high_type
</item>
...
</style>
可以通过便利的新 MDC 类 (MaterialColors) 以编程方式处理主题的颜色属性,这对于自定义视图也非常有用:
// Resolve color from theme attr
val primaryColor = MaterialColors.getColor(
view, R.attr.colorPrimary
)
// Layer background color with overlay color + alpha
val overlayedColor = MaterialColors.layer(
view, R.attr.colorSurface, R.attr.colorPrimary, 0.38f
)
下一步
现在,我们已经在 Android 应用中使用 MDC 实现了颜色主题。有关 Material 主题的其他课题,请阅读我们相关的介绍文章。
为什么推荐使用 MDC https://medium.com/androiddevelopers/we-recommend-material-design-components-81e6d165c2dd 字体主题 https://material.io/blog/android-material-theme-type 形状主题 https://material.io/blog/android-material-theme-shape 深色主题 https://medium.com/androiddevelopers/dark-theme-with-mdc-4c6fc357d956 动效系统 https://material.io/blog/android-material-motion
我们一如既往地期待您在 GitHub 上提交错误报告和功能需求。另外,请务必查看 Android 组件示例应用。
提交错误报告 https://github.com/material-components/material-components-android/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BComponent+name%5D+Short+description+of+issue 提交功能需求 https://github.com/material-components/material-components-android/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=%5BComponent+name%5D+Short+description+of+request Android 组件示例应用 https://github.com/material-components/material-components-android-examples
如果您已成功实现颜色主题或您在实现期间遇到问题,欢迎在下方评论区和我们分享。
推荐阅读