打造 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”
<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>
<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
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 叠加层,它支持所有相同的属性,行为上也类似于主题叠加层。
它们可以通过 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 时可能需要考虑这些叠加层。例如 FloatingActionButton 和 Chip,它们都通过叠加层将 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> 结合使用来实现渐变。
在应用主题中覆盖形状
接下来我们来看如何通过覆盖相关属性将您选择的形状添加到应用主题中。
<!-- 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
// 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 中的形状
使用 "构建 Material 主题" 项目
复制项目并在 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
阅读 MDC 开发者文档
我们最近更新了 MDC 开发者文档,加入了属性表,其中给出了库中所使用的设计术语和默认值。例如,下图是按钮文档的 "Anatomy and key properties"(详解和关键属性) 部分。
按钮文档 https://material.io/components/buttons/android
阅读源代码
阅读 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
在 widget 默认样式中将 android:background 设置为 @null 或 @empty
如果在解析属性时未检测到背景,则以编程方式实例化一个 MaterialShapeDrawable 并将其设置为背景
如果已设置背景 (例如在布局或自定义样式中),则予以保留,并不再使用 MaterialShapeDrawable
自定义视图中的形状
您的应用可能包含自己构建或从现有库中获得的自定义 widget。在和标准 MDC widget 进行混用时,让这些 widget 支持 Material 主题非常有用。我们来看看让自定义 widget 支持形状主题时要注意些什么。
通过使用 <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
推荐阅读