使用 Material Design 组件实现深色主题
作者 / Chris Banes, Android 开发者关系团队工程师
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/
<style name="Theme.MyApp"
parent="Theme.MaterialComponents.DayNight">
<!-- Other theme attributes -->
</style>
* 严格来说并不绝对是这样,因为有些设备制造商已经在运行 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
Material 深色主题
现在来看看 material.io 上介绍的深色主题设计特征。
您也可以在 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 https://www.w3.org/WAI/standards-guidelines/wcag/
Material 颜色工具 https://material.io/resources/color/
在 UI 中使用颜色 https://material.io/design/color/applying-color-to-ui.html#sheets-and-surfaces
如果设备和/或应用已经设置为使用深色主题,意味着用户在那一刻想要一个不太花哨的柔和配色方案。
考虑到这种意图,即使我们在品牌表面使用 50-200 的柔和色调,对于深色主题来说,它仍然可能过于鲜艳和明亮:
那么您应该怎么做呢?以下两个选项可以结合使用:
<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"
/>
MaterialToolbar https://material.io/develop/android/components/top-app-bars/
△ 如何计算品牌表面颜色
这样一来,您就可以在遵循柔和、低亮度颜色意图的前提下,将品牌颜色巧妙地融入整个应用。
使用 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>
<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
OK Google,晚安 🌚
希望本文可以让您了解如何为应用添加深色主题。欢迎在下方评论区留言分享您在实现深色主题时遇到过的问题。
推荐阅读