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

相关文章
相关标签/搜索