找回密码
 立即注册
首页 业界区 业界 Flutter个性化主题系统:Material Design 3的深度定制 ...

Flutter个性化主题系统:Material Design 3的深度定制

打阗渖 2025-10-1 17:38:16
Flutter个性化主题系统:Material Design 3的深度定制

本文基于BeeCount(蜜蜂记账)项目的实际开发经验,深入探讨如何构建灵活、美观的Material Design 3主题系统。
项目背景

BeeCount(蜜蜂记账)是一款开源、简洁、无广告的个人记账应用。所有财务数据完全由用户掌控,支持本地存储和可选的云端同步,确保数据绝对安全。
引言

在现代移动应用开发中,个性化体验已成为用户的基本期望。一个优秀的主题系统不仅能提升应用的视觉效果,更能让用户产生情感连接,提升使用体验。Material Design 3带来了全新的设计理念和技术实现,为Flutter开发者提供了强大的主题定制能力。
BeeCount采用了完全基于Material Design 3的主题系统,支持动态主色调整、深浅模式切换、以及丰富的个性化选项,为用户提供了极具个性的视觉体验。
Material Design 3核心特性

动态颜色系统

Material Design 3最大的亮点是动态颜色系统,它能:

  • 自适应配色:根据主色自动生成完整配色方案
  • 语义化颜色:每个颜色都有明确的语义和用途
  • 无障碍支持:自动保证颜色对比度符合无障碍标准
  • 深浅模式:完美支持明暗主题切换
全新的设计语言


  • 更大的圆角:更加柔和友好的视觉效果
  • 增强的层级:通过颜色和阴影表达信息层级
  • 动态形状:组件形状可以跟随主题动态调整
主题架构设计

