Flutter 深色模式分析與實踐

深色模式(Dark Mode),也被稱爲暗黑模式,是一種高對比度,或者反色模式的顯示模式,開啓以後在夜間能夠緩解疲勞,更易於閱讀,同時也能在必定程度上達到省電的效果。iOS和安卓分別從 iOS 13 和 Android 10(不一樣廠商不盡相同,部分 Android 9 也支持) 開始加入深色模式的支持,各大瀏覽器紛紛開始支持深色模式,強如微信也終於在 iOS 客戶端 7.0.十二、Android 客戶端 7.0.13 支持了深色模式,等網頁端適配深色模式後將更進一步提升用戶體驗的一致性。html

最近在業餘時間開發本身的 App,起初並開始考慮深色模式的適配,到晚上的時候,界面慘不忍睹。雖然能夠手動在系統設置裏配置外觀,可是全局修改也會影響其餘 App(很討厭修改了本身而影響了別人,比較傾向自完備性)。ios

對我來講,適配深色模式是勢在必行的:git

  • 我的很喜歡深色模式, 獨立作一款符合本身品味的 App 也是一大幸事。
  • 也不知道哪天 Apple 會硬性要求適配深色模式。現在硬件的性能愈來愈強大,內存也愈來愈大,人們對色彩的感知也愈來愈強烈。 App 除了能解決用戶的痛點以外,交互、色彩也變得愈來愈重要。
  • 寫過不少 App,但對主題這塊都沒涉及過,能夠借這個契機學習一波。

需求

用戶能夠主動設置深色模式、淺色模式、跟隨系統github

要實現這個需求,能夠先問幾個問題:web

  • 如何設置主題
  • 如何去切換主題
  • 如何保存切換的狀態

分析

咱們一塊兒逐個攻破上面的問題。canvas

如何設置主題

Flutter 提供了 Theme 組件,它能夠設置 Widget 的主題,Theme 組件能夠爲 Material App 定義主題數據(ThemeData)。Material 組件庫裏不少組件都使用了主題數據,如導航欄顏色、標題字體、Icon樣式等。Theme 內會使用 InheritedWidget 來爲其子樹共享樣式數據。它有兩種:數組

  • 全局 Theme
  • 局部 Theme

全局 Theme 是由應用程序根 MaterialAppTheme瀏覽器

/// 全局主題在MaterialApp的theme屬性
/// 全局生效 MaterialApp(  title: 'demo',  theme: ThemeData( // 這裏就是參數  brightness: Brightness.dark,  primaryColor: Colors.lightBlue[800],  accentColor: Colors.cyan[600],  ), ); 複製代碼

局部 Theme微信

/// 假如咱們要給 FloatingActionButton 設置主題樣式
/// 直接寫個 Theme 包裹 FloatingActionButton 組件 /// 而後設置 data,接收類型依然是 ThemeData,裏面填寫咱們的參數 /// (若是沒有設置局部主題則默認使用全局主題) Theme(  data: ThemeData(  accentColor: Colors.red,  ),  child: FloatingActionButton(  onPressed: () {},  child: Icon(Icons.add),  ), ); 複製代碼

Theme 使用舉例

擴展父主題:

擴展父主題時無需覆蓋全部的主題屬性,能夠經過使用 copyWith 方法來實現。markdown

Theme(
 data: Theme.of(context).copyWith(accentColor: Colors.yellow),  child: FloatingActionButton(  onPressed: (){},  child: new Icon(Icons.add),  ), ); 複製代碼

Theme.of(context) 將查找 Widget 樹並返回樹中最近的 Theme。若是 Widget 之上有一個單獨的 Theme 定義,則返回該值。若是沒有,則返回 App 主題。

區分平臺顯示指定主題

咱們也可使用 io 包裏的 Platform 來進行判斷。

MaterialApp(
 theme: defaultTargetPlatform == TargetPlatform.iOS  ? iOSTheme  : AndroidTheme,  title: 'Flutter Theme',  home: new MyHomePage(), ) 複製代碼

