使用Material Design组件实现深色主题

/   今日科技快讯   /

微软于9月22日周三宣布其五款最新的Surface产品,包括四款电脑产品Surface Pro 8、Surface Go 3、Surface Laptop Studio 和 Surface Pro X,以及此前市场关注一款新型折叠手机Surface Duo 2。

新的旗舰Surface Pro 8将带来自Surface Pro 3 七年前推出以来最大的硬件设计和规格变化。平板电脑本身的边缘将更加圆润,而且新的Surface 键盘还将容纳一个新的超薄Surface Pen,起售价为1100美元。

/   前言   /

Material 主题 (Theming) 涵盖的内容很多,但都旨在帮助开发者打造优良且富有表现力的用户体验,如果您对 Material 主题的颜色、字体和形状内容感兴趣的话,欢迎阅读 Nick Rout 的系列博文:

  • Material Design 组件: Android 上的颜色主题

    https://medium.com/androiddevelopers/material-theming-with-mdc-color-860dbba8ce2f

  • Material Design 组件: Android 上的字体主题

    https://medium.com/androiddevelopers/material-theming-with-mdc-type-8c2013430247

  • Material Design 组件: Android 上的形状主题

    https://medium.com/androiddevelopers/material-theming-with-mdc-shape-126c4e5cd7b4

本文将在此基础上,探讨如何调整应用以支持深色主题。

用户可选的深色主题在 Android 10 中被添加到 Android 平台,但应用开发者们应该早就接触过深色主题了: 在 Android 5.0 (Lollipop) 之前,Android 设备的默认主题都是深色!

  • Android 10

    https://developer.android.google.cn/about/versions/10

  • Android 5.0

    https://developer.android.google.cn/about/versions/lollipop

去年加入的深色主题的特别之处在于,平台增加了一个设备级别的设置,让用户可以对设备整体的主题进行控制,同时还能设置应用单独的主题。

除了最近加入的设备级别设置,在 material.io 上还提供了全面的设计指南,我们将在本文中详细介绍。

  • 设计指南: 深色主题

    https://material.io/design/color/dark-theme.html

/   为什么要支持深色主题   /

首先,到底为什么要支持深色主题?在 material.io 上很好地总结了深色主题的技术优势 (我加粗了想要强调的部分):

深色主题可以降低设备屏幕发出的亮度 […]。能够通过减少用眼疲劳、根据当前照明条件调节亮度,以及让屏幕在黑暗环境中也便于观看等方式,改善视觉工效,同时还能降低电量消耗 [OLED 显示屏]。

  • Material Design: 深色主题

    https://material.io/design/color/dark-theme.html#usage

不过,最根本的原因是用户想要深色主题——这是用户们一直以来的首要需求,因此 Android 团队添加了系统级别的深色主题设置。

相信看到这里,您已经打算在应用中支持深色主题了,接下来就让我们来看看怎么实现它。

/   快速入门   /

要向应用添加深色主题,可以使用 Android 版的 Material Design 组件 (MDC)。

  • Material Design 组件: Android

    https://material.io/develop/android/

1. 更改主题

您需要更改主题,使其扩展自一个 Theme.MaterialComponents.DayNight 主题:

<style name="Theme.MyApp"
    parent="Theme.MaterialComponents.DayNight">
    <!-- Other theme attributes -->
</style>

2. 选择模式 (可选)

这是可选步骤,可以支持 Android 10 之前版本的设备。由于 Android 10 之前大多数设备没有系统级的深色主题设置*,应用可以提供自己的应用内设置,允许用户按应用选择主题。

* 严格来说并不绝对是这样,因为有些设备制造商已经在运行 Android 9 (和更低版本) 的设备上添加了系统级深色主题。只不过这一点无法在运行时确定。

4580bc164435527cea9b4f18dc667674.png

△ 应用内深色主题设置示例

这在 Android 10 及以上版本的系统中也很有用,因为这让用户可以根据需要覆盖系统设置。比如用户将设备主题设置为按时间调整,但又希望社交应用始终为深色主题。

为了做到这一点,(MDC 使用的) AppCompat 提供了一个 API 来设置模式: AppCompatDelegate.setDefaultNightMode()。通常,当偏好设置发生变化时就会调用这个 API。

  • AppCompat

    https://developer.android.google.cn/jetpack/androidx/releases/appcompat

  • setDefaultNightMode()

    https://developer.android.google.cn/reference/androidx/appcompat/app/AppCompatDelegate#setDefaultNightMode(int)