核心主题类
  1. class BeeTheme {
  2.   // 预定义主色方案
  3.   static const Color honeyGold = Color(0xFFFFB000);
  4.   static const Color forestGreen = Color(0xFF4CAF50);
  5.   static const Color oceanBlue = Color(0xFF2196F3);
  6.   static const Color sunsetOrange = Color(0xFFFF5722);
  7.   static const Color lavenderPurple = Color(0xFF9C27B0);
  8.   static const Color cherryRed = Color(0xFFE91E63);
  9.   // 预设主色列表
  10.   static const List<Color> presetColors = [
  11.     honeyGold,
  12.     forestGreen,
  13.     oceanBlue,
  14.     sunsetOrange,
  15.     lavenderPurple,
  16.     cherryRed,
  17.   ];
  18.   // 生成完整主题数据
  19.   static ThemeData createTheme({
  20.     required Color primaryColor,
  21.     required Brightness brightness,
  22.     String? fontFamily,
  23.   }) {
  24.     final colorScheme = ColorScheme.fromSeed(
  25.       seedColor: primaryColor,
  26.       brightness: brightness,
  27.     );
  28.     return ThemeData(
  29.       useMaterial3: true,
  30.       colorScheme: colorScheme,
  31.       fontFamily: fontFamily,
  32.       
  33.       // 应用栏主题
  34.       appBarTheme: AppBarTheme(
  35.         centerTitle: true,
  36.         elevation: 0,
  37.         scrolledUnderElevation: 1,
  38.         backgroundColor: colorScheme.surface,
  39.         foregroundColor: colorScheme.onSurface,
  40.         titleTextStyle: TextStyle(
  41.           fontSize: 20,
  42.           fontWeight: FontWeight.w600,
  43.           color: colorScheme.onSurface,
  44.         ),
  45.       ),
  46.       // 卡片主题
  47.       cardTheme: CardTheme(
  48.         elevation: 0,
  49.         shape: RoundedRectangleBorder(
  50.           borderRadius: BorderRadius.circular(16),
  51.           side: BorderSide(
  52.             color: colorScheme.outlineVariant,
  53.             width: 1,
  54.           ),
  55.         ),
  56.       ),
  57.       // 输入框主题
  58.       inputDecorationTheme: InputDecorationTheme(
  59.         filled: true,
  60.         fillColor: colorScheme.surfaceVariant.withOpacity(0.5),
  61.         border: OutlineInputBorder(
  62.           borderRadius: BorderRadius.circular(12),
  63.           borderSide: BorderSide.none,
  64.         ),
  65.         focusedBorder: OutlineInputBorder(
  66.           borderRadius: BorderRadius.circular(12),
  67.           borderSide: BorderSide(
  68.             color: colorScheme.primary,
  69.             width: 2,
  70.           ),
  71.         ),
  72.         contentPadding: const EdgeInsets.symmetric(
  73.           horizontal: 16,
  74.           vertical: 12,
  75.         ),
  76.       ),
  77.       // 按钮主题
  78.       elevatedButtonTheme: ElevatedButtonThemeData(
  79.         style: ElevatedButton.styleFrom(
  80.           minimumSize: const Size(0, 48),
  81.           shape: RoundedRectangleBorder(
  82.             borderRadius: BorderRadius.circular(12),
  83.           ),
  84.           elevation: 0,
  85.           shadowColor: Colors.transparent,
  86.         ),
  87.       ),
  88.       filledButtonTheme: FilledButtonThemeData(
  89.         style: FilledButton.styleFrom(
  90.           minimumSize: const Size(0, 48),
  91.           shape: RoundedRectangleBorder(
  92.             borderRadius: BorderRadius.circular(12),
  93.           ),
  94.         ),
  95.       ),
  96.       // 列表瓦片主题
  97.       listTileTheme: ListTileThemeData(
  98.         shape: RoundedRectangleBorder(
  99.           borderRadius: BorderRadius.circular(12),
  100.         ),
  101.         contentPadding: const EdgeInsets.symmetric(
  102.           horizontal: 16,
  103.           vertical: 4,
  104.         ),
  105.       ),
  106.       // 底部导航栏主题
  107.       navigationBarTheme: NavigationBarThemeData(
  108.         height: 72,
  109.         labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
  110.         backgroundColor: colorScheme.surface,
  111.         indicatorColor: colorScheme.secondaryContainer,
  112.         labelTextStyle: MaterialStateProperty.resolveWith((states) {
  113.           if (states.contains(MaterialState.selected)) {
  114.             return TextStyle(
  115.               fontSize: 12,
  116.               fontWeight: FontWeight.w600,
  117.               color: colorScheme.onSecondaryContainer,
  118.             );
  119.           }
  120.           return TextStyle(
  121.             fontSize: 12,
  122.             fontWeight: FontWeight.normal,
  123.             color: colorScheme.onSurfaceVariant,
  124.           );
  125.         }),
  126.       ),
  127.       // 浮动操作按钮主题
  128.       floatingActionButtonTheme: FloatingActionButtonThemeData(
  129.         shape: RoundedRectangleBorder(
  130.           borderRadius: BorderRadius.circular(16),
  131.         ),
  132.         elevation: 3,
  133.         highlightElevation: 6,
  134.       ),
  135.     );
  136.   }
  137.   // 获取主题相关的语义颜色
  138.   static BeeColors colorsOf(BuildContext context) {
  139.     final colorScheme = Theme.of(context).colorScheme;
  140.     final brightness = Theme.of(context).brightness;
  141.    
  142.     return BeeColors(
  143.       // 收入颜色 - 使用绿色系
  144.       income: brightness == Brightness.light
  145.           ? const Color(0xFF1B5E20)  // 深绿色
  146.           : const Color(0xFF4CAF50), // 亮绿色
  147.       
  148.       // 支出颜色 - 使用红色系
  149.       expense: brightness == Brightness.light
  150.           ? const Color(0xFFD32F2F)  // 深红色
  151.           : const Color(0xFFF44336), // 亮红色
  152.       
  153.       // 转账颜色 - 使用蓝色系
  154.       transfer: colorScheme.primary,
  155.       
  156.       // 中性颜色
  157.       neutral: colorScheme.onSurfaceVariant,
  158.       
  159.       // 成功状态
  160.       success: const Color(0xFF4CAF50),
  161.       
  162.       // 警告状态
  163.       warning: const Color(0xFFFF9800),
  164.       
  165.       // 错误状态
  166.       error: colorScheme.error,
  167.       
  168.       // 信息状态
  169.       info: const Color(0xFF2196F3),
  170.     );
  171.   }
  172. }
  173. // 语义颜色定义
  174. class BeeColors {
  175.   final Color income;
  176.   final Color expense;
  177.   final Color transfer;
  178.   final Color neutral;
  179.   final Color success;
  180.   final Color warning;
  181.   final Color error;
  182.   final Color info;
  183.   const BeeColors({
  184.     required this.income,
  185.     required this.expense,
  186.     required this.transfer,
  187.     required this.neutral,
  188.     required this.success,
  189.     required this.warning,
  190.     required this.error,
  191.     required this.info,
  192.   });
  193. }
