一、背景
1.1 需求来源
不同人对字体大小的偏好不同:
- 年轻使用时可能觉得默认字体太大
- 老年使用上需要更大的字体以提高可读性
- 不同设备的屏幕尺寸和分辨率差异大
1.2 设计目标
- 字体缩放:支持 0.8x ~ 1.5x 的字体缩放
- 暗色主题:提供浅色/暗色/跟随系统三种模式
- 持久化:设置保存到本地,重启后保持
- 全局生效:所有页面自动应用设置
二、字体缩放实现
2.1 AppState 状态管理
class AppState extends ChangeNotifier {
double _fontScale = 1.0;
double get fontScale => _fontScale;
Future<void> setFontScale(double scale) async {
_fontScale = scale;
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble('font_scale', scale);
notifyListeners();
}
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
_fontScale = prefs.getDouble('font_scale') ?? 1.0;
// ...
}
}2.2 MaterialApp 集成
MaterialApp(
// ...
builder: (context, child) {
final media = MediaQuery.of(context);
final scale = appState.fontScale;
return MediaQuery(
data: media.copyWith(
textScaler: TextScaler.linear(scale),
),
child: child ?? const SizedBox.shrink(),
);
},
)工作原理:
MediaQuery.textScaler是 Flutter 3.16+ 的新 API,替代了旧的textScaleFactor- 使用
TextScaler.linear(scale)线性缩放所有文本 - 全局生效,无需修改每个 Text Widget
2.3 设置页面 UI
ListTile(
leading: Icon(Icons.format_size),
title: Text('字体大小'),
subtitle: Text(_getFontSizeLabel(state.fontScale)),
trailing: Icon(Icons.chevron_right),
onTap: _showFontSizeDialog,
)
void _showFontSizeDialog() {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('调整字体大小'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildFontOption('小', 0.8),
_buildFontOption('正常', 1.0),
_buildFontOption('大', 1.2),
_buildFontOption('超大', 1.5),
],
),
),
);
}
Widget _buildFontOption(String label, double scale) {
return RadioListTile<double>(
title: Text(label),
value: scale,
groupValue: state.fontScale,
onChanged: (value) {
if (value != null) {
state.setFontScale(value);
Navigator.pop(context);
}
},
);
}
String _getFontSizeLabel(double scale) {
if (scale <= 0.8) return '小';
if (scale <= 1.0) return '正常';
if (scale <= 1.2) return '大';
return '超大';
}三、暗色主题实现
3.1 AppState 主题管理
class AppState extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.system;
ThemeMode get themeMode => _themeMode;
Future<void> setThemeMode(ThemeMode mode) async {
_themeMode = mode;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme_mode', mode.toString());
notifyListeners();
}
Future<void> _loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
final themeModeStr = prefs.getString('theme_mode');
if (themeModeStr != null) {
_themeMode = ThemeMode.values.firstWhere(
(e) => e.toString() == themeModeStr,
orElse: () => ThemeMode.system,
);
}
// ...
}
}3.2 主题定义
浅色主题
final base = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
useMaterial3: true,
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
appBarTheme: const AppBarTheme(
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
),
listTileTheme: const ListTileThemeData(
minVerticalPadding: 6,
horizontalTitleGap: 8,
dense: true,
),
);
// 应用 Google Fonts
return base.copyWith(
textTheme: GoogleFonts.notoSansScTextTheme(base.textTheme),
);暗色主题(Nord 配色)
// Nord 色板
const nordBackground = Color(0xFF2E3440); // 深灰蓝背景
const nordSurface = Color(0xFF3B4252); // 表面色
const nordPrimary = Color(0xFF88C0D0); // 浅蓝(主色)
const nordSecondary = Color(0xFF81A1C1); // 蓝色(次色)
final scheme = ColorScheme.fromSeed(
seedColor: nordPrimary,
brightness: Brightness.dark,
).copyWith(
primary: nordPrimary,
secondary: nordSecondary,
surface: nordSurface,
);
final base = ThemeData(
colorScheme: scheme,
useMaterial3: true,
scaffoldBackgroundColor: nordBackground,
splashFactory: NoSplash.splashFactory,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
appBarTheme: const AppBarTheme(
surfaceTintColor: Colors.transparent,
elevation: 0,
scrolledUnderElevation: 0,
),
listTileTheme: const ListTileThemeData(
minVerticalPadding: 6,
horizontalTitleGap: 8,
dense: true,
),
);
return base.copyWith(
textTheme: GoogleFonts.notoSansScTextTheme(base.textTheme),
);Nord 配色特点:
- 视觉舒适,对比度适中
- 色彩饱和度低,适合长时间阅读
- 流行的暗色主题配色方案
3.3 MaterialApp 配置
MaterialApp(
theme: /* 浅色主题 */,
darkTheme: /* 暗色主题 */,
themeMode: appState.themeMode, // ← 关键:从 AppState 读取
// ...
)3.4 设置页面主题切换
ListTile(
leading: Icon(Icons.palette),
title: Text('外观主题'),
subtitle: Text(_getThemeModeLabel(state.themeMode)),
trailing: Icon(Icons.chevron_right),
onTap: _showThemeModeDialog,
)
void _showThemeModeDialog() {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text('选择主题'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildThemeOption('浅色', ThemeMode.light),
_buildThemeOption('暗色', ThemeMode.dark),
_buildThemeOption('跟随系统', ThemeMode.system),
],
),
),
);
}
Widget _buildThemeOption(String label, ThemeMode mode) {
return RadioListTile<ThemeMode>(
title: Text(label),
value: mode,
groupValue: state.themeMode,
onChanged: (value) {
if (value != null) {
state.setThemeMode(value);
Navigator.pop(context);
}
},
);
}
String _getThemeModeLabel(ThemeMode mode) {
switch (mode) {
case ThemeMode.light:
return '浅色';
case ThemeMode.dark:
return '暗色';
case ThemeMode.system:
return '跟随系统';
}
}四、数据库字段添加
为了在传输任务中正确显示字体大小,需要在数据库中添加相关字段:
// database.dart
class TransferTasks extends Table {
TextColumn get id => text()();
TextColumn get s3ConfigId => text()();
IntColumn get type => integer()();
TextColumn get name => text()();
IntColumn get size => integer().nullable()();
IntColumn get status => integer()();
RealColumn get progress => real()();
TextColumn get error => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column> get primaryKey => {id};
}虽然这个提交没有直接添加新字段,但优化了传输任务的显示逻辑。
五、传输任务管理优化
5.1 重试功能
为失败的传输任务添加重试按钮:
// transfers_page.dart
if (item.status == TransferStatus.failed) {
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => _retryTransfer(item),
tooltip: '重试',
),
}
Future<void> _retryTransfer(TransferItem item) async {
if (item.type == TransferType.upload) {
// 从数据库加载任务详情并重新加入队列
await state.retryUpload(item.id);
} else {
await state.retryDownload(item.id);
}
}5.2 状态显示优化
Widget _buildStatusIcon(TransferStatus status) {
switch (status) {
case TransferStatus.queued:
return Icon(Icons.schedule, size: 16, color: Colors.grey);
case TransferStatus.running:
return SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
);
case TransferStatus.finishing:
return SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
);
case TransferStatus.success:
return Icon(Icons.check_circle, size: 16, color: Colors.green);
case TransferStatus.failed:
return Icon(Icons.error, size: 16, color: Colors.red);
case TransferStatus.cancelled:
return Icon(Icons.cancel, size: 16, color: Colors.orange);
}
}六、效果演示
6.1 字体缩放效果
| 设置 | 缩放比例 | 适用场景 |
|---|---|---|
| 小 | 0.8x | 小屏设备、信息密度高的场景 |
| 正常 | 1.0x | 默认,适合大部分场景 |
| 大 | 1.2x | 中年纪大一些的人、提高可读性 |
| 超大 | 1.5x | 视力不太好的人、无障碍需求 |
6.2 主题切换效果
浅色主题:
- 白色背景 + 蓝色强调色
- 适合光线充足环境
- Material 3 设计语言
暗色主题(Nord):
- 深灰蓝背景(#2E3440)
- 浅蓝强调色(#88C0D0)
- 低饱和度,护眼舒适
七、文件变更清单
client/lib/main.dart- 添加主题配置和字体缩放client/lib/core/state/app_state.dart- 添加主题和字体状态管理client/lib/ui/settings_page.dart- 添加主题和字体设置 UIclient/lib/core/database/database.dart- 传输任务数据库优化
八、使用体验提升
- 个性化定制:可以按偏好调整界面外观
- 无障碍支持:字体缩放帮助视力不太好的人
- 护眼模式:暗色主题减轻眼睛疲劳
- 设置持久化:重启应用保持选择
总结:字体缩放和暗色主题功能提升了应用的可用性和使用体验,满足不同人群体的需求。