深色模式適配指南

65 篇原創好文~
本文首發於政採雲前端團隊博客: 深色模式適配指南

深色模式適配指南

背景

隨着 iOS 13 的發佈,深色模式(Dark Mode)愈來愈多地出如今大衆的視野中,支持深色模式已經成爲現代移動應用和網站的一個潮流,前段時間更是由於微信的適配再度引發熱議。深色模式不只能夠大幅減小電量的消耗,減弱強光對比 ,還能 提供更好的可視性和沉浸感。 javascript

那針對 一款 App 應用(原生 + H5)怎麼進行深色模式的適配呢?今天就讓咱們一塊兒來探究吧!css

系統兼容

想要實現深色模式的效果,前提條件是要系統支持, 目前 常見系統支持狀況以下: html

H5 深色適配

隨着深色模式的流行,愈來愈多的操做系統、瀏覽器開始支持深色模式,如今能夠利用 CSS 的媒體查詢方法(prefers-color-scheme)以及 CSS 變量(CSS variables、CSS custom properties)就能夠實現頁面主題跟隨系統自動切換深淺模式 。CSS 變量除了 IE ,其他各大瀏覽器都支持的比較好, 但 prefers-color-scheme 方法還處於 W3C 草案規範,須要對不兼容瀏覽器作向下兼容,具體瀏覽器兼容性能夠查詢 Can I Use, 綜合來講,高版本的主流瀏覽器都已經支持,IE 不支持。 前端

能夠經過如下兩種方式來實現 Web 端的深色適配:java

1、CSS 的媒體查詢

prefers-color-scheme 是一種 用於檢測用戶是否有將系統的主題色設置爲亮色或者暗色 的 CSS 媒體特性 。 利用其設置不一樣主題模式下的 CSS 樣式, 瀏覽器會自動根據當前系統主題加載對應的 CSS 樣式。light 適配淺色主題,dark 適配深色主題,no-preference 表示獲取不到主題時的適配方案。android

  • 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)" />

來看一下效果,將系統設置爲淺色外觀: webpack

而後將系統設置爲深色外觀: web

頁面已經加載了對應深色主題的樣式: npm

2、CSS 變量 + 媒體查詢

window.matchMedia 方法能夠用來查詢 指定的媒體查詢字符串解析後的結果。 結合 CSS 變量和 matchMedia 的查詢結果,設置對應的 CSS 主題顏色。該方法更靈活,能夠單獨抽離主題色進行適配。 json

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 預處理器 定義顏色變量做爲組件的基礎色值,並單獨抽離爲配置文件。因此,項目使用組件庫時能夠根據修改基礎色值來自定義主題。那麼針對項目的 深色模式適配方案 也同樣,主要分爲三步:1、組件庫深淺色主題 適配 ; 二是、項目中深淺色的顏色適配 ; 3、 完成 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 端。使用前必定要確保你的系統和瀏覽器是兼容深色模式的,否則就沒有效果了呢。本篇只簡單介紹了幾種方案,歡迎有更好想法的小夥伴一塊兒討論~

參考資料

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索