复制代码
Riverpod主题管理
  1. // 主题模式Provider
  2. final themeModeProvider = StateProvider<ThemeMode>((ref) => ThemeMode.system);
  3. // 主色Provider
  4. final primaryColorProvider = StateProvider<Color>((ref) => BeeTheme.honeyGold);
  5. // 字体Provider(可选)
  6. final fontFamilyProvider = StateProvider<String?>((ref) => null);
  7. // 主题初始化Provider - 处理持久化
  8. final primaryColorInitProvider = FutureProvider<void>((ref) async {
  9.   final prefs = await SharedPreferences.getInstance();
  10.   
  11.   // 加载保存的主色
  12.   final savedColor = prefs.getInt('primaryColor');
  13.   if (savedColor != null) {
  14.     ref.read(primaryColorProvider.notifier).state = Color(savedColor);
  15.   }
  16.   
  17.   // 加载主题模式
  18.   final savedMode = prefs.getString('themeMode');
  19.   if (savedMode != null) {
  20.     final mode = ThemeMode.values.firstWhere(
  21.       (e) => e.name == savedMode,
  22.       orElse: () => ThemeMode.system,
  23.     );
  24.     ref.read(themeModeProvider.notifier).state = mode;
  25.   }
  26.   // 监听变化并持久化
  27.   ref.listen<Color>(primaryColorProvider, (prev, next) async {
  28.     final colorValue = next.value;
  29.     await prefs.setInt('primaryColor', colorValue);
  30.   });
  31.   ref.listen<ThemeMode>(themeModeProvider, (prev, next) async {
  32.     await prefs.setString('themeMode', next.name);
  33.   });
  34. });
  35. // 计算主题数据的Provider
  36. final lightThemeProvider = Provider<ThemeData>((ref) {
  37.   final primaryColor = ref.watch(primaryColorProvider);
  38.   final fontFamily = ref.watch(fontFamilyProvider);
  39.   
  40.   return BeeTheme.createTheme(
  41.     primaryColor: primaryColor,
  42.     brightness: Brightness.light,
  43.     fontFamily: fontFamily,
  44.   );
  45. });
  46. final darkThemeProvider = Provider<ThemeData>((ref) {
  47.   final primaryColor = ref.watch(primaryColorProvider);
  48.   final fontFamily = ref.watch(fontFamilyProvider);
  49.   
  50.   return BeeTheme.createTheme(
  51.     primaryColor: primaryColor,
  52.     brightness: Brightness.dark,
  53.     fontFamily: fontFamily,
  54.   );
  55. });
  56. // 当前主题颜色Provider
  57. final currentBeeColorsProvider = Provider<BeeColors>((ref) {
  58.   // 这个Provider需要在Widget中使用,因为需要BuildContext
  59.   throw UnimplementedError('Use BeeTheme.colorsOf(context) instead');
  60. });
复制代码
主题选择器实现

