乐趣区

关于前端:深色模式适配指南

65 篇原创好文~
本文首发于政采云前端团队博客:深色模式适配指南

深色模式适配指南

背景

随着 iOS 13 的公布,深色模式(Dark Mode)越来越多地呈现在公众的视线中,反对深色模式曾经成为古代挪动利用和网站的一个潮流,前段时间更是因为微信的适配再度引起热议。深色模式不仅能够大幅缩小电量的耗费,削弱强光比照,还能 提供更好的可视性和沉迷感。

那针对 一款 App 利用(原生 + H5)怎么进行深色模式的适配呢?明天就让咱们一起来探索吧!

零碎兼容

想要实现深色模式的成果,前提条件是要零碎反对,目前 常见零碎反对状况如下:

H5 深色适配

随着深色模式的风行,越来越多的操作系统、浏览器开始反对深色模式,当初能够利用 CSS 的媒体查询方法(prefers-color-scheme)以及 CSS 变量(CSS variables、CSS custom properties)就能够实现页面主题追随零碎主动切换深浅模式。CSS 变量除了 IE,其余各大浏览器都反对的比拟好,但 prefers-color-scheme 办法还处于 W3C 草案标准,须要对不兼容浏览器做向下兼容,具体浏览器兼容性能够查问 Can I Use,综合来说,高版本的支流浏览器都曾经反对,IE 不反对。

能够通过以下两种形式来实现 Web 端的深色适配:

一、CSS 的媒体查问

prefers-color-scheme 是一种 用于检测用户是否有将零碎的主题色设置为亮色或者暗色 的 CSS 媒体个性。利用其设置不同主题模式下的 CSS 款式,浏览器会主动依据以后零碎主题加载对应的 CSS 款式。light 适配浅色主题,dark 适配深色主题,no-preference 示意获取不到主题时的适配计划。

  • CSS
@media (prefers-color-scheme: light) { 
  .article {  
    background:#fff; 
    color: #000;  
  } 
} 
@media (prefers-color-scheme: dark) { 
  .article {  
    background:#000;  
    color: white;  
  } 
} 
@media (prefers-color-scheme: no-preference) { 
  .article {  
    background:#fff; 
    color: #000;  
  } 
} 
  • Link 标签
<link href="./common.css" rel="stylesheet" type="text/css" /> 
<link href="./light-mode-theme.css" rel="stylesheet" type="text/css" /> 
<link href="./dark-mode-theme.css" rel="stylesheet" type="text/css" media="(prefers-color-scheme: dark)" /> 

来看一下成果,将零碎设置为浅色外观:

而后将零碎设置为深色外观:

页面曾经加载了对应深色主题的款式:

二、CSS 变量 + 媒体查问

window.matchMedia 办法能够用来查问 指定的媒体查问字符串解析后的后果。联合 CSS 变量和 matchMedia 的查问后果,设置对应的 CSS 主题色彩。该办法更灵便,能够独自抽离主题色进行适配。

CSS 变量的作用域与 CSS 的 ” 层叠 ” 规定统一,优先级最高的申明失效。所以当 body 上存在 “dark” 类名时,:root .dark 会失效,否则 :root 失效。