如果您想进一步了解 AppCompat 中夜间模式功能的运作细节,可以阅读这篇博文。

  • DayNight - 在应用中添加深色主题

    https://medium.com/androiddevelopers/appcompat-v23-2-daynight-d10f90c83e94

3. 测试!

现在,深色主题的基础已经完成!接下来应该在浅色和深色主题下检查应用的各个部分。要注意深色背景上是否有深色文本,以及相对于深色背景对比度不足的硬编码颜色 (通常为灰色)。

如果您在应用中使用了硬编码颜色值,建议阅读 Nick Butcher 的《Android 样式系统 | 主题背景属性》或观看我们在 Android Dev Summit ‘19 的演讲《如何正确开发外观样式》。

/   Material深色主题   /

现在来看看 material.io 上介绍的深色主题设计特征。

灰色与黑色

首先您可能会注意到,深色主题应用的默认背景不是黑色,而是深灰色: #121212

关于我们为什么选择灰色而不是黑色的讨论有很多,尤其是考虑到 Android 10 平台使用的是黑色背景。这主要是我们在易用性与节能之间权衡的结果。

在平台中使用纯黑 #000000 色作为背景,可以让系统应用和表面 (surfaces) 在 OLED 显示屏上显示时消耗最少的电量。这些系统表面往往很简单,通常只有文本和简单的图标,因此可以根据需要调整文本和图标的颜色解决对比度问题。

不过在应用中,表面可以包含任何内容: 复杂的彩色矢量动画、明亮的图像、对比强烈的品牌表面等。在纯黑背景下会产生非常高的对比度,这会增加用眼疲劳。不同于先前提到的文本和图标,这些更复杂的内容通常很难或不适合为了降低对比度而调色/重新着色,因此稍浅一些的背景色更加合适。

调色板

接下来让我们来看看应用的调色板。您很可能是按照浅色/白色背景为应用选择的调色板,因此当应用以深色主题运行时,我们需要对调色板做出一些调整。

Material 颜色系统

我们先快速回顾一下 Material 颜色系统,稍后会详细谈及色调的问题。Material 颜色系统将颜色定义为每种颜色内的一系列色调。色调编号从 50 (最浅、饱和度最低的色调) 到 900 (最深、饱和度最高的色调)。这是基准蓝绿色和靛蓝色调:

5e5679a3ade222600669e55677a6e984.png

△ 基准 Material 调色板

您也可以在 Material 颜色工具中上手操作,了解不同颜色的色调如何变化。Nick Rout 也在这篇文章里对颜色系统进行了深度剖析。

  • Material 颜色工具

    https://material.io/resources/color/

  • Material Design 组件: Android 上的颜色主题

    https://medium.com/androiddevelopers/material-theming-with-mdc-color-860dbba8ce2f

colorPrimary

应用的主色是显示最多的颜色 (除了背景和表面颜色),所以我们需要确保它在深色主题中清晰可辨。通常,浅色主题会是一个 500 色调的颜色,而在深色主题中,我们建议使用饱和度较低、亮度较高的色调,一般为 200,但根据不同色相最多可以达到 50

对于 colorPrimaryVariant,我们建议使用浅色主题中的 colorPrimary。下面是一个简单的参考表格:

09cdeb2f7d342096d8a12be23ba6c58d.png

这些值只是调整的基础。您应该确保所选颜色在所有使用的高程上与背景/表面颜色的 WCAG AA 对比度至少为 4.5:1 (后文会详细介绍)。

  • WCAG AA

    https://www.w3.org/WAI/standards-guidelines/wcag/

52e5fe8824d36c783de3754fee261006.png

Material 颜色工具非常适合尝试新颜色。

  • Material 颜色工具

    https://material.io/resources/color/

colorSecondary

对于辅色,和 colorPrimary 的处理方法一样,使用饱和度较低、亮度较高的同色色调。

基准 Material 深色主题对待 colorSecondaryVariant 与 colorPrimaryVariant 的方式有些不同,对 colorSecondary 和 colorSecondaryVariant 使用相同的色调。

下面是另一个简单的参考表格:

83d1d6a7f9f230d9c6fd1600465557be.png

表面颜色

大胆的彩色表面是在卡片等常用组件中表达品牌的好方法。虽然鲜艳大胆的颜色在白色背景下效果很好,但在深色背景下可能就是另一回事了。

  • 在 UI 中使用颜色

    https://material.io/design/color/applying-color-to-ui.html#sheets-and-surfaces

如果设备和/或应用已经设置为使用深色主题,意味着用户在那一刻想要一个不太花哨的柔和配色方案。

