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