打造 Material 形状主题 | 实现篇

作者 / Nick Rout,Material 开发技术推广工程师

Material 主题 (theming) 是一种自定义 Material 组件的方式,目的是使其与品牌保持一致。Material 主题涉及包括颜色、排版和形状,您可以对它们进行调整来获得近乎无限的组件变体,且依然保持其核心结构和易用性。

  • Material 主题

    https://material.io/design/material-theming/overview.html#material-theming

  • Material 组件

    https://material.io/components

  • 颜色

    https://material.io/design/color/

  • 排版

    https://material.io/design/typography/

  • 形状

    https://material.io/design/shape/

自版本 1.1.0 开始,您可在 Android 上通过 Material Components (MDC) 库实现 Material 主题。如果您要从设计支持库 (Design Support Library) 或 MDC 1.0.0 迁移,请查看我们的迁移指南。

  • MDC 库

    https://github.com/material-components/material-components-android

  • 迁移指南

    https://material.io/blog/migrate-android-material-components

本文将重点讨论如何实现形状主题

大多数 widget 都有一个背景形状,但您有没有思考过形状对用户行为的影响?就像颜色和排版一样,形状可以引导用户的注意力,展示交互性,并在视觉上区分界面中的元素。Material 的形状主题可以定义全局形状值,从而改变整个应用中组件的样式。例如,让所有的卡片、对话框和菜单都呈现出打磨过的圆角。

形状属性

Material Design 提供了 3 个形状 "类别",适用于应用中大大小小有形的 widget。每个类别都有一个设计术语 (例如 "小型组件"),以及相应的可以在您的应用主题中进行自定义的形状属性 (例如 shapeAppearanceSmallComponent)。每个类别都有默认的 "基准" 值 (角尺寸、角形状等)。

△ MDC 形状属性与基准值

Material 组件使用这些形状属性来设置 widget 背景的样式。

△ 一个按钮所用的形状属性 (红色)

它们以如下形式:

app:shapeAppearance=”?attr/shapeAppearanceSmallComponent”

应用于布局和 widget 样式中。

在 MDC 主题中,这些属性会映射到样式,如:

<style name=”Theme.MaterialComponents.*” parent=”...”>
    ...
    <item name=”shapeAppearanceMediumComponent”>
        @style/ShapeAppearance.MaterialComponents.MediumComponent
    </item>
<style />

ShapeAppearance 样式和对应属性对于 MDC 是新增内容,将在本文接下来的 "形状资源" 部分详细介绍。

选择形状

主题中使用的形状类别以及其中的参数取值可能会由设计师负责给出,也可能取自您的产品品牌。不过,了解每个形状类别的作用以及使用场景还是很有用的:

  • shapeAppearanceSmallComponent 用于按钮和文本字段等小尺寸组件

  • shapeAppearanceMediumComponent 用于卡片和对话框等中尺寸组件

  • shapeAppearanceLargeComponent 用于底部菜单等大尺寸组件

请参阅形状设计指南,了解每个组件对应的形状类别。

  • 形状设计指南

    https://material.io/design/shape/applying-shape-to-ui.html#shape-scheme

形状工具

Material Design 提供了一个实用的形状自定义工具,用于预览形状类别以及参数被如何应用到各种组件的倒角上。

  • 形状自定义工具

    https://material.io/design/shape/about-shape.html#shape-customization-tool

△ 形状自定义工具

形状资源

形状资源主要由 ShapeAppearance 样式组成。这类似于排版主题中的 TextAppearance 样式;在本文的上下文里,"样式" 仅关注形状属性。我们来看看 Android 与 MDC 上有哪些可用的样式,以及在声明样式时需要注意的几个问题。

XML 形状和 android:background

在 MDC 之前,您通常需要在 res/drawable 目录下定义一个自定义背景,例如:

<shape android:shape="rectangle">
    … 
    <corners android:radius="8dp" />
    <solid android:color="?attr/colorSurface" />
</shape>

它应用于这样的 widget:

<View
    …
    android:background=”@drawable/shape_background” />

这是一个简化示例。XML 形状可绘制对象可以包括许多其他元素,例如 <inset>、<stroke>、<gradient> 等,或支持多种状态。

虽然有些时候确实有必要使用这种方法,但仍有一些缺点值得注意:

  • 它缺乏其他主题系统的许多实用功能 (如颜色和字体),指定主题级别形状的预定义属性,叠加层以及从样式中抽象出形状值的能力

  • Material Design 的形状系统支持圆角和切角,但通过 XML 或编程方式并没有足够好的解决方案可以实现切角

  • 难以处理复杂形状,如无法实现底部应用栏顶部的倒角,并且需要实现一个自定义的 Drawable

  • Material Design 形状系统

    https://material.io/design/shape/about-shape.html#shaping-material

  • 底部应用栏

    https://material.io/components/app-bars-bottom