颜色选择器组件
  1. class ColorPickerSheet extends ConsumerWidget {
  2.   const ColorPickerSheet({Key? key}) : super(key: key);
  3.   @override
  4.   Widget build(BuildContext context, WidgetRef ref) {
  5.     final currentColor = ref.watch(primaryColorProvider);
  6.    
  7.     return DraggableScrollableSheet(
  8.       initialChildSize: 0.6,
  9.       minChildSize: 0.4,
  10.       maxChildSize: 0.8,
  11.       builder: (context, scrollController) {
  12.         return Container(
  13.           decoration: BoxDecoration(
  14.             color: Theme.of(context).scaffoldBackgroundColor,
  15.             borderRadius: const BorderRadius.vertical(
  16.               top: Radius.circular(20),
  17.             ),
  18.           ),
  19.           child: Column(
  20.             children: [
  21.               // 拖拽指示器
  22.               Container(
  23.                 width: 40,
  24.                 height: 4,
  25.                 margin: const EdgeInsets.symmetric(vertical: 12),
  26.                 decoration: BoxDecoration(
  27.                   color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.4),
  28.                   borderRadius: BorderRadius.circular(2),
  29.                 ),
  30.               ),
  31.               
  32.               // 标题
  33.               Padding(
  34.                 padding: const EdgeInsets.symmetric(horizontal: 24),
  35.                 child: Row(
  36.                   mainAxisAlignment: MainAxisAlignment.spaceBetween,
  37.                   children: [
  38.                     Text(
  39.                       '选择主题色',
  40.                       style: Theme.of(context).textTheme.headlineSmall,
  41.                     ),
  42.                     TextButton(
  43.                       onPressed: () => Navigator.pop(context),
  44.                       child: const Text('完成'),
  45.                     ),
  46.                   ],
  47.                 ),
  48.               ),
  49.               
  50.               const Divider(height: 1),
  51.               
  52.               // 颜色网格
  53.               Expanded(
  54.                 child: SingleChildScrollView(
  55.                   controller: scrollController,
  56.                   padding: const EdgeInsets.all(24),
  57.                   child: Column(
  58.                     crossAxisAlignment: CrossAxisAlignment.start,
  59.                     children: [
  60.                       // 预设颜色
  61.                       Text(
  62.                         '预设颜色',
  63.                         style: Theme.of(context).textTheme.titleMedium,
  64.                       ),
  65.                       const SizedBox(height: 16),
  66.                       _buildPresetColors(context, ref, currentColor),
  67.                      
  68.                       const SizedBox(height: 32),
  69.                      
  70.                       // 自定义颜色
  71.                       Text(
  72.                         '自定义颜色',
  73.                         style: Theme.of(context).textTheme.titleMedium,
  74.                       ),
  75.                       const SizedBox(height: 16),
  76.                       _buildCustomColorPicker(context, ref, currentColor),
  77.                     ],
  78.                   ),
  79.                 ),
  80.               ),
  81.             ],
  82.           ),
  83.         );
  84.       },
  85.     );
  86.   }
  87.   Widget _buildPresetColors(BuildContext context, WidgetRef ref, Color currentColor) {
  88.     return GridView.builder(
  89.       shrinkWrap: true,
  90.       physics: const NeverScrollableScrollPhysics(),
  91.       gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  92.         crossAxisCount: 4,
  93.         crossAxisSpacing: 16,
  94.         mainAxisSpacing: 16,
  95.         childAspectRatio: 1,
  96.       ),
  97.       itemCount: BeeTheme.presetColors.length,
  98.       itemBuilder: (context, index) {
  99.         final color = BeeTheme.presetColors[index];
  100.         final isSelected = color.value == currentColor.value;
  101.         
  102.         return _ColorSwatch(
  103.           color: color,
  104.           isSelected: isSelected,
  105.           onTap: () {
  106.             ref.read(primaryColorProvider.notifier).state = color;
  107.             HapticFeedback.selectionClick();
  108.           },
  109.         );
  110.       },
  111.     );
  112.   }
  113.   Widget _buildCustomColorPicker(BuildContext context, WidgetRef ref, Color currentColor) {
  114.     return Container(
  115.       height: 200,
  116.       decoration: BoxDecoration(
  117.         border: Border.all(
  118.           color: Theme.of(context).colorScheme.outline.withOpacity(0.5),
  119.         ),
  120.         borderRadius: BorderRadius.circular(12),
  121.       ),
  122.       child: ColorPicker(
  123.         pickerColor: currentColor,
  124.         onColorChanged: (Color color) {
  125.           ref.read(primaryColorProvider.notifier).state = color;
  126.         },
  127.         colorPickerWidth: 300,
  128.         pickerAreaHeightPercent: 0.7,
  129.         enableAlpha: false,
  130.         displayThumbColor: true,
  131.         showLabel: false,
  132.         paletteType: PaletteType.hsl,
  133.         pickerAreaBorderRadius: BorderRadius.circular(8),
  134.       ),
  135.     );
  136.   }
  137. }
  138. class _ColorSwatch extends StatelessWidget {
  139.   final Color color;
  140.   final bool isSelected;
  141.   final VoidCallback onTap;
  142.   const _ColorSwatch({
  143.     required this.color,
  144.     required this.isSelected,
  145.     required this.onTap,
  146.   });
  147.   @override
  148.   Widget build(BuildContext context) {
  149.     return GestureDetector(
  150.       onTap: onTap,
  151.       child: AnimatedContainer(
  152.         duration: const Duration(milliseconds: 200),
  153.         decoration: BoxDecoration(
  154.           color: color,
  155.           shape: BoxShape.circle,
  156.           border: isSelected
  157.               ? Border.all(
  158.                   color: Theme.of(context).colorScheme.outline,
  159.                   width: 3,
  160.                 )
  161.               : null,
  162.           boxShadow: isSelected
  163.               ? [
  164.                   BoxShadow(
  165.                     color: color.withOpacity(0.4),
  166.                     blurRadius: 8,
  167.                     spreadRadius: 2,
  168.                   ),
  169.                 ]
  170.               : [
  171.                   BoxShadow(
  172.                     color: Colors.black.withOpacity(0.1),
  173.                     blurRadius: 4,
  174.                     offset: const Offset(0, 2),
  175.                   ),
  176.                 ],
  177.         ),
  178.         child: isSelected
  179.             ? const Icon(
  180.                 Icons.check,
  181.                 color: Colors.white,
  182.                 size: 24,
  183.               )
  184.             : null,
  185.       ),
  186.     );
  187.   }
  188. }