根據當前展現的模式指定顏色

經過 Theme.of(context).brightness 的來判斷如今是深色仍是淺色模式。

var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
 Text("APP",  color : isDarkTheme ? AppColors.darkPink : AppColors.textBlack, ) 複製代碼

ThemeData 解讀

上面說了這麼多主題的使用,可是當咱們真正要進行適配的時候,仍是無從下手,由於咱們不知道設置主題後到底起了哪些樣式變化,那麼 ThemeData 就是咱們的答案。

ThemeData({
 Brightness brightness, // 應用程序總體主題的亮度。 由按鈕等 Widget 使用,以肯定在不使用主色或強調色時要選擇的顏色  MaterialColor primarySwatch, // 主題顏色樣本  Color primaryColor, // 前景色(文本、按鈕等)  Brightness primaryColorBrightness, // primaryColor 的亮度  Color primaryColorLight, // primaryColor 的較亮版本  Color primaryColorDark, // primaryColor 的較暗版本  Color accentColor, // 前景色(文本、按鈕等)  Brightness accentColorBrightness, // accentColor的亮度。 用於肯定放置在突出顏色頂部的文本和圖標的顏色(例如FloatingButton上的圖標)  Color canvasColor, // MaterialType.canvas Material 的默認顏色  Color scaffoldBackgroundColor, // 做爲Scaffold基礎的Material默認顏色,典型Material應用或應用內頁面的背景顏色。  Color bottomAppBarColor, // BottomAppBar 的默認顏色  Color cardColor, // Material被用做Card時的顏色  Color dividerColor, // Dividers 和 PopupMenuDividers的顏色,也用於ListTiles中間,和DataTables 的每行中間  Color focusColor, // 焦點獲取時的顏色,例如,一些按鈕焦點、輸入框焦點。  Color hoverColor, // 點擊以後徘徊中的顏色,例如,按鈕長按,按住以後的顏色  Color highlightColor, // 用於相似墨水噴濺動畫或指示菜單被選中的高亮顏色。  Color splashColor, // 墨水噴濺的顏色。  InteractiveInkFeatureFactory splashFactory, // 定義InkWall和InkResponse生成的墨水噴濺的外觀。  Color selectedRowColor, // 選中行時的高亮顏色  Color unselectedWidgetColor, // 用於 Widget 處於非活動(但已啓用)狀態的顏色。 例如,未選中的複選框。 一般與 accentColor 造成對比。  Color disabledColor, // 用於 Widget 無效的顏色,不管任何狀態。例如禁用複選框  Color buttonColor, // Material 中 RaisedButtons 使用的默認填充色  ButtonThemeData buttonTheme, // 定義了按鈕等控件的默認配置  ToggleButtonsThemeData toggleButtonsTheme, // Flutter 1.9 全新組件 ToggleButtons 的主題  Color secondaryHeaderColor, // 有選定行時 PaginatedDataTable 標題的顏色  Color textSelectionColor, // 文本字段中選中文本的顏色,例如 TextField  Color cursorColor, // 輸入框光標顏色  Color textSelectionHandleColor, // 用於調整當前文本的哪一個部分的句柄顏色  Color backgroundColor, // 與 primaryColor 對比的顏色(例如 用做進度條的剩餘部分)  Color dialogBackgroundColor, // Dialog 元素的背景色  Color indicatorColor, // TabBar 中選項選中的指示器顏色。  Color hintColor, // 用於提示文本或佔位符文本的顏色,例如在 TextField 中。  Color errorColor, // 用於輸入驗證錯誤的顏色,例如在 TextField 中  Color toggleableActiveColor, // 用於突出顯示切換Widget(如Switch,Radio和Checkbox)的活動狀態的顏色。  String fontFamily, // 字體樣式  TextTheme textTheme, // 與卡片和畫布對比的文本顏色  TextTheme primaryTextTheme, // 一個與主色對比的文本主題  TextTheme accentTextTheme, // 與突出顏色對照的文本主題  InputDecorationTheme inputDecorationTheme, // InputDecorator,TextField 和 TextFormField 的默認 InputDecoration 值基於此主題  IconThemeData iconTheme, // 與卡片和畫布顏色造成對比的圖標主題  IconThemeData primaryIconTheme, // 一個與主色對比的圖片主題  IconThemeData accentIconTheme, // 與突出顏色對照的圖片主題  SliderThemeData sliderTheme, // 用於渲染 Slider 的顏色和形狀  TabBarTheme tabBarTheme, // TabBar 的主題樣式  TooltipThemeData tooltipTheme, // tooltip 提示的主題樣式  CardTheme cardTheme, // 卡片的主題樣式  ChipThemeData chipTheme, // 用於渲染Chip的顏色和樣式  TargetPlatform platform, // Widget 須要適配的目標類型  MaterialTapTargetSize materialTapTargetSize, // Chip 等組件的尺寸主題設置  bool applyElevationOverlayColor, // 是否應用 elevation 覆蓋顏色  PageTransitionsTheme pageTransitionsTheme, // 頁面轉場主題樣式  AppBarTheme appBarTheme, // AppBar 主題樣式  BottomAppBarTheme bottomAppBarTheme, // 底部導航主題樣式  ColorScheme colorScheme, // scheme組顏色,一組13種顏色,可用於配置大多數組件的顏色屬性  DialogTheme dialogTheme, // 對話框主題樣式  FloatingActionButtonThemeData floatingActionButtonTheme, // FloatingActionButton 的主題樣式,也就是 Scaffold 屬性的那個  Typography typography, // 用於配置 TextTheme、primaryTextTheme 和 accentTextTheme的顏色和幾何文本主題值  CupertinoThemeData cupertinoOverrideTheme, // cupertino 覆蓋的主題樣式  SnackBarThemeData snackBarTheme, // 彈出的 snackBar 的主題樣式  BottomSheetThemeData bottomSheetTheme, // 底部滑出對話框的主題樣式  PopupMenuThemeData popupMenuTheme, // 彈出菜單對話框的主題樣式  MaterialBannerThemeData bannerTheme, // Material 材質的 Banner 主題樣式  DividerThemeData dividerTheme, // Divider 組件的主題樣式,也就是那個橫向線條組件  ButtonBarThemeData buttonBarTheme, }) 複製代碼

更多完成信息,你們可參閱它的源碼註釋。

屬性非常比較多的,一般咱們用到的 5 ~ 10 個左右,若是要高度定製可能會更多點。

primarySwatch 它是主題顏色的一個 樣本色, 經過這個樣本色能夠在一些條件下生成一些其它的屬性,例如,若是沒有指定 primaryColor,而且當前主題不是深色主題,那麼 primaryColor 就會默認爲primarySwatch 指定的顏色,還有一些類似的屬性如 accentColor 、indicatorColor 等也會受primarySwatch 影響。

切換 & 保存

咱們能夠經過 shared_preferences 保存用戶設置,經過 Provider 實現狀態管理。

添加依賴

provider: ^4.0.5
flustars: ^0.2.6+1 複製代碼

實踐

定義淺色主題

// light_color.dart
import 'package:flutter/material.dart';  const MaterialColor lightColor =  MaterialColor(_lightColorPrimaryValue, <int, Color>{  50: Color(0xFFFDEAE7),  100: Color(0xFFFACBC3),  200: Color(0xFFF7A89C),  300: Color(0xFFF48574),  400: Color(0xFFF16B56),  500: Color(_lightColorPrimaryValue),  600: Color(0xFFED4A32),  700: Color(0xFFEB402B),  800: Color(0xFFE83724),  900: Color(0xFFE42717), });  const int _lightColorPrimaryValue = 0xFFEF5138;  const MaterialColor lightColorAccent =  MaterialColor(_lightColorAccentValue, <int, Color>{  100: Color(0xFFFFFFFF),  200: Color(_lightColorAccentValue),  400: Color(0xFFFFB4AF),  700: Color(0xFFFF9C96), }); const int _lightColorAccentValue = 0xFFFFE4E2; 複製代碼

定義好本身的主題色0xFFEF5138, 而後經過工具生成。工具地址: mbitson/mcg

通用深色模式 Provider Model 類

// theme_state.dart
 class ThemeState with ChangeNotifier {  /// 0:淺色模式 1:深色模式 2:跟隨系統  int _darkMode;  int get darkMode => _darkMode;   static const Map<int, String> darkModeMap = {0: '淺色模式', 1: '深色模式', 2: '跟隨系統'};   ThemeData get lightTheme =>  ThemeData(brightness: Brightness.light, primarySwatch: lightColor);  ThemeData get darkTheme => ThemeData.dark();   ThemeState() {  _init();  }   void _init() async {  await SpUtil.getInstance();  int localModel = SpUtil.getInt('kDarkMode', defValue: 2);  changeMode(localModel);  }   void changeMode(int darkMode) async {  _darkMode = darkMode;  notifyListeners();  SpUtil.putInt("kDarkMode", darkMode);  } } 複製代碼

主題選擇頁面

// theme_page.dart
class ThemePage extends StatelessWidget {  @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  elevation: 0,  title: Text('主題選擇'),  leading: GestureDetector(  onTap: () {  Navigator.of(context).pop();  },  child: Icon(Icons.arrow_back_ios),  ),  ),  body: Consumer<ThemeState>(  builder: (context, themeState, child) {  Map items = ThemeState.darkModeMap;  return ListView.builder(  itemBuilder: (context, index) {  return ListTile(  onTap: () {  themeState.changeMode(items.keys.toList()[index]);  },  title: Text(  items.values.toList()[index],  style: TextStyle(  color: index == themeState.darkMode  ? Colors.red  : Color(0xff333333)),  ),  );  },  itemCount: items.length,  );  },  ));  } } 複製代碼