ShapeAppearance 样式

MDC 提供了一种定义形状的新方法。ShapeAppearance 样式可被视为 Material Design 形状在 Android 系统中的等效项。这些样式提供了一种无需直接处理可绘制对象即可定义形状特征的方法。目前仅适用于 MDC widget,并由一个新的 MaterialShapeDrawable 类支持,下文会有详细介绍。

定义形状主题时,我们有两种推荐方法来进行解耦,并为您应用中的形状参数创建单一数据来源:

  • 在一个 res/values/shape.xml 文件中保存所有 ShapeAppearance 样式

  • 将 MDC ShapeAppearance 样式用作父级并遵循相同的命名规则

您可以在这些样式中使用的属性和值与 MaterialShapeDrawable 支持的一致:

  • cornerFamily 是所有角的形状,分为 "圆角" 和 "切角"

  • cornerFamilyTopLeft、cornerFamilyTopRight、cornerFamilyBottomLeft 和 cornerFamilyBottomRight 允许您更改特定角的形状,并且优先于 cornerFamily

  • cornerSize 是所有角的尺寸,通常为 dp 尺寸

  • cornerSizeTopLeft、cornerSizeTopRight、cornerSizeBottomLeft 和 cornerSizeBottomRight 允许您更改特定角的尺寸,并且优先于 cornerSize

<!-- In res/values/shape.xml -->
<style name="ShapeAppearance.App.SmallComponent" parent="ShapeAppearance.MaterialComponents.SmallComponent">
    <item name="cornerFamily">cut</item>
    <item name="cornerSize">4dp</item>
    ...
</style>
<style name="ShapeAppearance.App.MediumComponent" parent="ShapeAppearance.MaterialComponents.MediumComponent">
    <item name="cornerFamily">cut</item>
    <item name="cornerSize">6dp</item>
    ...
</style>
<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.MaterialComponents.LargeComponent">
    <item name="cornerFamily">cut</item>
    <item name="cornerSize">0dp</item>
    ...
</style>

ShapeAppearance 叠加层

您还可以定义 ShapeAppearance 叠加层,它支持所有相同的属性,行为上也类似于主题叠加层。

它们可以通过 app:shapeAppearanceOverlay 与常规 ShapeAppearance 样式一起应用,以更改特定某个角的属性值。以下是底部菜单叠加层的示例 (来自 MDC 源代码),将菜单底部两个角的半径设为和屏幕的角半径相同:

<!-- In bottomsheet/res/values/styles.xml -->
<style name="Widget.MaterialComponents.BottomSheet" parent="...">
    ...
    <item name="shapeAppearance">?attr/shapeAppearanceLargeComponent</item>
    <item name="shapeAppearanceOverlay">@style/ShapeAppearanceOverlay.MaterialComponents.BottomSheet</item>
</selector>
<style name="ShapeAppearanceOverlay.MaterialComponents.BottomSheet" parent="">
    <item name="cornerSizeBottomRight">0dp</item>
    <item name="cornerSizeBottomLeft">0dp</item>
</style>

注: 某些 MDC widget 默认已应用叠加层,您在调整其 shapeAppearance 时可能需要考虑这些叠加层。例如 FloatingActionButtonChip,它们都通过叠加层将 cornerSize 设置为 50%。

  • FloatingActionButton

    https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/floatingactionbutton/FloatingActionButton.java

  • Clip

    https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/chip/Chip.java

填充和描边

与 XML 可绘制对象不同,ShapeAppearance 样式不包括任何填充或描边的概念。MDC 倾向于在主 widget 样式中单独指定这些值来减少耦合:

<style name=”Widget.MaterialComponents.*” parent=”...”>
    <item name=”backgroundTint”>?attr/colorSurface</item>
    <item name="strokeColor">?attr/colorOnSurface</item>
    <item name="strokeWidth">1dp</item>
    <item name=”shapeAppearance”>?attr/shapeAppearanceLargeComponent</item>
</style>

注: ShapeAppearance 样式和背后的 MaterialShapeDrawable 类仅支持纯色填充和描边。目前尚不支持渐变,您需要将 XML 可绘制对象与 <gradient> 结合使用来实现渐变。

在应用主题中覆盖形状

接下来我们来看如何通过覆盖相关属性将您选择的形状添加到应用主题中。

