查看原文
其他

打造 Material 颜色主题 | 实现篇

Material Design 谷歌开发者 2021-08-05
作者 / Nick Rout, Material Developer Advocate

使用 Material 主题自定义 Material 组件,目的是让组件观感与品牌保持一致。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 颜色主题 | 设计篇》之后,本文为大家介绍如何实现颜色主题。


颜色属性


Material Design 提供可供填充的 12 个颜色 "槽 (slots)",这些色值构成应用的整体调色板。每个 "槽" 都有一个设计术语 (如 Primary),该术语则对应一个可在应用主题中覆盖的颜色属性 (如 Primary 这个术语对应 colorPrimary 这个颜色属性)。这些是您的浅色和深色主题默认的基准色值。

△ 浅色主题的基准 MDC 颜色属性

△ 深色主题的基准 MDC 颜色属性

Material 组件使用这些颜色属性为各个 widget 着色。

△ 一个按钮中用到的颜色属性
例如,将颜色属性用于布局和 widget 样式中,写法如下所示:
app:backgroundTint=”?attr/colorSecondary”

您可能会认得下表中的一些颜色属性,如 colorPrimary。这些因为一些颜色属性继承自 AppCompat 和平台本身,而其余属性则来由 MDC。每个颜色属性的完整来源见下表。



挑选颜色


主题里每个颜色槽应该使用的具体颜色值由设计师负责给出,或是取自您的产品品牌。但是,了解每个颜色的作用、颜色之间的关系以及如何满足无障碍功能要求仍非常有用:
  • colorPrimarycolorSecondary 是用于呈现品牌的颜色
  • colorPrimaryVariantcolorSecondaryVariant 是品牌颜色较浅或较深的阴影色
  • colorSurface 用于表单或表面 (如卡片颜色和应用的底部弹出菜单颜色)
  • android:colorBackground 是应用的窗口背景颜色
  • 顾名思义,colorError 用于错误和警告
  • 各种各样的 "On" 颜色 (colorOnPrimarycolorOnSecondarycolorOnSurface 等) 用于为显示在其他颜色上方的前景内容 (如文本和图标) 进行着色。这些颜色需要满足无障碍功能要求并与所在表面的颜色有足够的对比度。


颜色工具


Material Design 为大家提供了丰富的工具,用于预览颜色、确定合适的变体颜色以及 "On" 颜色:
  • 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> 资源。对于自定义颜色,我们建议使用以下两种方法,以帮助区分关注点,并在应用中为颜色主题中的色值创建单一来源:
  • 将浅色和深色主题用到的全部 <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 组件将根据主题全局应用您覆盖好的颜色:



颜色复用性和最佳实践


在许多情况下,都要在布局、可绘制对象、样式和其他位置使用颜色。我们将介绍一些可尽量复用代码的方法,而且不会影响您在应用主题中指定的色值。


首选属性
我们强烈建议使用 ?attr/ 语法来设置颜色。在创建可复用的布局和支持多个模式 (如浅色/深色) 的默认样式时,尤其推荐使用这种语法。
<Button    ...-    app:backgroundTint="@color/green_300"+    app:backgroundTint="?attr/colorPrimary"-    android:textColor="@color/black"+    android:textColor="?attr/colorOnPrimary"/>

请参阅 Nick Butcher 的《Android 样式系统 | 主题背景属性》,以了解更多说明和此规则的一些例外情况。


带有 alpha 的颜色

有时,您可能希望使用 MDC 主题中的一种颜色,但带上 alpha 值 (例如 60% 的 colorPrimary)。例如,触发点击时的波纹动画和项目被选中的状态。


Android <color> 资源支持 alpha 通道:

<!-- 60% alpha = 99 -->
<color name=”navy_700_alpha_60”>#9937596D</color>

但是,在使用此方法时,我们需要将每个带 alpha 的色值视作颜色资源。这也意味着我们不能用 ?attr/ 来使用这些颜色资源,因为这种做法违背了上文提及的唯一来源准则。

因此,我们建议使用 res/color 目录中存储的 ColorStateList (CSL)。CSL 里的项目可以包含一个颜色引用和 alpha 值,这非常适合我们的用例:
<!-- In res/color/primary_60.xml --><selector ...> <item android:alpha="0.6" android:color="?attr/colorPrimary" /></selector>

使用这些 CSL (通过 @color/primary_60 语法进行引用) 可能会让您感到惊讶,但考虑到 CSL 本身也使用 ?attr/ 来引用底层的主题颜色,因此这样做不会有什么问题。

每种状态的颜色和主题叠加

更为常见的情况是根据视图状态使用 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>

在 XML 中应用主题叠加时,应考虑两个选项:
  • 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 用于其图标和文本呢?让我们来看一下几个选项。


构建 Material 主题
构建 Material 主题是一个交互式 Android 项目,支持您通过自定义颜色、字体和形状来创建自己的 Material 主题。项目还包含所有主题中用到的参数和组件的目录。可通过执行以下操作确定哪些 widget 会对主题颜色属性的更改作出响应:
  • 复制这个项目并在 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 主题中的色值变化


MDC 开发者文档
最近我们更新了 MDC 开发者文档。在本次更新中,我们加入了属性表,涵盖了开发库中所使用的设计术语和默认值。例如下面是更新的按钮文档的 "Anatomy and key properties" (详解和关键属性) 部分。
△ MDC 按钮开发文档的属性表中提供了默认色值
  • 按钮文档
    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 中的属性名称非常有用。使用 <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>

MaterialColors 实用程序类

可以通过便利的新 MDC 类 (MaterialColors) 以编程方式处理主题的颜色属性,这对于自定义视图也非常有用:

// Resolve color from theme attrval primaryColor = MaterialColors.getColor( view, R.attr.colorPrimary)
// Layer background color with overlay color + alphaval 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


如果您已成功实现颜色主题或您在实现期间遇到问题,欢迎在下方评论区和我们分享。



推荐阅读






 点击屏末 | 阅读原文 | 查看 Material Design 设计指南



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存