/ 今日科技快讯 /
微软于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 (和更低版本) 的设备上添加了系统级深色主题。只不过这一点无法在运行时确定。
△ 应用内深色主题设置示例
这在 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 (最深、饱和度最高的色调)。这是基准蓝绿色和靛蓝色调:
△ 基准 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。下面是一个简单的参考表格:
这些值只是调整的基础。您应该确保所选颜色在所有使用的高程上与背景/表面颜色的 WCAG AA 对比度至少为 4.5:1 (后文会详细介绍)。
WCAG AA
https://www.w3.org/WAI/standards-guidelines/wcag/
Material 颜色工具非常适合尝试新颜色。
Material 颜色工具
https://material.io/resources/color/
colorSecondary
对于辅色,和 colorPrimary 的处理方法一样,使用饱和度较低、亮度较高的同色色调。
基准 Material 深色主题对待 colorSecondaryVariant 与 colorPrimaryVariant 的方式有些不同,对 colorSecondary 和 colorSecondaryVariant 使用相同的色调。
下面是另一个简单的参考表格:
表面颜色
大胆的彩色表面是在卡片等常用组件中表达品牌的好方法。虽然鲜艳大胆的颜色在白色背景下效果很好,但在深色背景下可能就是另一回事了。
在 UI 中使用颜色
https://material.io/design/color/applying-color-to-ui.html#sheets-and-surfaces
如果设备和/或应用已经设置为使用深色主题,意味着用户在那一刻想要一个不太花哨的柔和配色方案。
考虑到这种意图,即使我们在品牌表面使用 50-200 的柔和色调,对于深色主题来说,它仍然可能过于鲜艳和明亮:
△ 不要这样做。演示中底部应用栏的表面颜色过于明艳
那么您应该怎么做呢?以下两个选项可以结合使用:
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"
/>
△ 通过样式和属性实现 PrimarySurface 的示例
事实上,在浅色主题中使用明亮表面颜色的组件 (如 MaterialToolbar) 默认具有同样的行为。因此您在使用它们时可能不需要做任何工作。
MaterialToolbar
https://material.io/develop/android/components/top-app-bars/
2. 使用品牌表面颜色
要在应用的所有表面含蓄地表现品牌色彩,您可以在深色主题时将 colorSurface 设置为不透明度 8% 的 colorPrimary 与 colorSurface 叠加之后的颜色。
例如,使用基准主题的色值:
△ 如何计算品牌表面颜色
这样一来,您就可以在遵循柔和、低亮度颜色意图的前提下,将品牌颜色巧妙地融入整个应用。
制作深色主题的示例
如果您想查看向浅色主题应用加入深色主题的示例,可以观看 Liam Spradlin 的视频,了解如何为 Reply 这个 Material 教学应用添加深色主题。
使用 Material Design 打造深色主题
https://youtu.be/hbJmm-d94FA
Reply 应用
https://material.io/design/material-studies/reply.html
我们已经介绍过许多有关选择颜色的知识,但是如何在 Android 应用中进行设置呢?
我们要搭建一个主题的结构。如下所示:
△ 适合深色主题的主题结构
这种结构让我们可以轻松地在浅色和深色主题中改变主题,还允许我们在基础主题中重用常见的内容。
如果您想进一步了解这种结构,建议观看去年 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
对于白色等浅色表面,这种变化不易察觉。但在深色表面上则会产生很大的影响:
△ 不同高程的高度叠加层演示
这就是高度叠加层起作用的地方。提亮表面颜色的行为表现为在表面颜色上叠加一个半透明的白色 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
推荐阅读:
Android 12 SplashScreen API快速入门
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注