首先,您的主题需要妥善处理浅色和深色调色板,同时减少与基础主题的重复。有关这方面的更多信息,请查看 Chris Banes 关于深色主题的文章,以及他和 Nick Butcher 的 "如何正确开发外观样式" 演讲。

设置完成后,在基础主题中覆盖要更改的形状属性:

<!-- In res/values/themes.xml -->
<style name="Theme.App.Base" parent="Theme.MaterialComponents.*">
    ...
    <item name="shapeAppearanceSmallComponent">
        @style/ShapeAppearance.App.SmallComponent
    </item>
    <item name="shapeAppearanceMediumComponent">
        @style/ShapeAppearance.App.MediumComponent
    </item>
    <item name="shapeAppearanceLargeComponent">
        @style/ShapeAppearance.App.LargeComponent
    </item>
</style>

Material Design 组件将响应主题级别的形状覆盖:

△ Material Design 组件响应主题级别的形状覆盖

MaterialShapeDrawable

形状主题由 MaterialShapeDrawable 类驱动。它是所有 MDC widget 默认的背景可绘制对象,并用于呈现形状。与其他可绘制对象不同,它无法在 XML 中使用,需要以编程方式处理。

  • MaterialShapeDrawable

    https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/shape/MaterialShapeDrawable.java

△ MaterialShapeDrawable 和 ShapeAppearanceModel 图示

MaterialShapeDrawable 可以这样实例化:

// Default constructor
val msd = MaterialShapeDrawable()
// ShapeAppearanceModel constructor
val msdFromSam = MaterialShapeDrawable(shapeAppearanceModel)
// Style/attr resources constructor (reads shapeAppearance and shapeAppearanceOverlay)
val msdFromStyles = MaterialShapeDrawable(context, attrs, defStyleAttr, defStyleRes)
// Cast from widget background
val msdFromWidget = widget.background as MaterialShapeDrawable

ShapeAppearanceModel

ShapeAppearanceModel 是 ShapeAppearance 样式的程序化等效项,它存储形状的倒角和边线的数据。MaterialShapeDrawable 则使用此类渲染其形状。

  • ShapeAppearanceModel

    https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/shape/ShapeAppearanceModel.java

生成器模式用于实例化 ShapeAppearanceModel:

// Default builder
val sam = ShapeAppearanceModel.builder()
    .setAllCorners(CornerFamily.CUT, cornerSize)
    // Also setTopRightCorner, setAllEdges, etc.
    .build()
// Style/attr resources builder (reads shapeAppearance and shapeAppearanceOverlay)
val samFromStyles = ShapeAppearanceModel.builder(context, attrs, defStyleAttr, defStyleRes)
    .build()
// Build from existing ShapeAppearanceModel
val samFromExisting = sam.toBuilder()
    ...
    .build()

有关边线和自定义路径更高级的示例,请参阅 MDC 目录中的 BottomAppBarCutCornersTopEdge。

  • BottomAppBarCutCornersTopEdge

    https://github.com/material-components/material-components-android/blob/master/catalog/java/io/material/catalog/bottomappbar/BottomAppBarCutCornersTopEdge.java

填充和描边

MaterialShapeDrawable 处理填充和描边的渲染。有许多方法可以调整这些属性:

// Fill color
msd.setFillColor(fillColorStateList)
// Stroke color
msd.setStrokeColor(strokeColorStateList)
// Stroke width
msd.setStrokeWidth(strokeWidthDimension)

高程和叠加层

MaterialShapeDrawable 负责渲染叠加层以呈现深色主题中的高程 (深色主题时不使用阴影,而是使用明暗表示高程),这些操作由 MDC widget 默认处理。启用和使用此功能的方法如下:

// Initialize elevation overlays
msd.initializeElevationOverlay(context)
// Pass elevation value to MSD to apply overlay (in dark theme)
msd.setElevation(elevation)

如需了解更多信息,请参阅之前发布的《打造 Material 颜色主题 | 实现篇》以及 Chris Banes 关于深色主题的文章

阴影渲染

平台只在 API 21 及更高级别中支持高程阴影的渲染。MaterialShapeDrawable 为向后移植阴影渲染提供了可能性:

/**
 * Set shadow compat mode to be one of:
 * - SHADOW_COMPAT_MODE_DEFAULT: Use platform rendering on API 21+, else compat rendering
 * - SHADOW_COMPAT_MODE_NEVER: Use platform rendering always
 * - SHADOW_COMPAT_MODE_ALWAYS: Use compay rendering always
 */
