鸿蒙Flutter实战:Material 3种子色亮暗双主题系统
前言
Flutter 3.x 开始,Material 3(M3)成为默认设计语言。相比 Material 2,M3 最大的变化之一是动态配色——通过一个"种子色",自动生成整个应用的色调系统,包括主色、次要色、表面色、错误色,以及它们在不同亮度等级下的变体。
鸿蒙 Flutter 备忘录使用薄荷绿#4DB6AC作为种子色,同时支持亮色和暗色双主题,跟随系统设置自动切换。本文拆解这套主题系统的设计和实现。
项目仓库:todo_flutter_harmony
为什么是 ColorScheme.fromSeed
Material 2 时代,开发者需要手动定义 8-12 个颜色值来构建一个完整的主题:
// Material 2 —— 繁琐ThemeData(primaryColor:Color(0xFF4DB6AC),primaryColorLight:Color(0xFF80CBC4),primaryColorDark:Color(0xFF00897B),accentColor:Color(0xFFFF8A65),// ... 还要 backgroundColor, surfaceColor, errorColor 等等)Material 3 的ColorScheme.fromSeed让你只需要一个颜色:
// Material 3 —— 简洁ThemeData(colorScheme:ColorScheme.fromSeed(seedColor:constColor(0xFF4DB6AC),brightness:Brightness.light,),)fromSeed内部使用基于 HCT(Hue-Chroma-Tone)色彩空间的算法,自动生成 30+ 个色调变体。这个算法由 Google 的 Material Design 团队开发,考虑了人眼对不同色调的敏感度差异,生成的色板在任何组合下都能保持足够的对比度。
App 入口:ThemeMode 和 ThemeData
classAppextendsStatefulWidget{@overrideState<App>createState()=>_AppState();}class_AppStateextendsState<App>{@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(title:'芯捷备忘录',debugShowCheckedModeBanner:false,themeMode:ThemeMode.system,// 跟随系统设置theme:_buildLightTheme(),darkTheme:_buildDarkTheme(),home:constHomePage(),onGenerateRoute:_generateRoute,);}themeMode: ThemeMode.system:应用自动跟随系统的亮/暗模式设置theme:系统为亮色模式时的主题darkTheme:系统为暗色模式时的主题
如果想让用户手动控制(而不是跟随系统),可以换成:
themeMode:ThemeMode.light,// 始终亮色themeMode:ThemeMode.dark,// 始终暗色亮色主题
ThemeData_buildLightTheme(){finalcolorScheme=ColorScheme.fromSeed(seedColor:constColor(0xFF4DB6AC),// 薄荷绿brightness:Brightness.light,);returnThemeData(useMaterial3:true,colorScheme:colorScheme,appBarTheme:AppBarTheme(centerTitle:true,backgroundColor:colorScheme.surface,foregroundColor:colorScheme.onSurface,elevation:0,scrolledUnderElevation:1,),cardTheme:CardTheme(elevation:1.0,shadowColor:colorScheme.shadow.withOpacity(0.3),shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(16),),clipBehavior:Clip.antiAlias,),navigationBarTheme:NavigationBarThemeData(elevation:2,indicatorColor:colorScheme.primaryContainer,surfaceTintColor:colorScheme.surfaceTint,),floatingActionButtonTheme:FloatingActionButtonThemeData(backgroundColor:colorScheme.primaryContainer,foregroundColor:colorScheme.onPrimaryContainer,elevation:4,),inputDecorationTheme:InputDecorationTheme(border:OutlineInputBorder(borderRadius:BorderRadius.circular(12),),contentPadding:constEdgeInsets.symmetric(horizontal:16,vertical:14),),snackBarTheme:SnackBarThemeData(behavior:SnackBarBehavior.floating,shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(10)),),);}暗色主题
ThemeData_buildDarkTheme(){finalcolorScheme=ColorScheme.fromSeed(seedColor:constColor(0xFF4DB6AC),brightness:Brightness.dark,);returnThemeData(useMaterial3:true,colorScheme:colorScheme,appBarTheme:AppBarTheme(centerTitle:true,backgroundColor:colorScheme.surface,foregroundColor:colorScheme.onSurface,elevation:0,scrolledUnderElevation:1,),cardTheme:CardTheme(elevation:1.0,shadowColor:Colors.black.withOpacity(0.3),shape:RoundedRectangleBorder(borderRadius:BorderRadius.circular(16),),clipBehavior:Clip.antiAlias,),// ... 其余与亮色主题一致);}注意暗色主题用Brightness.dark——ColorScheme.fromSeed会基于这个参数自动调整色调的明暗层级。同一个色值#4DB6AC在 HCT 色彩空间中的表现会因亮度不同而变化。
在组件中使用 ColorScheme
在具体组件中,通过Theme.of(context).colorScheme获取颜色:
classMemoCardextendsStatelessWidget{finalMemomemo;@overrideWidgetbuild(BuildContextcontext){finalcolors=Theme.of(context).colorScheme;returnCard(color:colors.surface,child:ListTile(title:Text(memo.title,style:TextStyle(color:colors.onSurface)),subtitle:Text(memo.content,style:TextStyle(color:colors.onSurfaceVariant)),trailing:Icon(memo.isPinned?Icons.push_pin:null,color:colors.primary,),),);}}常用 colorScheme 属性速查:
| 属性 | 用途 |
|---|---|
primary | 主色,用于高亮交互元素 |
onPrimary | 主色上的文字颜色 |
primaryContainer | 主色的容器背景(比主色浅) |
secondary | 次要色 |
surface | 卡片、AppBar 等表面色 |
onSurface | 表面上的文字颜色 |
onSurfaceVariant | 表面上的次要文字(灰色调) |
error | 错误提示色 |
surfaceTint | 表面色调 |
关键规则:color和onColor成对使用。如果背景是surface,文字就是onSurface。这个命名约定在 Material 3 的文档中被严格执行。
ColorScheme 的 HCT 色彩空间
ColorScheme.fromSeed使用 HCT 而非传统的 HSL/RGB 色彩空间。HCT 的三个维度:
- Hue(色相):与 HSL 的色相相同
- Chroma(色度/饱和度):颜色的鲜艳程度
- Tone(亮度):从 0(黑)到 100(白)
HCT 的关键优势是感知均匀度——在 RGB 空间中看起来"亮度一致"的两个颜色,在人眼感知下可能分别偏亮和偏暗。HCT 修正了这个问题,让生成的色调在感知上真正一致。
对开发者来说,这意味着ColorScheme.fromSeed生成的色板在所有亮度层级下都具有足够的 WCAG 对比度,无需手动调整。Material 3 的颜色生成算法会自动保证可访问性。
皮肤页面(SkinPage)
应用预留了一个皮肤设置页面,允许用户在亮色/暗色/跟随系统之间手动切换。核心代码:
classSkinPageextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){finalcurrentMode=context.watch<ThemeProvider>().themeMode;returnScaffold(appBar:AppBar(title:constText('主题设置')),body:ListView(children:[RadioListTile<ThemeMode>(title:constText('浅色模式'),value:ThemeMode.light,groupValue:currentMode,onChanged:(mode)=>context.read<ThemeProvider>().setThemeMode(mode!),),RadioListTile<ThemeMode>(title:constText('深色模式'),value:ThemeMode.dark,groupValue:currentMode,onChanged:(mode)=>context.read<ThemeProvider>().setThemeMode(mode!),),RadioListTile<ThemeMode>(title:constText('跟随系统'),value:ThemeMode.system,groupValue:currentMode,onChanged:(mode)=>context.read<ThemeProvider>().setThemeMode(mode!),),],),);}}实现用户手动切换需要将ThemeMode提升为一个可持久化的状态:
classThemeProviderextendsChangeNotifier{ThemeMode_themeMode=ThemeMode.system;ThemeModegetthemeMode=>_themeMode;Future<void>loadThemeMode()async{// 从文件/SharedPreferences 读取用户偏好finalsaved=await_loadFromPrefs('theme_mode');if(saved!=null){_themeMode=ThemeMode.values[savedasint];notifyListeners();}}Future<void>setThemeMode(ThemeModemode)async{_themeMode=mode;await_saveToPrefs('theme_mode',mode.index);notifyListeners();}}鸿蒙兼容性
Material 3 的主题系统完全在 Flutter 框架层实现。ColorScheme.fromSeed的 HCT 算法和ThemeMode.system的亮暗检测都在 Flutter 引擎中完成。ThemeMode.system在鸿蒙上依赖@ohos/flutter_ohos引擎向 Flutter 报告系统亮暗模式。
如果鸿蒙设备上的系统亮暗模式检测有问题,可以通过MediaQuery.platformBrightnessOf(context)查看引擎返回的实际值,或在 Texture 中手动设置MediaQuery的platformBrightness。
总结
Material 3 种子色主题系统让应用配色从"手动拼凑 12 个色值"简化为"选一个种子色":
ColorScheme.fromSeed(seedColor: Color(0xFF4DB6AC)):一行代码生成 30+ 色调- 亮色 + 暗色两个 ThemeData:
Brightness.light和Brightness.dark控制色调层级 ThemeMode.system:自动跟随系统亮暗设置- HCT 色彩空间保证感知均匀度和无障碍对比度
完整项目代码见:todo_flutter_harmony
