字体缩放与暗色主题实现

December 16, 2025
5 min read
By devshan

Table of Contents

This is a list of all the sections in this post. Click on any of them to jump to that section.

一、背景

1.1 需求来源

不同人对字体大小的偏好不同:

  • 年轻使用时可能觉得默认字体太大
  • 老年使用上需要更大的字体以提高可读性
  • 不同设备的屏幕尺寸和分辨率差异大

1.2 设计目标

  1. 字体缩放:支持 0.8x ~ 1.5x 的字体缩放
  2. 暗色主题:提供浅色/暗色/跟随系统三种模式
  3. 持久化:设置保存到本地,重启后保持
  4. 全局生效:所有页面自动应用设置

二、字体缩放实现

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 - 添加主题和字体设置 UI
  • client/lib/core/database/database.dart - 传输任务数据库优化

八、使用体验提升

  1. 个性化定制:可以按偏好调整界面外观
  2. 无障碍支持:字体缩放帮助视力不太好的人
  3. 护眼模式:暗色主题减轻眼睛疲劳
  4. 设置持久化:重启应用保持选择

总结:字体缩放和暗色主题功能提升了应用的可用性和使用体验,满足不同人群体的需求。