使用 Flutter 给 App 适配暗黑模式相对来说比较容易实现,因为 Flutter 本身已经提供了很多接口,直接使用就行了,简单搭建个 Demo 玩玩是可以的。但如果真做项目,需要多考虑一步,那就是文本样式的适配。如何在暗黑模式和浅色模式下,对文本颜色做适配,怎么做比较规范,看起来不那么乱。
我看了一些开源项目,也自己琢磨了一套目前使用的方案,用着还行,分享在这。
归纳文本样式
将整个项目设计图的文本做个浏览,一般来说,设计图使用的文本都是有一定规范,大小和颜色就那么几套,不会天马行空没点规律,随处取色。所以,首先找出使用到文本的大小有哪些,其次,将使用到的颜色,能分类的颜色提取出来,分成浅色模式和暗黑模式。现在很多设计软件,像 Figma 都有提供一键渲染暗黑模式设计图,这个步骤不难。
class ColorStyle {
// 浅色模式
static const Color primary = Colors.blue;
static const Color accent = Colors.amber;
static const Color background = Colors.white;
static const Color foreground = Colors.black;
static const Color textPrimary = Colors.black87;
static const Color textSecondary = Colors.black54;
// 暗黑模式
static Color darkPrimary = Colors.blue[700]!;
static Color darkAccent = Colors.amber[200]!;
static const Color darkBackground = Colors.black;
static const Color darkForeground = Colors.white;
static const Color darkTextPrimary = Colors.white70;
static const Color darkTextSecondary = Colors.white60;
// 共用颜色
static const Color success = Colors.green;
static const Color warning = Colors.orange;
static const Color error = Colors.red;
static const Color info = Colors.lightBlue;
}
接着定义文本样式,按照字体大小定义不同颜色的文本样式,可能存在同一个字体大小有不同的颜色。比如设计稿里 12 号字体,可能会有两种,甚至三种颜色。对这些文本,我们不能使用 TextTheme
规范定义的,而是要单独定义暗黑模式下的样式。
class AppTextTheme {
static final TextStyle title17 = TextStyle(fontSize: 17.0, fontWeight: FontWeight.w500 ,color: ColorStyle.foreground);
static final TextStyle title16 = TextStyle(fontSize: 16.0, fontWeight: FontWeight.w500, color: ColorStyle.background);
static final TextStyle body15 = TextStyle(fontSize: 15.0, fontWeight: FontWeight.w500, color: ColorStyle.foreground);
static final TextStyle body14Black = TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: ColorStyle.foreground);
static final TextStyle label13Black = TextStyle(fontSize: 13.0, color: ColorStyle.textSecondary);
static final TextStyle label13Blue = TextStyle(fontSize: 13.0, color: ColorStyle.primary);
static final TextStyle label12Black = TextStyle(fontSize: 12.0, color: ColorStyle.textSecondary);
static final TextStyle label12Blue = TextStyle(fontSize: 12.0, color: ColorStyle.primary);
static final TextStyle label11 = TextStyle(fontSize: 11.0, color: ColorStyle.textSecondary);
/// 自定义暗黑模式下的蓝色文本样式
static final TextStyle label12BlueDark = TextStyle(fontSize: 12.0, color: ColorStyle.darkPrimary);
static final TextStyle label13BlueDark = TextStyle(fontSize: 13.0, color: ColorStyle.darkPrimary);
static final TextStyle body14WhiteDark = TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: ColorStyle.background);
static TextTheme lightTextTheme = TextTheme(
titleMedium: title17,
titleSmall: title16,
bodyMedium: body15,
bodySmall: body14Black,
labelLarge: label13Black,
labelMedium: label12Black,
labelSmall: label11,
);
static TextTheme darkTextTheme = TextTheme(
titleMedium: title17.copyWith(color: ColorStyle.darkForeground),
titleSmall: title16.copyWith(color: ColorStyle.darkBackground),
bodyMedium: body15.copyWith(color: ColorStyle.darkForeground),
bodySmall: body14Black.copyWith(color: ColorStyle.darkForeground),
labelLarge: label13Black.copyWith(color: ColorStyle.darkTextSecondary),
labelMedium: label12Black.copyWith(color: ColorStyle.darkTextSecondary),
labelSmall: label11.copyWith(color: ColorStyle.darkTextSecondary),
);
}
然后就是配置由 Materia 提供的暗黑模式接口
///主题颜色
theme: AppTheme.lightThemeData,
darkTheme: AppTheme.darkThemeData,
现在给 UI 布局使用上面的文本,比如看这个布局
大部分文本都可以使用已经定义规范好的 TextTheme
。比如 titleMedium
、labelMedium
Text('欢迎使用', style: Theme
.of(context)
.textTheme
.titleMedium),
Text('Sign in to continue', style: Theme
.of(context)
.textTheme
.labelMedium),
TextField(
decoration: InputDecoration(
labelText: '你的账号',
labelStyle: Theme.of(context).textTheme.labelMedium,
prefixIcon: Icon(Icons.person),
),
),
如果碰到不能兼容使用的,则要单独设置,分别设置好浅色模式和暗黑模式使用的文本样式。
child: Text('登录', style: Theme.of(context).brightness == Brightness.dark ? AppTextTheme.body14WhiteDark : AppTextTheme.body14Black,),
适配暗黑模式
我们大概想实现一个什么样的效果呢?下面是微信的深色模式,App 的主题默认跟随系统,也就是说系统设置是暗黑模式,则 App 展示暗黑模式,如果是浅色模式,App 则浅色。也可以自己定义,就是要单独给 App 定义一个不同于系统的主题模式,这就是手动选择。
那在 Flutter MaterialApp 决定主题样式的对象是 ThemeMode
。
enum ThemeMode {
system,
light,
dark,
}
看命名大概就清楚这三个属性的含义,分别是跟随系统、浅色模式、暗黑模式。这三个属性在这里设置:
///主题颜色
theme: AppTheme.lightThemeData,
darkTheme: AppTheme.darkThemeData,
themeMode: ThemeMode.system,
默认不设置就是跟随系统 ThemeMode.system
,如果你将它修改成 ThemeMode.dark
,那就算系统设置成浅色模式,这 App 还是固定成暗黑模式。接下来看这个布局
我使用 GetX 管理状态,这里就直接贴代码里。
class DarkModePage extends GetView<ThemeController> {
const DarkModePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("深色模式"),
),
body: Obx((){
return Column(
children: [
SwitchListTile(
title: Text('跟随系统'),
value: controller.isSystemTheme.value,
onChanged: controller.toggleSystemTheme,
),
Visibility(
visible: !controller.isSystemTheme.value,
child: Column(
children: [
RadioListTile<bool>(
title: const Text('浅色模式'),
value: false,
groupValue: controller.isDarkMode.value,
onChanged: controller.toggleDarkMode,
),
RadioListTile<bool>(
title: const Text('深色模式'),
value: true,
groupValue: controller.isDarkMode.value,
onChanged: controller.toggleDarkMode,
),
],
),
)
],
);
}),
);
}
}
然后是 Controller ,这里大家情况都不一样,可以做个参考,关键代码我加了注释,还有这个 Controller 一定要最早初始化,添加到 GetX 中管理。
class ThemeController extends BaseGetController {
var isSystemTheme = true.obs; // 跟随系统主题
var isDarkMode = false.obs; // 深色模式
@override
void onInit() {
super.onInit();
}
ThemeMode get currentThemeMode {
if (isSystemTheme.value) {
return ThemeMode.system;
} else {
return isDarkMode.value ? ThemeMode.dark : ThemeMode.light;
}
}
void toggleSystemTheme(bool value) {
isSystemTheme.value = value;
if (!value) {
// 如果不跟随系统,则默认设置为浅色模式
isDarkMode.value = false;
}
updateThemeMode();
}
void toggleDarkMode(bool? value) {
if (!isSystemTheme.value) {
isDarkMode.value = value!;
updateThemeMode();
}
}
void updateThemeMode() {
Get.changeThemeMode(currentThemeMode); // 通知框架切换主题
}
@override
void onClose() {
super.onClose();
}
}
因为 MaterialApp
要使用ThemeController
里的变量,所以要先添加。
class Injection{
static Future<void> init() async{
await Get.putAsync(() => SharedPreferences.getInstance());
Get.lazyPut(() => RequestRepository());
Get.put(BaseGetController());
Get.put(ThemeController());
}
}
ThemeController themeController = Get.find<ThemeController>();
themeMode: themeController.currentThemeMode,
接下来就是对主题做缓存,将用户的主题偏好保存起来,这样下次打开 App 时可以读取之前设置的主题,不然用户切换了暗黑模式,结果下次打开还是浅色模式,这肯定不对,一般使用 shared_preferences
保存就够了。
初始化加载缓存
import 'package:shared_preferences/shared_preferences.dart';
class ThemeController extends GetxController {
var isSystemTheme = true.obs;
var isDarkMode = false.obs;
@override
void onInit() {
super.onInit();
loadThemeMode();
}
Future<void> saveThemeMode() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isSystemTheme', isSystemTheme.value);
await prefs.setBool('isDarkMode', isDarkMode.value);
}
Future<void> loadThemeMode() async {
final prefs = await SharedPreferences.getInstance();
isSystemTheme.value = prefs.getBool('isSystemTheme') ?? true;
isDarkMode.value = prefs.getBool('isDarkMode') ?? false;
}
}
更改主题时缓存
void toggleSystemTheme(bool value) {
isSystemTheme.value = value;
if (!value) {
isDarkMode.value = false;
}
saveThemeMode();
updateThemeMode();
}
void toggleDarkMode(bool? value) {
if (!isSystemTheme.value && value != null) {
isDarkMode.value = value;
saveThemeMode();
updateThemeMode();
}
}
感觉有点虎头蛇尾,但每个人情况不一样,加上这里面涉及状态管理部分内容,我这直接使用 GetX,但很多人可能使用 Provider,大家情况都不一样,我只能尽量多提供逻辑清晰的代码,关键代码加了注释,大概就这样。
本文由老郭种树原创,转载请注明:https://guozh.net/2024-flutter-adapts-to-dark-mode/