考虑到这种意图,即使我们在品牌表面使用 50-200 的柔和色调,对于深色主题来说,它仍然可能过于鲜艳和明亮:

b17a121d22ee265abe9314ab0f990b09.png

△ 不要这样做。演示中底部应用栏的表面颜色过于明艳

那么您应该怎么做呢?以下两个选项可以结合使用:

1. 使用主表面

第一步当然是在深色主题下不使用明亮、多彩的表面。Android 版 Material Design 组件使用 PrimarySurface 样式简化了这一过程,您可以在浅色主题中使用 Primary 颜色,在深色主题中使用 Surface 颜色。

下面我们来看一个示例。假设我们有一个像上个示例一样的 BottomAppBar,那么可以使用 Widget.MaterialComponents.BottomAppBar.PrimarySurface 样式:

<com.google.android.material.bottomappbar.BottomAppBar
    style="Widget.MaterialComponents.BottomAppBar.PrimarySurface"
/>
  • BottomAppBar

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

如果您想对一个非 MDC 视图进行类似处理,可以使用 ?attr/colorPrimarySurface 主题属性:

<FrameLayout
    android:background="?attr/colorPrimarySurface"
/>

eb5f063aa3b875091afa1d0745187aa7.png

△ 通过样式和属性实现 PrimarySurface 的示例

事实上,在浅色主题中使用明亮表面颜色的组件 (如 MaterialToolbar) 默认具有同样的行为。因此您在使用它们时可能不需要做任何工作。

  • MaterialToolbar

    https://material.io/develop/android/components/top-app-bars/

2. 使用品牌表面颜色

要在应用的所有表面含蓄地表现品牌色彩,您可以在深色主题时将 colorSurface 设置为不透明度 8% 的 colorPrimary 与 colorSurface 叠加之后的颜色。

例如,使用基准主题的色值:

451fa906476e5bebe34e5f21b56cf24c.png

△ 如何计算品牌表面颜色

这样一来,您就可以在遵循柔和、低亮度颜色意图的前提下,将品牌颜色巧妙地融入整个应用。

制作深色主题的示例

如果您想查看向浅色主题应用加入深色主题的示例,可以观看 Liam Spradlin 的视频,了解如何为 Reply 这个 Material 教学应用添加深色主题。

  • 使用 Material Design 打造深色主题

    https://youtu.be/hbJmm-d94FA

  • Reply 应用

    https://material.io/design/material-studies/reply.html

我们已经介绍过许多有关选择颜色的知识,但是如何在 Android 应用中进行设置呢?

我们要搭建一个主题的结构。如下所示:

5c4fbfd2bd65d45d6bf6f4d1b4c3e88e.png

△ 适合深色主题的主题结构

这种结构让我们可以轻松地在浅色和深色主题中改变主题,还允许我们在基础主题中重用常见的内容。

如果您想进一步了解这种结构,建议观看去年 Nick Butcher 演讲的《如何正确开发外观样式》。

<style name="Base.Theme.Tivi"
    parent="Theme.MaterialComponents.DayNight">
    <!-- Your app theme, minus color palette -->
</style>

<style name="Theme.Tivi" parent="Base.Theme.Tivi">
    <item name="colorPrimary">@color/slate_500</item>
    <item name="colorOnPrimary">#000000</item>
    <item name="colorSecondary">@color/orange_500</item>
    <item name="colorOnSecondary">#000000</item>
</style>

△ values/themes.xml

<style name="Base.Theme.Tivi"
    parent="Theme.MaterialComponents.DayNight">
    <!-- Your app theme, minus color palette -->
</style>
<style name="Theme.Tivi" parent="Base.Theme.Tivi">
    <item name="colorSurface">@color/slate_200_8pc_surface</item>
    <item name="colorPrimary">@color/slate_200</item>
    <item name="colorOnPrimary">#000000</item>
    <item name="colorSecondary">@color/orange_00</item>
    <item name="colorOnSecondary">#000000</item>
</style>

△ values-night/themes.xml

/   高度叠加层   /

前文已经提到了需要针对所有高程进行对比度测试。您可能会困惑,毕竟高程是关于提升表面来投射阴影的吧?没错,高程是与提升表面有关,但不仅仅是为了投射阴影。

Material 系统中的阴影是由许多光源投射形成的,当我们 (使用高程属性) 提升表面时,是在把它们朝着光源提升。和我们的现实世界一样,当这些光源被表面遮挡时就会出现阴影。同样,表面离光源越近,表面被照亮的程度就越高,从而改变了呈现的颜色。

  • 光源

    https://material.io/design/environment/light-shadows.html#light

