深色模式适配指南
背景
随着 iOS 13 的(de)發布,深色模式(Dark Mode)越來(lái)越多地(dì / de)出(chū)現在(zài)大(dà)衆的(de)視野中,支持深色模式已經成爲(wéi / wèi)現代移動應用和(hé / huò)網站的(de)一(yī / yì /yí)個(gè)潮流,前段時(shí)間更是(shì)因爲(wéi / wèi)微信的(de)适配再度引起熱議。深色模式不(bù)僅可以(yǐ)大(dà)幅減少電量的(de)消耗,減弱強光對比,還能提供更好的(de)可視性和(hé / huò)沉浸感。
那針對一(yī / yì /yí)款 App 應用(原生 + H5)怎麽進行深色模式的(de)适配呢?今天就(jiù)讓我們一(yī / yì /yí)起來(lái)探究吧!
系統兼容
想要(yào / yāo)實現深色模式的(de)效果,前提條件是(shì)要(yào / yāo)系統支持,目前常見系統支持情況如下:
H5 深色适配
随着深色模式的(de)流行,越來(lái)越多的(de)操作系統、浏覽器開始支持深色模式,現在(zài)可以(yǐ)利用 CSS 的(de)媒體查詢方法:prefers-color-scheme 以(yǐ)及 CSS 變量 (CSS variables、CSS custom properties)就(jiù)可以(yǐ)實現頁面主題跟随系統自動切換深淺模式。CSS 變量除了(le/liǎo) IE,其餘各大(dà)浏覽器都支持的(de)比較好,但 prefers-color-scheme 方法還處于(yú) W3C 草案規範,需要(yào / yāo)對不(bù)兼容浏覽器做向下兼容,具體浏覽器兼容性可以(yǐ)查詢 Can I Use ,綜合來(lái)說(shuō),高版本的(de)主流浏覽器都已經支持,IE 不(bù)支持。
可以(yǐ)通過以(yǐ)下兩種方式來(lái)實現 Web 端的(de)深色适配:
一(yī / yì /yí)、CSS 的(de)媒體查詢
prefers-color-scheme 是(shì)一(yī / yì /yí)種用于(yú)檢測用戶是(shì)否有将系統的(de)主題色設置爲(wéi / wèi)亮色或者暗色的(de) CSS 媒體特性。利用其設置不(bù)同主題模式下的(de) CSS 樣式,浏覽器會自動根據當前系統主題加載對應的(de) CSS 樣式。light 适配淺色主題,dark 适配深色主題,no-preference 表示獲取不(bù)到(dào)主題時(shí)的(de)适配方案。
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)" />
來(lái)看一(yī / yì /yí)下效果,将系統設置爲(wéi / wèi)淺色外觀:
然後将系統設置爲(wéi / wèi)深色外觀:
頁面已經加載了(le/liǎo)對應深色主題的(de)樣式:
二、CSS 變量 + 媒體查詢
window.matchMedia方法可以(yǐ)用來(lái)查詢指定的(de)媒體查詢字符串解析後的(de)結果。結合 CSS 變量和(hé / huò) matchMedia 的(de)查詢結果,設置對應的(de) CSS 主題顔色。該方法更靈活,可以(yǐ)單獨抽離主題色進行适配。
CSS 變量的(de)作用域與 CSS 的(de)"層疊"規則一(yī / yì /yí)緻,優先級最高的(de)聲明生效。所以(yǐ)當 body 上(shàng)存在(zài) "dark" 類名時(shí),: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)
。
監聽主題模式,深色模式時(shí)爲(wéi / wèi) body 添加類名 dark,根據 CSS 變量的(de)響應式布局特點,自動生效 dark 類名下的(de) CSS。
const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)'); // 判斷是(shì)否匹配深色模式 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'); } });
那麽,針對不(bù)支持 CSS 變量的(de) IE 浏覽器怎麽辦呢?不(bù)做兼容性處理的(de)話那頁面可能就(jiù)是(shì)一(yī / yì /yí)團糟了(le/liǎo)。所以(yǐ)我們需要(yào / yāo)針對不(bù)兼容的(de)浏覽器做一(yī / yì /yí)些兜底處理,這(zhè)裏我們可以(yǐ)在(zài) webpack 等構建工具中借助 post-css 的(de) postcss-css-variables插件來(lái)自動解析 CSS 變量對應的(de)色值,并在(zài)原始 CSS 定義之(zhī)上(shàng)添加一(yī / yì /yí)條新的(de) CSS 樣式,做到(dào)對不(bù)支持 CSS 變量浏覽器的(de)兼容。
用法如下:
// 根目錄 postcss.config.js module.exports = { plugins: { "postcss-css-variables": { preserve: true, // 保留 var() 定義 preserveInjectedVariables: false, // 去除其他(tā)模塊的(de)重複變量 variables: require("./page.json"), // CSS 變量,可以(yǐ)支持多個(gè) } } };
項目實踐
現在(zài)的(de) Web、App 項目大(dà)都引用第三方開源組件庫,組件庫一(yī / yì /yí)般會使用 Sass、Less 等 CSS 預處理器定義顔色變量作爲(wéi / wèi)組件的(de)基礎色值,并單獨抽離爲(wéi / wèi)配置文件。所以(yǐ),項目使用組件庫時(shí)可以(yǐ)根據修改基礎色值來(lái)自定義主題。那麽針對項目的(de)深色模式适配方案也(yě)一(yī / yì /yí)樣,主要(yào / yāo)分爲(wéi / wèi)三步:一(yī / yì /yí)、組件庫深淺色主題 适配;二、項目中深淺色的(de)顔色适配;三、 完成 CSS 變量到(dào)頁面的(de)注入。
組件庫樣式、自定義樣式适配
如果第三方組件本身支持多主題或者深色模式,可以(yǐ)直接按說(shuō)明給組件設置對應主題模式;如果第三方組件庫不(bù)支持的(de)話,隻能用覆蓋的(de)方式。這(zhè)裏以(yǐ) Less 爲(wéi / wèi)例進行簡單實例說(shuō)明:
修改前:
// index.less @white: #fff; // 顔色預定義 @background-color: @white; // 組件樣式 panel.less .panel-background-color { background-color: @background-color; // 組件中使用 less 變量定義顔色樣式 }
新增兩個(gè) js 或者 JSON 文件,分别定義深淺模式下的(de) CSS 變量,并命名爲(wéi / wèi) light-theme1.js、dark-theme1.js 他(tā)們并不(bù)會影響組件的(de)樣式,隻是(shì)便于(yú)後期注入到(dào)全局 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 變量支持第二參數,當變量不(bù)存在(zài)或者未注冊成功時(shí),可以(yǐ)爲(wéi / wèi)其設置默認值,優化如下:
// 組件樣式 panel.less .panel-background-color { background-color: var(--background-color, @background-color); // 組件中顔色樣式,其中 @background-color 代表修改前組件的(de)背景顔色變量,這(zhè)裏設其爲(wéi / wèi)默認值,在(zài)适配不(bù)成功情況下,可以(yǐ)保持适配前的(de)樣式。 }
項目才是(shì)真正使用組件的(de)地(dì / de)方,并且項目本身也(yě)有很多自定義 CSS 的(de)顔色樣式,需要(yào / yāo)做與組件庫類似的(de)處理,結果也(yě)會得到(dào)兩個(gè) js/json 文件,分别命名爲(wéi / wèi) light-theme2.js、dark-theme2.js。
CSS 注入
在(zài)頁面渲染前,需要(yào / yāo)把定義深淺樣式的(de) CSS 變量注入到(dào)頁面。
以(yǐ)上(shàng)兩步得到(dào)了(le/liǎo)四個(gè)文件,合并淺色樣式文件 light-theme1.js 和(hé / huò) light-theme2.js 得到(dào) light-theme.js,合并深色樣式文件 dark-theme1.js 和(hé / huò) dark-theme2.js 得到(dào) dark-theme.js,最後把 light-theme.js、dark-theme.js 兩個(gè)文件注入到(dào)頁面中,注入腳本如下:
import lightTheme from './light-theme'; import darkTheme from './dark-theme'; // 創建一(yī / yì /yí)個(gè) style 元素,用于(yú)插入 css 定義 const createStyle = (content) => { const style = document.createElement('style'); style.type = 'text/css'; style.innerHTML = content; document.getElementsByTagName("script")[0].parentNode.appendChild(style); // 在(zài) 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(); };
注入完成後,項目頁面中就(jiù)有了(le/liǎo) CSS 變量定義,包括淺色模式 CSS 變量定義和(hé / huò)深色模式 CSS 變量定義,具體哪一(yī / yì /yí)個(gè)生效,就(jiù)可以(yǐ)根據上(shàng)面提到(dào)的(de)兩種适配方案給 body 添加 class 來(lái)控制。默認時(shí)淺色模式生效,添加 dark
類名時(shí),深色模式會生效。至此就(jiù)實現了(le/liǎo)一(yī / yì /yí)套完整的(de)深色模式适配方案。
native 深色适配
iOS
在(zài) iOS 系統中,開發者從顔色和(hé / huò)圖片兩個(gè)方面來(lái)進行适配,我們不(bù)需要(yào / yāo)關心切換模式後該怎麽操作,因爲(wéi / wèi)這(zhè)些都由系統幫我們實現。顔色的(de)适配,需要(yào / yāo)使用系統提供的(de) API,在(zài)回調用中不(bù)同的(de)模式下分别設置顔色,而(ér)圖片的(de)适配,需要(yào / yāo)在(zài) XCode 的(de) 工具欄中 Appearances 下選擇 Any,Dark,在(zài)同一(yī / yì /yí)名稱資源的(de)配置下分别添加圖片資源。當切換深色模式時(shí),系統會根據适配的(de)顔色和(hé / huò)圖片資源進行查找和(hé / huò)自動切換對應模式下的(de)顔色和(hé / huò)資源文件。
Android
安卓在(zài) Android 10(API 級别 29)及更高版本中提供深色主題背景,可以(yǐ)通過以(yǐ)下三種方法啓用深色主題背景:
使用系統設置(Settings -> Display -> Theme)啓用深色主題背景
使用"快捷設置"圖塊,從通知托盤中切換主題背景(啓用後)
在(zài) Pixel 設備上(shàng),選擇"省電模式"将同時(shí)啓用深色主題背景,其他(tā)原始設備制造商 (OEM) 不(bù)一(yī / yì /yí)定支持這(zhè)種行爲(wéi / wèi)
在(zài)應用中支持深色主題背景
如要(yào / yāo)支持深色主題背景,必須将應用的(de)主題背景(通常可在(zài) res/values/styles.xml
中找到(dào))設置爲(wéi / wèi)繼承 DayNight
主題背景:
<style name="AppTheme" parent="Theme.AppCompat.DayNight">
還可以(yǐ)使用 MaterialComponent 的(de)深色主題背景:
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight">
這(zhè)會将應用的(de)主要(yào / yāo)主題背景與系統控制的(de)夜間模式标記相關聯,并将應用的(de)默認主題背景設置爲(wéi / wèi)深色主題背景(如果已啓用)。
主題背景和(hé / huò)樣式
主題背景和(hé / huò)樣式應避免使用旨在(zài)于(yú)淺色主題背景下使用的(de)硬編碼顔色或圖标,您應改用主題背景屬性(首選)或适合在(zài)夜間使用的(de)資源,以(yǐ)下是(shì)需要(yào / yāo)了(le/liǎo)解的(de)兩個(gè)最重要(yào / yāo)的(de)主題背景屬性:
?android:attr/textColorPrimary
這(zhè)是(shì)一(yī / yì /yí)種通用型文本顔色,它在(zài)淺色主題背景下接近于(yú)黑色,在(zài)深色主題背景下接近于(yú)白色,該顔色包含一(yī / yì /yí)個(gè)停用狀态。?attr/colorControlNormal
一(yī / yì /yí)種通用圖标顔色,該顔色包含一(yī / yì /yí)個(gè)停用狀态。
Flutter
這(zhè)裏以(yǐ) Flutter 爲(wéi / wèi)例,簡單介紹下跨平台開發框架如何适配深色模式。Flutter 定義主題有兩種方式:全局主題或使用 Theme 來(lái)定義應用程序局部的(de)顔色和(hé / huò)字體樣式。
全局主題
全局主題就(jiù)是(shì)由應用程序根 MaterialAPP 創建的(de) Theme。爲(wéi / wèi)了(le/liǎo)在(zài)整個(gè)應用程序中共享包含顔色和(hé / huò)字體樣式的(de)主題,我們可以(yǐ)提供 ThemeData 給 Material 的(de)構造函數。Theme 指定的(de)是(shì)淺色模式,darkTheme 指定的(de)是(shì)深色模式,程序會根據系統設定的(de)暗黑模式自動匹配模式。
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], ), );
局部主題
如果我們想在(zài)應用程序的(de)一(yī / yì /yí)部分中覆蓋應用程序的(de)全局的(de)主題,我們可以(yǐ)将要(yào / yāo)覆蓋的(de)部分封裝在(zài)一(yī / yì /yí)個(gè) Theme 的(de) Widget 中,有 2 種方法可解決:創建特有的(de) ThemeData 或擴展父主題。
創建特有的(de) ThemeData
如果我們不(bù)想繼承任何應用程序的(de)顔色或字體樣式,我們可以(yǐ)通過 new ThemeData()
創建一(yī / yì /yí)個(gè)實例并将其傳遞給 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), ), );
擴展父主題
擴展父主題時(shí)無需覆蓋所有的(de)主題屬性,我們可以(yǐ)通過使用 copyWith
方法來(lái)實現。
// 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), ), );
使用主題
我們可以(yǐ)在(zài) Widget 的(de) build
方法中通過 Theme.of(context)
函數使用自定義的(de)主題。
new Container( color: Theme.of(context).accentColor, child: new Text( 'Text with a background color', style: Theme.of(context).textTheme.title, ), );
渲染效果 如下 :
總結
以(yǐ)上(shàng)分别介紹了(le/liǎo)在(zài) App 應用中對 H5 頁面和(hé / huò)客戶端的(de)深色模式适配方案,當然其中 H5 的(de)方案頁同樣适應于(yú) PC 端。使用前一(yī / yì /yí)定要(yào / yāo)确保你的(de)系統和(hé / huò)浏覽器是(shì)兼容深色模式的(de),不(bù)然就(jiù)沒有效果了(le/liǎo)呢。本篇隻簡單介紹了(le/liǎo)幾種方案,歡迎有更好想法的(de)小夥伴一(yī / yì /yí)起讨論~
原文:https://mp.weixin.qq.com/s/XVckb7sw2_YVmhd4986qng來(lái)源:政采雲前端團隊 - 微信公衆号 [ID:Zoo-Team]