复制代码
主题模式切换器
  1. class ThemeModeSelector extends ConsumerWidget {
  2.   const ThemeModeSelector({Key? key}) : super(key: key);
  3.   @override
  4.   Widget build(BuildContext context, WidgetRef ref) {
  5.     final currentMode = ref.watch(themeModeProvider);
  6.    
  7.     return Card(
  8.       child: Padding(
  9.         padding: const EdgeInsets.all(16),
  10.         child: Column(
  11.           crossAxisAlignment: CrossAxisAlignment.start,
  12.           children: [
  13.             Text(
  14.               '外观模式',
  15.               style: Theme.of(context).textTheme.titleMedium,
  16.             ),
  17.             const SizedBox(height: 16),
  18.             
  19.             ...ThemeMode.values.map((mode) {
  20.               return RadioListTile<ThemeMode>(
  21.                 title: Text(_getThemeModeLabel(mode)),
  22.                 subtitle: Text(_getThemeModeDescription(mode)),
  23.                 value: mode,
  24.                 groupValue: currentMode,
  25.                 onChanged: (ThemeMode? value) {
  26.                   if (value != null) {
  27.                     ref.read(themeModeProvider.notifier).state = value;
  28.                     HapticFeedback.selectionClick();
  29.                   }
  30.                 },
  31.                 contentPadding: EdgeInsets.zero,
  32.               );
  33.             }).toList(),
  34.           ],
  35.         ),
  36.       ),
  37.     );
  38.   }
  39.   String _getThemeModeLabel(ThemeMode mode) {
  40.     switch (mode) {
  41.       case ThemeMode.system:
  42.         return '跟随系统';
  43.       case ThemeMode.light:
  44.         return '浅色模式';
  45.       case ThemeMode.dark:
  46.         return '深色模式';
  47.     }
  48.   }
  49.   String _getThemeModeDescription(ThemeMode mode) {
  50.     switch (mode) {
  51.       case ThemeMode.system:
  52.         return '根据系统设置自动切换';
  53.       case ThemeMode.light:
  54.         return '始终使用浅色主题';
  55.       case ThemeMode.dark:
  56.         return '始终使用深色主题';
  57.     }
  58.   }
  59. }
复制代码
主题应用实践

在MaterialApp中应用主题
  1. class BeeCountApp extends ConsumerWidget {
  2.   const BeeCountApp({Key? key}) : super(key: key);
  3.   @override
  4.   Widget build(BuildContext context, WidgetRef ref) {
  5.     // 确保主题初始化完成
  6.     final themeInit = ref.watch(primaryColorInitProvider);
  7.    
  8.     return themeInit.when(
  9.       data: (_) => _buildApp(ref),
  10.       loading: () => _buildLoadingApp(),
  11.       error: (_, __) => _buildApp(ref), // 错误时使用默认主题
  12.     );
  13.   }
  14.   Widget _buildApp(WidgetRef ref) {
  15.     final themeMode = ref.watch(themeModeProvider);
  16.     final lightTheme = ref.watch(lightThemeProvider);
  17.     final darkTheme = ref.watch(darkThemeProvider);
  18.     return MaterialApp(
  19.       title: 'BeeCount',
  20.       debugShowCheckedModeBanner: false,
  21.       
  22.       // 主题配置
  23.       theme: lightTheme,
  24.       darkTheme: darkTheme,
  25.       themeMode: themeMode,
  26.       
  27.       // 路由配置
  28.       home: const AppScaffold(),
  29.       
  30.       // 国际化配置
  31.       localizationsDelegates: const [
  32.         GlobalMaterialLocalizations.delegate,
  33.         GlobalWidgetsLocalizations.delegate,
  34.         GlobalCupertinoLocalizations.delegate,
  35.       ],
  36.       supportedLocales: const [
  37.         Locale('zh', 'CN'),
  38.         Locale('en', 'US'),
  39.       ],
  40.     );
  41.   }
  42.   Widget _buildLoadingApp() {
  43.     return MaterialApp(
  44.       home: Scaffold(
  45.         body: Center(
  46.           child: Column(
  47.             mainAxisAlignment: MainAxisAlignment.center,
  48.             children: [
  49.               CircularProgressIndicator(),
  50.               const SizedBox(height: 16),
  51.               Text('正在加载主题...'),
  52.             ],
  53.           ),
  54.         ),
  55.       ),
  56.     );
  57.   }
  58. }
复制代码
在组件中使用语义颜色
  1. class TransactionCard extends StatelessWidget {
  2.   final Transaction transaction;
  3.   const TransactionCard({
  4.     Key? key,
  5.     required this.transaction,
  6.   }) : super(key: key);
  7.   @override
  8.   Widget build(BuildContext context) {
  9.     final colors = BeeTheme.colorsOf(context);
  10.     final theme = Theme.of(context);
  11.    
  12.     Color getTransactionColor() {
  13.       switch (transaction.type) {
  14.         case 'income':
  15.           return colors.income;
  16.         case 'expense':
  17.           return colors.expense;
  18.         case 'transfer':
  19.           return colors.transfer;
  20.         default:
  21.           return colors.neutral;
  22.       }
  23.     }
  24.     return Card(
  25.       child: ListTile(
  26.         leading: Container(
  27.           width: 48,
  28.           height: 48,
  29.           decoration: BoxDecoration(
  30.             color: getTransactionColor().withOpacity(0.1),
  31.             shape: BoxShape.circle,
  32.           ),
  33.           child: Icon(
  34.             _getTransactionIcon(),
  35.             color: getTransactionColor(),
  36.           ),
  37.         ),
  38.         
  39.         title: Text(
  40.           transaction.note ?? '无备注',
  41.           style: theme.textTheme.bodyLarge,
  42.         ),
  43.         
  44.         subtitle: Text(
  45.           DateFormat('MM月dd日 HH:mm').format(transaction.happenedAt),
  46.           style: theme.textTheme.bodyMedium?.copyWith(
  47.             color: theme.colorScheme.onSurfaceVariant,
  48.           ),
  49.         ),
  50.         
  51.         trailing: Text(
  52.           '${transaction.type == 'expense' ? '-' : '+'}${transaction.amount.toStringAsFixed(2)}',
  53.           style: theme.textTheme.titleMedium?.copyWith(
  54.             color: getTransactionColor(),
  55.             fontWeight: FontWeight.w600,
  56.           ),
  57.         ),
  58.       ),
  59.     );
  60.   }
  61.   IconData _getTransactionIcon() {
  62.     switch (transaction.type) {
  63.       case 'income':
  64.         return Icons.add;
  65.       case 'expense':
  66.         return Icons.remove;
  67.       case 'transfer':
  68.         return Icons.swap_horiz;
  69.       default:
  70.         return Icons.help_outline;
  71.     }
  72.   }
  73. }