在 main.dart 集成調用

void main() {  runApp(MyApp()); }  class MyApp extends StatefulWidget {  @override  _MyAppState createState() => _MyAppState(); }  class _MyAppState extends State<MyApp> {   @override  Widget build(BuildContext context) {  return MultiProvider(  providers: [  ChangeNotifierProvider(create: (ctx) => ThemeState())  ],  child: Consumer<ThemeState>(  builder: (context, themeState, child) {  if (themeState.darkMode == 2) { // 跟隨系統  return MaterialApp(  title: 'Oldbirds',  theme: themeState.lightTheme,  darkTheme: themeState.darkTheme,  onGenerateRoute: generateRoute,  initialRoute: SplashRoute,  debugShowCheckedModeBanner: false,  );  } else {  return MaterialApp(  title: 'Oldbirds',  theme: themeState.darkMode == 1 // 深色模式  ? themeState.darkTheme  : themeState.lightTheme,  onGenerateRoute: generateRoute,  initialRoute: SplashRoute,  debugShowCheckedModeBanner: false,  );  }  },  ));  } } 複製代碼

心得

上面的配置完成後,深色適配的功能完成 80% 左右,還有殘餘的,須要局部按需設置,有些固然還需按設計的色彩進行改動。

全局配置儘可能通用,須要規範專業級別的 ui 設計(由於通常會有設計規範)。

若是不得不改,那麼就是 去同存異

  • 好比指定的文字樣式與全局配置相同時,就刪除它

  • 若是文字顏色相同,可是字號不一樣。那就刪除顏色配置信息,保留字號設置

    Text(
     "僅保留不一樣",  style: Theme.of(context).textTheme.body1.copyWith(fontSize: 14.0) ) 複製代碼
  • 顏色不一樣,由於深色模式主要就是顏色變化:

    Text(
     "僅保留不一樣",  style: Theme.of(context).textTheme.body1.copyWith(color: Colors.red, fontSize: 14.0) ) 複製代碼

參考

更多文章閱讀,請搜索微信公衆號: OldBirds

相關文章
相關標籤/搜索