天天看點

深色模式适配指南

背景

随着 iOS 13 的釋出,深色模式(Dark Mode)越來越多地出現在大衆的視野中,支援深色模式已經成為現代移動應用和網站的一個潮流,前段時間更是因為微信的适配再度引起熱議。深色模式不僅可以大幅減少電量的消耗,減弱強光對比,還能提供更好的可視性和沉浸感。

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

系統相容

想要實作深色模式的效果,前提條件是要系統支援,目前常見系統支援情況如下:

深色模式适配指南

H5 深色适配

随着深色模式的流行,越來越多的作業系統、浏覽器開始支援深色模式,現在可以利用 CSS 的媒體查詢方法:prefers-color-scheme (https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/prefers-color-scheme) 以及 CSS 變量 (https://developer.mozilla.org/zh-CN/docs/Web/CSS/Using_CSS_custom_properties)(CSS variables、CSS custom properties)就可以實作頁面主題跟随系統自動切換深淺模式。CSS 變量除了 IE,其餘各大浏覽器都支援的比較好,但 prefers-color-scheme 方法還處于 W3C 草案規範,需要對不相容浏覽器做向下相容,具體浏覽器相容性可以查詢 Can I Use (https://caniuse.com/#search=prefers-color-scheme),綜合來說,高版本的主流浏覽器都已經支援,IE 不支援。

可以通過以下兩種方式來實作 Web 端的深色适配:

一、CSS 的媒體查詢

prefers-color-scheme (https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/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 (https://developer.mozilla.org/zh-CN/docs/Web/API/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 (https://www.npmjs.com/package/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-theme1.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 (https://material.io/develop/android/theming/dark) 的深色主題背景:

<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/