msd.setShadowCompatibilityMode(shadowMode)

角插值

MaterialShapeDrawable 提供了所有角尺寸的插值方法,通过提供从 0.0~1.0 之间取值的乘数,方便开发者在动画和转场中使用。

// Set corner interpolation to half of current cornerSize(s)
msd.setInterpolation(0.5f)

了解 MDC widget 中的形状

如前所述,MDC widget 会响应主题级别覆盖的形状属性。但是,举例来说,怎样才能知道一个按钮会使用 shapeAppearanceSmallComponent 作为其容器的样式?让我们来看看几种学习途径。

使用 "构建 Material 主题" 项目

构建 Material 主题是一个交互式 Android 项目,您可以自定义颜色、排版和形状来创建自己的 Material 主题。项目里还包含了所有主题的参数和组件的目录。要确定哪些 widget 响应主题中属性的变化,可以这样操作:

  • 复制项目并在 Android Studio 中运行应用

  • 修改 res/values/shape.xml 以及 res/values/themes.xml 中的值

  • 重新运行应用并观察界面中发生的变化

  • 构建 Material 主题

    https://github.com/material-components/material-components-android-examples/tree/develop/MaterialThemeBuilder

  • Shape.xml

    https://github.com/material-components/material-components-android-examples/blob/develop/MaterialThemeBuilder/app/src/main/res/values/shape.xml

  • Themes.xml

    https://github.com/material-components/material-components-android-examples/blob/develop/MaterialThemeBuilder/app/src/main/res/values/themes.xml

△ 构建 Material 主题中形状值的变化

阅读 MDC 开发者文档

我们最近更新了 MDC 开发者文档,加入了属性表,其中给出了库中所使用的设计术语和默认值。例如,下图是按钮文档的 "Anatomy and key properties"(详解和关键属性) 部分。

  • 按钮文档

    https://material.io/components/buttons/android

△ MDC 开发者文档中按钮的形状属性表以及默认值

阅读源代码

阅读 MDC 源代码可谓是最可靠的方法。MDC 使用默认样式实现 Material 主题,因此看一看这些样式以及所有可设置的属性和 java 源文件是个不错的办法。例如,查看 MaterialButton 的 styles、attrs 和 java 源文件。

  • Material Button 的源文件

    https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/button/res/values/styles.xml

    https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/button/res/values/attrs.xml

    https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/button/MaterialButton.java

一个有趣的做法是观察 MDC widget 如何使用默认样式来确保 MaterialShapeDrawable 为默认背景。一般的做法是:

  • 在 widget 默认样式中将 android:background 设置为 @null 或 @empty

  • 如果在解析属性时未检测到背景,则以编程方式实例化一个 MaterialShapeDrawable 并将其设置为背景

  • 如果已设置背景 (例如在布局或自定义样式中),则予以保留,并不再使用 MaterialShapeDrawable

△ MDC 按钮默认样式以及取值

自定义视图中的形状

您的应用可能包含自己构建或从现有库中获得的自定义 widget。在和标准 MDC widget 进行混用时,让这些 widget 支持 Material 主题非常有用。我们来看看让自定义 widget 支持形状主题时要注意些什么。

在 <declare-styleable> 和默认样式中使用 MDC 属性

通过使用 <declare-styleable> 可以让自定义视图支持样式。在保持一致性方面,复用 MDC 中的属性名称是个很好的做法。使用 <declare-styleable> 的默认样式还可以引用 MDC 主题的形状属性为其赋值,同时也可以通过使用 @null/@empty 来实现 MaterialShapeDrawable 背景:

<!-- In res/values/attrs.xml -->
<declare-styleable name="AppCustomView">
    <attr name="shapeAppearance" />
    ...
</declare-styleable>




<!-- In res/values/styles.xml -->
<style name="Widget.App.CustomView" parent="android:Widget">
    <item name="android:background">@null</item>
    <item name="shapeAppearance">?attr/shapeAppearanceMediumComponent</item>
    ...
</style>

注意高程和叠加层

如果您希望自定义视图支持高程叠加层或向后移植阴影渲染,最好覆盖 setElevation 方法并将其值传递至 MaterialShapeDrawable 背景:

class AppCustomView ... {




    ...




    private lateinit var materialShapeDrawable: MaterialShapeDrawable




    override fun setElevation(elevation: Float) {
        super.setElevation(elevation)
        materialShapeDrawable.setElevation(elevation)
    }
}

下一步

现在,我们已经在 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-color

  • 深色主题

  • 动效系统

    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 设计指南


已标记关键词 清除标记
相关推荐