复制代码
响应式设计适配
  1. class ResponsiveTheme {
  2.   static ThemeData adaptForScreen(
  3.     ThemeData baseTheme,
  4.     BuildContext context,
  5.   ) {
  6.     final screenSize = MediaQuery.of(context).size;
  7.     final isTablet = screenSize.shortestSide >= 600;
  8.    
  9.     if (isTablet) {
  10.       return baseTheme.copyWith(
  11.         // 平板适配
  12.         appBarTheme: baseTheme.appBarTheme.copyWith(
  13.           titleTextStyle: baseTheme.appBarTheme.titleTextStyle?.copyWith(
  14.             fontSize: 24,
  15.           ),
  16.         ),
  17.         
  18.         textTheme: baseTheme.textTheme.copyWith(
  19.           headlineLarge: baseTheme.textTheme.headlineLarge?.copyWith(
  20.             fontSize: 36,
  21.           ),
  22.           headlineMedium: baseTheme.textTheme.headlineMedium?.copyWith(
  23.             fontSize: 30,
  24.           ),
  25.           bodyLarge: baseTheme.textTheme.bodyLarge?.copyWith(
  26.             fontSize: 18,
  27.           ),
  28.         ),
  29.         
  30.         cardTheme: baseTheme.cardTheme.copyWith(
  31.           shape: RoundedRectangleBorder(
  32.             borderRadius: BorderRadius.circular(20),
  33.             side: BorderSide(
  34.               color: baseTheme.colorScheme.outlineVariant,
  35.               width: 1,
  36.             ),
  37.           ),
  38.         ),
  39.       );
  40.     }
  41.    
  42.     return baseTheme;
  43.   }
  44. }
复制代码
主题动画与过渡

颜色过渡动画
  1. class AnimatedColorTransition extends StatefulWidget {
  2.   final Widget child;
  3.   final Duration duration;
  4.   const AnimatedColorTransition({
  5.     Key? key,
  6.     required this.child,
  7.     this.duration = const Duration(milliseconds: 300),
  8.   }) : super(key: key);
  9.   @override
  10.   State createState() => _AnimatedColorTransitionState();
  11. }
  12. class _AnimatedColorTransitionState extends State
  13.     with SingleTickerProviderStateMixin {
  14.   late AnimationController _controller;
  15.   late Animation<double> _animation;
  16.   @override
  17.   void initState() {
  18.     super.initState();
  19.     _controller = AnimationController(
  20.       duration: widget.duration,
  21.       vsync: this,
  22.     );
  23.     _animation = CurvedAnimation(
  24.       parent: _controller,
  25.       curve: Curves.easeInOut,
  26.     );
  27.   }
  28.   @override
  29.   void didChangeDependencies() {
  30.     super.didChangeDependencies();
  31.     _controller.forward();
  32.   }
  33.   @override
  34.   Widget build(BuildContext context) {
  35.     return FadeTransition(
  36.       opacity: _animation,
  37.       child: widget.child,
  38.     );
  39.   }
  40.   @override
  41.   void dispose() {
  42.     _controller.dispose();
  43.     super.dispose();
  44.   }
  45. }
复制代码
主题切换动画
  1. class ThemeAnimatedSwitcher extends ConsumerWidget {
  2.   final Widget child;
  3.   const ThemeAnimatedSwitcher({
  4.     Key? key,
  5.     required this.child,
  6.   }) : super(key: key);
  7.   @override
  8.   Widget build(BuildContext context, WidgetRef ref) {
  9.     final primaryColor = ref.watch(primaryColorProvider);
  10.    
  11.     return AnimatedSwitcher(
  12.       duration: const Duration(milliseconds: 400),
  13.       transitionBuilder: (Widget child, Animation<double> animation) {
  14.         return FadeTransition(
  15.           opacity: animation,
  16.           child: child,
  17.         );
  18.       },
  19.       child: Container(
  20.         key: ValueKey(primaryColor.value),
  21.         child: child,
  22.       ),
  23.     );
  24.   }
  25. }
