AtomGit Flutter鸿蒙客户端:主题系统

设计理念:遵循 Material 3

项目采用 Material 3 的主题体系,核心理念是"从单一种子色生成完整的设计系统"。Material 3 引入了基于 HCT(Hue-Chroma-Tone,色调-色度-明度)的颜色系统,只需提供一个种子色,框架会自动生成 Primary、Secondary、Tertiary、Error、Surface 五个颜色组,每个组包含多个明度变体。

AppTheme 类

dart 复制代码
import 'package:flutter/material.dart';

class AppTheme {
  static const _seedColor = Color(0xFF1A73E8);  // Google Blue

  static ThemeData get light => _buildTheme(Brightness.light);
  static ThemeData get dark => _buildTheme(Brightness.dark);

  static ThemeData _buildTheme(Brightness brightness) {
    final colorScheme = ColorScheme.fromSeed(
      seedColor: _seedColor,
      brightness: brightness,
    );

    return ThemeData(
      colorScheme: colorScheme,
      useMaterial3: true,

      // 组件主题覆盖
      appBarTheme: const AppBarTheme(
        centerTitle: false,
        elevation: 0,
        scrolledUnderElevation: 1,
      ),

      cardTheme: CardTheme(
        elevation: 1,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        clipBehavior: Clip.antiAlias,
      ),

      inputDecorationTheme: InputDecorationTheme(
        border: OutlineInputBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        contentPadding: const EdgeInsets.symmetric(
          horizontal: 16,
          vertical: 12,
        ),
        isDense: true,
      ),

      filledButtonTheme: FilledButtonThemeData(
        style: FilledButton.styleFrom(
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(8),
          ),
        ),
      ),
    );
  }
}

种子色的选择

#1A73E8(Google Blue)被选为种子色。这个颜色在蓝色系中具有良好的饱和度和对比度,Material 3 的 ColorScheme.fromSeed 会基于它生成完整的调色板。如果后续需要更换品牌色,只需修改这一个常量。

Brightness 枚举的使用

_buildTheme 接收 Brightness 枚举并根据其值生成对应的 ColorScheme。Material 3 的 ColorScheme.fromSeed 在接收 brightness: Brightness.dark 时,会自动调整所有颜色的明度------Primary 色会调亮以在深色背景上有足够对比度,Surface 色会变暗。

组件主题覆盖的详细说明

全局的 ThemeData 是组件样式的最高优先级。通过 ThemeData 的组件属性覆盖,可以确保应用中所有同类组件保持一致的视觉效果,无需在每个使用点重复设置样式。

AppBar 主题

dart 复制代码
appBarTheme: const AppBarTheme(
  centerTitle: false,       // 标题左对齐(而非居中)
  elevation: 0,             // 无阴影(扁平设计)
  scrolledUnderElevation: 1, // 滚动时显示 1px 阴影
),

centerTitle: false 是 Material 3 的默认值,左对齐标题适合内容密度高的应用。scrolledUnderElevation: 1 在用户滚动内容时在 AppBar 底部添加细微阴影,提供视觉层次------内容滚动到 AppBar 下面时,阴影分离了两者。

Card 主题

dart 复制代码
cardTheme: CardTheme(
  elevation: 1,             // 轻微阴影
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(12),  // 12px 圆角
  ),
  clipBehavior: Clip.antiAlias,  // 抗锯齿裁剪
),

elevation: 1 创建非常轻微的阴影------刚好能让 Card 从背景中浮出来,但不会有 Material 2 那种厚重的投影感。12px 的圆角属于 Material 3 的"中等圆角"风格。

clipBehavior: Clip.antiAlias 确保 Card 的子 Widget 被裁剪到圆角边界内。没有这个设置,子 Widget(如 InkWell 的水波纹)可能超出圆角范围,产生视觉瑕疵。

InputDecoration 主题

dart 复制代码
inputDecorationTheme: InputDecorationTheme(
  border: OutlineInputBorder(
    borderRadius: BorderRadius.circular(12),
  ),
  contentPadding: const EdgeInsets.symmetric(
    horizontal: 16,
    vertical: 12,
  ),
  isDense: true,
),