对于白色等浅色表面,这种变化不易察觉。但在深色表面上则会产生很大的影响:

063e353338a1978816fc1f05c6003991.gif

△ 不同高程的高度叠加层演示

这就是高度叠加层起作用的地方。提亮表面颜色的行为表现为在表面颜色上叠加一个半透明的白色 onSurface 层。高程越大,叠加层越不透明,表面也就越亮。

  • 高度叠加层

    https://material.io/design/color/dark-theme.html#properties

这就是先前提到的需要在不同高程进行测试的原因。由于视觉表面会根据高程变化,您需要确保所有前景色都能提供足够的对比度。理想情况下,可以设置一个单一 onSurface 颜色来适用于应用中的所有高程。

/   Widget支持   /

MDC 中的所有组件都自动支持高度叠加层,包括: 顶部应用栏、底部应用栏、底部导航、标签页、卡片、对话框、菜单、底部动作条、抽屉式导航栏和开关。

  • 顶部应用栏

    https://material.io/components/android/catalog/top-app-bars/

  • 底部应用栏

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

  • 底部导航

    https://material.io/components/android/catalog/bottom-navigation/

  • 标签页

    https://material.io/components/android/catalog/tab-layout/

  • 卡片

    https://material.io/components/android/catalog/cards/

  • 对话框

    https://material.io/components/android/catalog/dialogs/

  • 菜单

    https://material.io/components/android/catalog/menu/

  • 底部动作条

    https://material.io/components/android/catalog/bottom-sheet-behavior/

  • 抽屉式导航栏

    https://material.io/components/android/catalog/navigation-view/

  • 开关

    https://material.io/components/android/catalog/switches/

因此,只要背景设置为 ?attr/colorSurface (显式使用或使用表面样式变体),使用标准高程 API 就会自动应用高度叠加层。回到我们前面的示例:

<!-- 👍 Elevation overlay is applied: we're using colorSurface -->
<com.google.android.material.bottomappbar.BottomAppBar
    android:elevation="2dp"
    android:background="?attr/colorSurface"
/>

<!-- 👍 Elevation overlay is applied:
    Surface style uses colorSurface -->
<com.google.android.material.bottomappbar.BottomAppBar
    android:elevation="2dp"
    style="@style/Widget.MaterialComponents.BottomAppBar.Surface"
/>

<!-- ❌ No elevation overlay applied: we're using colorSecondary -->
<com.google.android.material.bottomappbar.BottomAppBar
    android:elevation="2dp"
    android:background="?attr/colorSecondary"
/>

<!-- ❌ No elevation overlay applied: we're using colorPrimary -->
<com.google.android.material.bottomappbar.BottomAppBar
    android:elevation="2dp"
    style="@style/Widget.MaterialComponents.BottomAppBar.Primary"
/>

您可以设置一些主题属性来更改高度叠加层的行为:

  • ?attr/elevationOverlayEnabled 允许您打开/关闭主题的高度叠加层。深色主题默认为 true,浅色主题默认为 false。

  • ?attr/elevationOverlayColor 允许您改变任何高度叠加层的颜色。默认为 ?attr/colorOnSurface。

不过,您实际上不需要更改它们。

/   自定义视图   /

如果需要支持高度叠加层的自定义视图怎么办?告诉您一个好消息: MaterialShapeDrawable 直接支持高度叠加层,只需要在视图中进行一点改动即可:

class CustomSurfaceView : View {
    private val shapeDrawable = MaterialShapeDrawable()

    init {
        background = shapeDrawable
        shapeDrawable.initializeElevationOverlay(context)
    }

    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        // Update the shape drawable with this view's absolute
        // elevation value in the view hierarchy
        MaterialShapeUtils.setParentAbsoluteElevation(this, shapeDrawable)
    }

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

    override fun setTranslationZ(translationZ: Float) {
        super.setTranslationZ(translationZ)
        onZChanged()
    }

    override fun setZ(z: Float) {
        super.setTranslationZ(translationZ)
        onZChanged()

    }

    private fun onZChanged() {
        // Tell the ShapeDrawable what our new Z value is
        shapeDrawable.z = z
    }
}
  • MaterialShapeDrawable

    https://developer.android.google.cn/reference/com/google/android/material/shape/MaterialShapeDrawable

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

关于RecyclerView的事件拦截机制

Android 12 SplashScreen API快速入门

欢迎关注我的公众号

学习技术或投稿

d1cfcc93ed6b4178b148b6954e2eade6.png

4b2f73225c13085aa5eebfdc7ba8e432.png

长按上图,识别图中二维码即可关注

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值