复制代码
主题测试与调试

主题预览工具
  1. class ThemePreviewPage extends ConsumerWidget {
  2.   const ThemePreviewPage({Key? key}) : super(key: key);
  3.   @override
  4.   Widget build(BuildContext context, WidgetRef ref) {
  5.     return Scaffold(
  6.       appBar: AppBar(
  7.         title: const Text('主题预览'),
  8.         actions: [
  9.           PopupMenuButton<Color>(
  10.             onSelected: (color) {
  11.               ref.read(primaryColorProvider.notifier).state = color;
  12.             },
  13.             itemBuilder: (context) => BeeTheme.presetColors
  14.                 .map((color) => PopupMenuItem(
  15.                       value: color,
  16.                       child: Row(
  17.                         children: [
  18.                           Container(
  19.                             width: 24,
  20.                             height: 24,
  21.                             decoration: BoxDecoration(
  22.                               color: color,
  23.                               shape: BoxShape.circle,
  24.                             ),
  25.                           ),
  26.                           const SizedBox(width: 12),
  27.                           Text('主色 ${color.value.toRadixString(16).toUpperCase()}'),
  28.                         ],
  29.                       ),
  30.                     ))
  31.                 .toList(),
  32.           ),
  33.         ],
  34.       ),
  35.       body: SingleChildScrollView(
  36.         padding: const EdgeInsets.all(16),
  37.         child: Column(
  38.           crossAxisAlignment: CrossAxisAlignment.start,
  39.           children: [
  40.             _buildColorShowcase(context),
  41.             const SizedBox(height: 24),
  42.             _buildComponentShowcase(context),
  43.           ],
  44.         ),
  45.       ),
  46.     );
  47.   }
  48.   Widget _buildColorShowcase(BuildContext context) {
  49.     final colorScheme = Theme.of(context).colorScheme;
  50.     final colors = BeeTheme.colorsOf(context);
  51.    
  52.     return Column(
  53.       crossAxisAlignment: CrossAxisAlignment.start,
  54.       children: [
  55.         Text(
  56.           '颜色方案',
  57.           style: Theme.of(context).textTheme.headlineSmall,
  58.         ),
  59.         const SizedBox(height: 16),
  60.         
  61.         Wrap(
  62.           spacing: 8,
  63.           runSpacing: 8,
  64.           children: [
  65.             _ColorChip('Primary', colorScheme.primary),
  66.             _ColorChip('Secondary', colorScheme.secondary),
  67.             _ColorChip('Surface', colorScheme.surface),
  68.             _ColorChip('Error', colorScheme.error),
  69.             _ColorChip('Income', colors.income),
  70.             _ColorChip('Expense', colors.expense),
  71.             _ColorChip('Transfer', colors.transfer),
  72.           ],
  73.         ),
  74.       ],
  75.     );
  76.   }
  77.   Widget _buildComponentShowcase(BuildContext context) {
  78.     return Column(
  79.       crossAxisAlignment: CrossAxisAlignment.start,
  80.       children: [
  81.         Text(
  82.           '组件预览',
  83.           style: Theme.of(context).textTheme.headlineSmall,
  84.         ),
  85.         const SizedBox(height: 16),
  86.         
  87.         // 按钮组
  88.         Row(
  89.           children: [
  90.             ElevatedButton(
  91.               onPressed: () {},
  92.               child: const Text('Elevated'),
  93.             ),
  94.             const SizedBox(width: 8),
  95.             FilledButton(
  96.               onPressed: () {},
  97.               child: const Text('Filled'),
  98.             ),
  99.             const SizedBox(width: 8),
  100.             OutlinedButton(
  101.               onPressed: () {},
  102.               child: const Text('Outlined'),
  103.             ),
  104.           ],
  105.         ),
  106.         
  107.         const SizedBox(height: 16),
  108.         
  109.         // 卡片
  110.         Card(
  111.           child: ListTile(
  112.             leading: CircleAvatar(
  113.               child: Icon(Icons.account_balance_wallet),
  114.             ),
  115.             title: Text('示例交易'),
  116.             subtitle: Text('12月25日 14:30'),
  117.             trailing: Text(
  118.               '-128.50',
  119.               style: TextStyle(
  120.                 color: BeeTheme.colorsOf(context).expense,
  121.                 fontWeight: FontWeight.w600,
  122.               ),
  123.             ),
  124.           ),
  125.         ),
  126.         
  127.         const SizedBox(height: 16),
  128.         
  129.         // 输入框
  130.         TextField(
  131.           decoration: InputDecoration(
  132.             labelText: '备注',
  133.             hintText: '请输入备注信息',
  134.             prefixIcon: Icon(Icons.note),
  135.           ),
  136.         ),
  137.       ],
  138.     );
  139.   }
  140. }
  141. class _ColorChip extends StatelessWidget {
  142.   final String label;
  143.   final Color color;
  144.   const _ColorChip(this.label, this.color);
  145.   @override
  146.   Widget build(BuildContext context) {
  147.     final isDark = color.computeLuminance() < 0.5;
  148.    
  149.     return Container(
  150.       padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
  151.       decoration: BoxDecoration(
  152.         color: color,
  153.         borderRadius: BorderRadius.circular(16),
  154.       ),
  155.       child: Text(
  156.         label,
  157.         style: TextStyle(
  158.           color: isDark ? Colors.white : Colors.black,
  159.           fontSize: 12,
  160.           fontWeight: FontWeight.w500,
  161.         ),
  162.       ),
  163.     );
  164.   }
  165. }
