在iOS 13 和 android 10 之后的系统上添加了新特性,暗黑模式(darkMode) 可以实现随着系统主题模式的切换而进行app跟随的功能,色调可以在深色和浅色之间相互切换,暗黑模式自从发布以来,便受到广大用户的喜爱和支持。所以越来越多的app都已经接入了暗黑模式,同时苹果也要求app开发者对自己的app进行暗黑模式的适配工作,下面就介绍一下flutter项目在暗黑模式下的适配工作。
接入:
Flutter在MaterialApp
中提供了theme
和darkTheme
两个参数配置让我们设置两种模式下的主题颜色和文字样式,配置在App的入口处,所以涵盖了Material Widget
中的颜色和文字样式(前提是子widget
使用了Material
提供的theme
和darkTheme
)
具体的入口代码:
MaterialApp(
title: 'Flutter Demo',
themeMode: ThemeMode.system,
darkTheme: ThemeData(
primarySwatch: Colors.red,
),
theme: ThemeData(
primarySwatch: Colors.blue,
),
highContrastTheme: ThemeData(),
highContrastDarkTheme: ThemeData(),
home: MyHomePage(title: 'Flutter Demo Home Page'),
)
themeMode
主体模式分为三种:
- 跟随系统
- 浅色模式
- 深色模式
enum ThemeMode {
/// Use either the light or dark theme based on what the user has selected in
/// the system settings.
system,
/// Always use the light mode regardless of system preference.
light,
/// Always use the dark mode (if available) regardless of system preference.
dark,
}
默认是使用系统的模式:
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
themeData
themeData的参数过多,我们这里就列举几个主要的
ThemeData({
Brightness brightness, //深色还是浅色
MaterialColor primarySwatch, //主题颜色样本
Color primaryColor, //主色,决定导航栏颜色
Color accentColor, //次级色,决定大多数Widget的颜色,如进度条、开关等。
Color cardColor, //卡片颜色
Color dividerColor, //分割线颜色
ButtonThemeData buttonTheme, //按钮主题
Color cursorColor, //输入框光标颜色
Color dialogBackgroundColor,//对话框背景颜色
String fontFamily, //文字字体
TextTheme textTheme,// 字体主题,包括标题、body等文字样式
IconThemeData iconTheme, // Icon的默认样式
TargetPlatform platform, //指定平台,应用特定平台控件风格
...
})
** themeData**
themeData的模式值,在赋值的时候是需要判断isdark
来判断的
assert(colorScheme?.brightness == null || brightness == null || colorScheme.brightness == brightness);
final Brightness _brightness = brightness ?? colorScheme?.brightness ?? Brightness.light;
final bool isDark = _brightness == Brightness.dark;
visualDensity ??= const VisualDensity();
primarySwatch ??= Colors.blue;
primaryColor ??= isDark ? Colors.grey[900] : primarySwatch;
...
同时提供了工厂方法:
/// A default light blue theme.
///
/// This theme does not contain text geometry. Instead, it is expected that
/// this theme is localized using text geometry using [ThemeData.localize].
factory ThemeData.light() => ThemeData(brightness: Brightness.light);
/// A default dark theme with a teal secondary [ColorScheme] color.
///
/// This theme does not contain text geometry. Instead, it is expected that
/// this theme is localized using text geometry using [ThemeData.localize].
factory ThemeData.dark() => ThemeData(brightness: Brightness.dark);
theme的使用策略
theme或者darkTheme数据是在顶层
使用全局的主题
在Material 中配置theme和darkTheme的themeData,后续widget使用themeData的数据来设置
new MaterialApp(
title: title,
theme: ThemeData(
primaryColor: Colors.red,
//...
),
);
使用局部的主题
如有一些局部widget
需要特殊处理来单独配置theme
,则为该widget
创建局部theme
单独适配:
new Theme(
data: ThemeData(
accentColor: Colors.yellow,
//...
),
child: Text('Hello World'),
);
如果只是想修改主题中的部分样式,可以使用copyWith
的方法来继承:
new Theme(
data: Theme.of(context).copyWith(accentColor: Colors.yellow),
child: Text('copyWith method'),
);
Material App
在创建的时候主题选择的逻辑:
Widget _materialBuilder(BuildContext context, Widget? child) {
// Resolve which theme to use based on brightness and high contrast.
final ThemeMode mode = widget.themeMode ?? ThemeMode.system;
final Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);
final bool useDarkTheme = mode == ThemeMode.dark
|| (mode == ThemeMode.system && platformBrightness == ui.Brightness.dark);
final bool highContrast = MediaQuery.highContrastOf(context);
ThemeData? theme;
if (useDarkTheme && highContrast && widget.highContrastDarkTheme != null) {
theme = widget.highContrastDarkTheme;
} else if (useDarkTheme && widget.darkTheme != null) {
theme = widget.darkTheme;
} else if (highContrast && widget.highContrastTheme != null) {
theme = widget.highContrastTheme;
}
theme ??= widget.theme ?? ThemeData.light();
主题的获取
我们可以在Widget
的build
方法中通过Theme.of(context)
函数使用它,因为theme的数据数据在InheritedTheme
中,查询其父类是InheritedWidget
.
static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
final _InheritedTheme inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
if (shadowThemeOnly) {
if (inheritedTheme == null || inheritedTheme.theme.isMaterialAppTheme)
return null;
return inheritedTheme.theme.data;
}
// _InheritedTheme
class _InheritedTheme extends InheritedTheme
// InheritedTheme
abstract class InheritedTheme extends InheritedWidget
Theme.of(context)
将查找Widget
树并返回树中最近的Theme
。如果我们的Widget
之上有一个单独的Theme
定义,则返回该值。如果不是,则返回App主题。
举例:FloatingActionButton使用theme:
final ThemeData theme = Theme.of(context);
final FloatingActionButtonThemeData floatingActionButtonTheme = theme.floatingActionButtonTheme;
// Applications should no longer use accentIconTheme's color to configure
// the foreground color of floating action buttons. For more information, see
// https://flutter.dev/go/remove-fab-accent-theme-dependency.
if (this.foregroundColor == null && floatingActionButtonTheme.foregroundColor == null) {
final bool accentIsDark = theme.accentColorBrightness == Brightness.dark;
final Color defaultAccentIconThemeColor = accentIsDark ? Colors.white : Colors.black;
final Color foregroundColor = this.foregroundColor
?? floatingActionButtonTheme.foregroundColor
?? theme.colorScheme.onSecondary;
final Color backgroundColor = this.backgroundColor
?? floatingActionButtonTheme.backgroundColor
?? theme.colorScheme.secondary;
final Color focusColor = this.focusColor
?? floatingActionButtonTheme.focusColor
?? theme.focusColor;
.......
在项目使用
项目中使用,默认是创建了亮色主题:
// init
themeState: ThemeState.initial(),
// ThemeState
factory ThemeState.initial() {
return ThemeState(
themeType: ThemeType.light,
);
}
// 有dark主题,但是一直没有使用
static final Map<ThemeType, CustomTheme> _themes = {
ThemeType.light: _buildBlueTheme(),
ThemeType.dark: _buildDarkTheme(),
};
// 部分颜色提供了 浅色和深色的区别。
static bool get isLight => _theme.isLight;
static ButtonStyle get bsOutlineAuto => isLight ? bsOutline : bsOutlineDark;
// 但是适配的颜色较少,且大量颜色没有做深浅色适配。
static Color get black => Colors.black.withOpacity(a1);
static Color get black2 => Colors.black.withOpacity(a2);
项目前期考虑了深浅色适配的问题,后续开发过程用因为没有适配的需求,导致现在要花大量时间来做适配的问题,任重而道远。