OutlineInputBorder 是 Material 3 推荐的输入框样式(对比 Material 2 的 UnderlineInputBorder 线条样式)。12px 圆角与 Card 保持一致。isDense: true 减小输入框的默认高度,适合移动端紧凑布局。

FilledButton 主题

dart 复制代码
filledButtonTheme: FilledButtonThemeData(
  style: FilledButton.styleFrom(
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(8),
    ),
  ),
),

FilledButton 是 Material 3 的主要按钮样式,使用 Primary 色填充。8px 圆角(比 Card 的 12px 略小)是 Material 3 按钮的推荐圆角值。

在 MaterialApp 中使用主题

dart 复制代码
class AtomGitApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [...],
      child: MaterialApp(
        title: 'AtomGit',
        theme: AppTheme.light,
        darkTheme: AppTheme.dark,
        themeMode: ThemeMode.system,  // 跟随系统设置
        home: const MainShell(),
        onGenerateRoute: AtomGitApp.generateRoute,
      ),
    );
  }
}

themeMode: ThemeMode.system 让主题自动跟随系统:

  • 系统浅色模式 → AppTheme.light
  • 系统深色模式 → AppTheme.dark

Flutter 在系统主题切换时会自动重建 MaterialApp,无需应用代码手动处理。这是在 ThemeData 级别实现的------两个 ThemeData 对象中的颜色值不同,MaterialApp 自动选择。

组件中消费主题

所有组件通过 Theme.of(context) 获取当前主题,而不是硬编码颜色值:

dart 复制代码
// 获取主题色
final primaryColor = Theme.of(context).colorScheme.primary;

// 获取错误色
final errorColor = Theme.of(context).colorScheme.error;

// 获取 Surface 容器色(代码块背景、头像占位)
final containerColor = Theme.of(context)
    .colorScheme.surfaceContainerHighest;

// 获取文字样式
final titleStyle = Theme.of(context).textTheme.titleMedium;
final bodyStyle = Theme.of(context).textTheme.bodySmall;

为什么不能硬编码颜色

dart 复制代码
// 不好:切换深色模式后文字不可见
Text('Hello', style: TextStyle(color: Colors.black));

// 好:自动适配深色/浅色
Text('Hello',
    style: TextStyle(
        color: Theme.of(context).colorScheme.onSurface));

硬编码颜色只在一个主题下是正确的。Theme.of(context) 获取的颜色是当前有效主题的值,自动适配系统切换。

ColorScheme 的关键令牌

Material 3 的 ColorScheme 提供了丰富的颜色令牌。项目中使用频率最高的:

令牌 浅色模式示例 深色模式示例 用途
primary 亮蓝 淡蓝 主题色,用于按钮、选中态、Tab 指示器
onPrimary 白色 深蓝黑 Primary 色上的文字色
primaryContainer 淡蓝 深蓝 展示 primary 的背景容器
onPrimaryContainer 深蓝 淡蓝 容器上的文字色
secondary 蓝灰 浅蓝灰 次要元素(FilterChip)
error 红色 浅红 错误状态图标和文字
surface 近白 深灰 页面背景
surfaceContainerHighest 浅灰 深灰 最突出的表面容器(代码块背景)
onSurface 深灰(近黑) 近白 主要文字色
onSurfaceVariant 中灰 浅灰 次要文字色

Material 3 ColorScheme.fromSeed 的原理