复制代码
性能优化与最佳实践

主题缓存策略
  1. class ThemeCache {
  2.   static final Map<String, ThemeData> _cache = {};
  3.   
  4.   static ThemeData getOrCreate({
  5.     required Color primaryColor,
  6.     required Brightness brightness,
  7.     String? fontFamily,
  8.   }) {
  9.     final key = '${primaryColor.value}_${brightness.name}_${fontFamily ?? 'default'}';
  10.    
  11.     if (_cache.containsKey(key)) {
  12.       return _cache[key]!;
  13.     }
  14.    
  15.     final theme = BeeTheme.createTheme(
  16.       primaryColor: primaryColor,
  17.       brightness: brightness,
  18.       fontFamily: fontFamily,
  19.     );
  20.    
  21.     _cache[key] = theme;
  22.     return theme;
  23.   }
  24.   
  25.   static void clearCache() {
  26.     _cache.clear();
  27.   }
  28. }
复制代码
主题延迟加载
  1. final themeDataProvider = FutureProvider.family<ThemeData, ThemeConfig>((ref, config) async {
  2.   // 模拟主题计算耗时(如自定义字体加载等)
  3.   await Future.delayed(const Duration(milliseconds: 100));
  4.   
  5.   return ThemeCache.getOrCreate(
  6.     primaryColor: config.primaryColor,
  7.     brightness: config.brightness,
  8.     fontFamily: config.fontFamily,
  9.   );
  10. });
  11. class ThemeConfig {
  12.   final Color primaryColor;
  13.   final Brightness brightness;
  14.   final String? fontFamily;
  15.   const ThemeConfig({
  16.     required this.primaryColor,
  17.     required this.brightness,
  18.     this.fontFamily,
  19.   });
  20.   @override
  21.   bool operator ==(Object other) =>
  22.       identical(this, other) ||
  23.       other is ThemeConfig &&
  24.           runtimeType == other.runtimeType &&
  25.           primaryColor == other.primaryColor &&
  26.           brightness == other.brightness &&
  27.           fontFamily == other.fontFamily;
  28.   @override
  29.   int get hashCode =>
  30.       primaryColor.hashCode ^ brightness.hashCode ^ fontFamily.hashCode;
  31. }
复制代码
最佳实践总结

1. 设计原则


  • 一致性:确保整个应用的视觉风格统一
  • 可访问性:遵循无障碍设计原则
  • 响应式:适配不同屏幕尺寸和方向
2. 性能考虑


  • 主题缓存:避免重复计算主题数据
  • 延迟加载:大型主题资源按需加载
  • 内存管理:及时清理不需要的主题缓存
3. 用户体验


  • 平滑过渡:主题切换使用动画过渡
  • 即时反馈:颜色选择提供实时预览
  • 持久化:记住用户的主题偏好
4. 开发体验


  • 类型安全:使用强类型的主题API
  • 代码复用:提取可复用的主题组件
  • 调试工具:提供主题预览和调试界面
实际应用效果

在BeeCount项目中,Material Design 3主题系统带来了显著的价值:

  • 用户满意度:个性化主题让用户更有归属感
  • 视觉一致性:统一的设计语言提升专业感
  • 开发效率:规范的主题系统减少了样式代码
  • 维护成本:集中的主题管理便于维护和更新
结语

Material Design 3为Flutter应用带来了全新的设计可能性。通过合理的架构设计、灵活的组件化实现和良好的用户体验设计,我们可以构建出既美观又实用的个性化主题系统。
BeeCount的实践证明,一个好的主题系统不仅能提升应用的视觉效果,更能增强用户的使用体验和情感连接。这对于任何注重用户体验的应用都具有重要价值。
关于BeeCount项目

项目特色

<ul>
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册