.article {color: var(--text-color, #eee); 
  background: var(--text-background, #fff); 
} 
:root { 
  --text-color: #000; 
  --text-background: #fff; 
} 
:root .dark { 
  --text-color: #fff; 
  --text-background: #000; 
} 

应用 matchMedia 匹配主题媒体,深色模式匹配 (prefers-color-scheme: dark),浅色模式匹配 (prefers-color-scheme: light)

监听主题模式,深色模式时为 body 增加类名 dark,依据 CSS 变量的响应式布局特点,主动失效 dark 类名下的 CSS。

const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)'); 
// 判断是否匹配深色模式 
if (darkMode && darkMode.matches) {document.body.classList.add('dark'); 
} 
// 监听主题切换事件 
darkMode && darkMode.addEventListener('change', e => {if (e.matches) {document.body.classList.add('dark'); 
  } else {document.body.classList.remove('dark');  
  } 
});  

那么,针对不反对 CSS 变量的 IE 浏览器怎么办呢?不做兼容性解决的话那页面可能就是一团糟了。所以咱们须要针对不兼容的浏览器做一些兜底解决,这里咱们能够在 webpack 等构建工具中借助 post-css 的 postcss-css-variables 插件来主动解析 CSS 变量对应的色值,并在原始 CSS 定义之上增加一条新的 CSS 款式,做到对不反对 CSS 变量 浏览器 的兼容。

用法如下:

// 根目录 postcss.config.js 
module.exports = { 
  plugins: { 
    "postcss-css-variables": {preserve: true, // 保留 var() 定义 
      preserveInjectedVariables: false, // 去除其余模块的反复变量 
      variables: require("./page.json"), // CSS 变量,能够反对多个 
    } 
  } 
}; 

我的项目实际

当初的 Web、App 我的项目大都援用第三方开源组件库,组件库个别应用会 Sass、Less 等 CSS 预处理器 定义色彩变量作为组件的根底色值,并独自抽离为配置文件。所以,我的项目应用组件库时能够依据批改根底色值来自定义主题。那么针对我的项目的 深色模式适配计划 也一样,次要分为三步:一、组件库深浅色主题 适配;二是、我的项目中深浅色的色彩适配;三、实现 CSS 变量到页面的 注入。

组件库款式、自定义款式适配

如果第三方组件自身反对多主题或者深色模式,能够间接按阐明给组件设置对应主题模式;如果第三方组件库不反对的话,只能用笼罩的形式。这里以 Less 为例进行简略实例阐明:

批改前:

// index.less 
@white: #fff; // 色彩预约义 
@background-color: @white; // 组件款式 panel.less 
.panel-background-color {background-color: @background-color; // 组件中应用 less 变量定义色彩款式} 

新增两个 js 或者 JSON 文件,别离定义深浅模式下的 CSS 变量,并命名为 light-theme1.js、dark-theme2.js 他们并不会影响组件的款式,只是便于前期注入到全局 style 中。

批改后:

// 浅色主题文件 light-theme1.js 
const bgColor = '#fff';// 色彩预约义 
module.exports = {"--background-color": bgColor;} 
// 深色主题文件 dark-theme1.js 
const bgColor = '#000';// 色彩预约义 
module.exports = {"--background-color": bgColor;} 
// 组件款式 panel.less 
.panel-background-color {background-color: var(--background-color); // 组件中色彩款式 
} 

CSS 变量反对第二参数,当变量不存在或者未注册胜利时,能够为其设置默认值,优化如下:

// 组件款式 panel.less 
.panel-background-color {background-color: var(--background-color, @background-color); // 组件中色彩款式,其中 @background-color 代表批改前组件的背景色彩变量,这里设其为默认值,在适配不胜利状况下,能够放弃适配前的款式。} 

我的项目才是真正应用组件的中央,并且我的项目自身也有很多 自定义 CSS 的 色彩款式,须要做与组件库相似的解决,后果也会失去两个 js / json 文件,别离命名为 light-theme2.js、dark-theme2.js。

CSS 注入

在页面渲染前,须要把定义深浅 款式的 CSS 变量注入到页面。

以上两步失去了四个文件,合并浅色款式文件 light-theme1.js 和 light-theme2.js 失去 light-theme.js,合并深色款式文件 dark-theme1.js 和 dark-theme2.js 失去 dark-theme.js,最初 把 light-theme.js、dark-theme.js 两个文件注入到页面中,注入脚本如下:

import lightTheme from './light-theme'; 
import darkTheme from './dark-theme'; 
// 创立一个 style 元素,用于插入 css 定义 
const createStyle = (content) => {const style = document.createElement('style');  
  style.type = 'text/css'; 
  style.innerHTML = content;  
  document.getElementsByTagName("script")[0].parentNode.appendChild(style); 
// 在 body 标签中定义 css 变量 
const createCssStyle = () => {const lightThemeStr = Object.keys(lightTheme).map(key => key + ':' +                         lightTheme[key]).join(';'); 
  const darkThemeStr = Object.keys(darkTheme).map(key => key + ':' + darkTheme[key]).join(';'); 
  const lightContent = `body{${lightThemeStr}}`; // 浅色模式 CSS 变量定义 
  const darkContent = `body.dark{${darkThemeStr}}`; // 深色模式 CSS 变量定义 
  createStyle(lightContent); 
  createStyle(darkContent); 
  isDarkSchemePreference();}; 

注入实现后,我的项目页面中就有了 css 变量定义,包含浅色模式 CSS 变量定义和深色模式 CSS 变量定义,具体哪一个失效,就能够依据下面提到的两种适配计划 给 body 增加 class 来管制。默认时浅色模式失效,增加 dark 类名时,深色模式会失效。至此就实现了一套残缺的深色模式适配计划。

native 深色适配

iOS

在 iOS 零碎中,开发者从色彩和图片两个方面来进行适配,咱们不须要关怀切换模式后该怎么操作,因为这些都由零碎帮咱们实现。色彩的适配,须要应用零碎提供的 API,在回调用中不同的模式下别离设置色彩,而图片的适配,须要在 XCode 的 工具栏中 Appearances 下抉择 Any,Dark,在同一名称资源的配置下别离增加图片资源。当切换深色模式时,零碎会依据适配的色彩和图片资源进行查找和主动切换对应模式下的色彩和资源文件。

Android

安卓在 Android 10(API 级别 29)及更高版本中提供深色主题背景,能够通过以下三种办法启用深色主题背景:

  • 应用零碎设置(Settings -> Display -> Theme)启用深色主题背景
  • 应用 ” 快捷设置 ” 图块,从告诉托盘中切换主题背景(启用后)
  • 在 Pixel 设施上,抉择 ” 省电模式 ” 将同时启用深色主题背景,其余原始设施制造商 (OEM) 不肯定反对这种行为

在利用中反对深色主题背景

如要反对深色主题背景,必须将利用的主题背景(通常可在 res/values/styles.xml 中找到)设置为继承 DayNight 主题背景:

<style name="AppTheme" parent="Theme.AppCompat.DayNight"> 

还能够应用 MaterialComponent 的深色主题背景:

<style name="AppTheme" parent="Theme.MaterialComponents.DayNight"> 

这会将利用的次要主题背景与系统控制的夜间模式标记相关联,并将利用的默认主题背景设置为深色主题背景(如果已启用)。

主题背景和款式

主题背景和款式应防止应用旨在于浅色主题背景下应用的硬编码色彩或图标,您应改用主题背景属性(首选)或适宜在夜间应用的资源,以下是须要理解的两个最重要的主题背景属性:

  • ?android:attr/textColorPrimary 这是一种通用型文本色彩,它在浅色主题背景下靠近于彩色,在深色主题背景下靠近于红色,该色彩蕴含一个停用状态。
  • ?attr/colorControlNormal 一种通用图标色彩,该色彩蕴含一个停用状态。

Flutter

这里以 Flutter 为例,简略介绍下跨平台开发框架如何适配深色模式。Flutter 定义主题有两种形式:全局主题或应用 Theme 来定义应用程序部分的色彩和字体款式。

全局主题

全局主题就是有应用程序根 MaterialAPP 创立 的 Theme。为了在整个应用程序中共享蕴含色彩和字体款式的主题,咱们能够提供 ThemeData 给 Material 的构造函数。theme 指定的是浅色模式,darkTheme 指定的是深色模式,程序会依据零碎设定的暗黑模式主动匹配模式。

new MaterialApp( 
  title: title, 
  theme: new ThemeData( 
     brightness: Brightness.light, 
     primaryColor: Colors.lightBlue[800], 
     accentColor: Colors.cyan[600] , 
  ), 
  darkTheme: new ThemeData( 
     brightness: Brightness.dark, 
     primaryColor: Colors.lightGreen[800] , 
     accentColor: Colors.cyan[200], 
  ), 
); 

部分主题

如果咱们想在应用程序的一部分中笼罩应用程序的全局的主题,咱们能够将要笼罩得局部封装在一个 Theme 的 Widget 中,有 2 种办法可解决:创立特有的 ThemeData 或扩大父主题。

创立特有的 ThemeData

如果咱们不想继承任何应用程序的色彩或字体款式,咱们能够通过 new ThemeData() 创立一个实例并将其传递给 Theme Widget。

// Create a unique theme with "new ThemeData" 
new Theme( 
  data: new ThemeData(accentColor: Colors.yellow,), 
  child: new FloatingActionButton(onPressed: () {}, 
    child: new Icon(Icons.add), 
  ), 
); 

扩大父主题

扩大父主题时无需笼罩所有的主题属性,咱们能够通过应用 copyWith 办法来实现。

// Find and Extend the parent theme using "copyWith". Please see the next section for more info on `Theme.of`. 
new Theme(data: Theme.of(context).copyWith(accentColor: Colors.yellow), 
  child: new FloatingActionButton( 
    onPressed: null, 
    child: new Icon(Icons.add), 
  ), 
); 

应用主题

咱们能够在 Widget 的 build 办法中通过 Theme.of(context) 函数应用自定义的主题。

new Container(color: Theme.of(context).accentColor, 
  child: new Text( 
    'Text with a background color', 
    style: Theme.of(context).textTheme.title, 
  ), 
); 

渲染成果 如下:

总结

以上别离介绍了在 App 利用中对 H5 页面和客户端的深色模式适配计划,当然其中 H5 的计划页同样适应于 PC 端。应用前肯定要确保你的零碎和浏览器是兼容深色模式的,不然就没有成果了呢。本篇只简略介绍了几种计划,欢送有更好想法的小伙伴一起探讨~

参考资料

  • https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
  • https://juejin.im/post/5eca7cbf518825430c3ab223
  • https://developer.android.com/guide/topics/ui/look-and-feel/darktheme
  • https://flutterchina.club/cookbook/design/themes

招贤纳士

政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。

如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com

退出移动版