ColorScheme.fromSeed 使用 Material 3 的 HCT 色彩系统:

  1. 输入 :一个种子色(#1A73E8)和一个亮度模式
  2. 色调(Hue)分析:从种子色提取色调值
  3. 色度(Chroma)分配:为 Primary 分配最高色度(鲜艳),Secondary 降低色度(柔和),Tertiary 取互补色调
  4. 明度(Tone)调整:根据 brightness 参数为每个颜色生成不同明度的变体
  5. 对比度保证OnXxx 颜色自动选择与对应 Xxx 色具有足够对比度的明度值(满足 WCAG 无障碍标准)

深色模式的颜色适配

Material 3 的深色模式不是简单地反转颜色,而是精心调整色调和明度:

  • Primary 变淡 :在深色背景上,primary 变得更亮以确保对比度
  • Surface 变暗:背景色从浅灰变为深灰,减少屏幕发光量
  • Container 颜色反转:浅色模式下 Container 是主色的淡化版(白底上带淡色),深色模式下 Container 是主色的加深版(黑底上带暗色)
  • OnSurface 变亮:文字色从深色变为浅色,保证在暗背景上的可读性

项目中不需要为深色模式写任何额外的颜色代码------所有颜色通过 ColorScheme 引用,自动获得适配。

API 常量和应用配置

dart 复制代码
class ApiConstants {
  static const String baseUrl =
      'https://api.atomgit.com/api/v5';
  static const String authorizeUrl =
      'https://api.atomgit.com/login/oauth/authorize';
  static const String tokenUrl =
      'https://api.atomgit.com/login/oauth/access_token';
  static const String redirectUri =
      'atomgit://oauth/callback';
  static const String apiVersion = '2023-02-21';
  static const String scope = 'repo user';

  static const int pageSize = 30;
  static const int rateLimitAuthenticated = 5000;
  static const int rateLimitUnauthenticated = 60;
}

API 相关常量集中在一个类中。这遵循"单一数据源"原则------如果 API 地址变更,只需修改一处。

主题扩展

未来可以为应用添加应用内主题切换(不依赖系统设置)。实现思路:

dart 复制代码
enum AppThemeMode { light, dark, system }

class ThemeProvider extends ChangeNotifier {
  AppThemeMode _mode = AppThemeMode.system;

  ThemeMode get themeMode => switch (_mode) {
    AppThemeMode.light  => ThemeMode.light,
    AppThemeMode.dark   => ThemeMode.dark,
    AppThemeMode.system => ThemeMode.system,
  };

  void setMode(AppThemeMode mode) {
    _mode = mode;
    notifyListeners();
  }
}

然后在 MaterialApp 中:

dart 复制代码
MaterialApp(
  themeMode: context.watch<ThemeProvider>().themeMode,
  // ...
)

使用主题的最佳实践

几个贯穿整个项目的主题使用原则:

  1. 永远使用 Theme.of(context),不要缓存主题引用。系统主题切换时缓存的引用会过时。

  2. 使用语义颜色令牌 ,而非低级颜色值。colorScheme.error 好于 Colors.red,因为 error 色在深色模式下会自动调节明度。

  3. 使用 textTheme 的样式层级 ,而非自定义 fontSize。textTheme.titleMedium 好于 fontSize: 16,因为字体大小适配无障碍设置。

  4. Card 和 InputDecoration 的圆角保持一致(12px),FilledButton 略小(8px),形成清晰的视觉层次。

相关推荐
yuegu7773 小时前
HarmonyOS应用<节气通>开发第17篇:意见反馈页面
华为·harmonyos
yuegu7773 小时前
HarmonyOS应用<节气通>开发第19篇:空态页面设计
harmonyos
烬羽3 小时前
JS 单线程为什么不卡?一文吃透同步异步、Event Loop 和 Promise
javascript·面试
伶俜663 小时前
零基础学 ArkUI 传感器(专题二):从加速度计到指南针,玩转硬件能力
学习·华为·harmonyos
G_dou_3 小时前
Flutter三方库适配OpenHarmony【expense_tracker】消费记录器项目完整实战
flutter·harmonyos
葬送的代码人生3 小时前
JavaScript 数组完全指南:从入门到实战
前端·javascript·算法
用户938515635074 小时前
深入理解 JavaScript 同步与异步:从单线程到事件循环与 Promise
前端·javascript
FrameNotWork4 小时前
HarmonyOS6.1 从图像分类到目标检测的扩展实现
人工智能·harmonyos
西西学代码4 小时前
Flutter---GlobalKey
flutter