前言

Flutter是Google开发的开源UI工具包,支持用一套代码构建iOSAndroidWebWindowsmacOSLinux六大平台应用,实现"一次编写,多处运行"。

OpenHarmony是由开放原子开源基金会运营的分布式操作系统,为全场景智能设备提供统一底座,具有多设备支持、模块化设计、分布式能力和开源开放等特性。

Flutter for OpenHarmony技术方案使开发者能够:

  1. 复用Flutter现有代码(Skia渲染引擎、热重载、丰富组件库)
  2. 快速构建符合OpenHarmony规范的UI
  3. 降低多端开发成本
  4. 利用Dart生态插件资源加速生态建设

先看效果

在这里插入图片描述

在鸿蒙真机 上模拟器上成功运行后的效果

本文档说明当前项目中与「加载、进度」相关的实现:入口与目录、极光背景、加载对话框、霓虹进度条,以及演示页内使用的各个 UI 子组件。每个组件均包含说明实现要点使用方法,便于理解与复用。


目录(锚点导航)


一、项目结构说明

1.1 入口文件 main.dart

作用:Flutter 应用入口,负责启动应用并指定首页和主题。

// lib/main.dart
import 'package:flutter/material.dart';
import 'ui/pages/loading_demo_page.dart';

void main() {
  runApp(const MyApp());  // 将根 Widget 挂载到 Flutter 引擎
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Loading Demo',
      theme: ThemeData(
        useMaterial3: true,
        brightness: Brightness.dark,
        fontFamilyFallback: const ['PingFang SC', 'Microsoft YaHei', 'sans-serif'],
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF22D3EE),   // 青色,用于强调与进度高亮
          secondary: Color(0xFF7C3AED), // 紫色
          surface: Color(0xFF0B0D14),  // 深色背景
        ),
      ),
      home: const LoadingDemoPage(),   // 首页为加载演示页
    );
  }
}

说明

  • runApp(MyApp()) 把整棵 Widget 树交给 Flutter 渲染。
  • ThemeData 里统一了深色主题和主色,后续组件可直接用 Theme.of(context).colorScheme 保持风格一致。
  • home 指向 LoadingDemoPage,所有加载、进度相关的演示都在该页完成。

1.2 目录与职责划分

路径 职责
lib/main.dart 应用入口,MaterialApp 与主题配置
lib/ui/pages/loading_demo_page.dart 加载演示页:展示对话框、内联进度条、切换确定/不确定进度
lib/ui/widgets/aurora_background.dart 极光背景:网格 + 动态光斑,仅重绘自身
lib/ui/widgets/loading_dialog.dart 加载对话框:标题/文案/进度可更新,支持取消,内部用 ValueNotifier 局部刷新
lib/ui/widgets/neon_progress_indicator.dart 霓虹风格进度条:线性 + 环形,确定/不确定两种模式,CustomPainter 绘制

二、AuroraBackground 组件

2.1 组件说明

AuroraBackground 是一个「极光风格」的全屏背景:深色底 + 半透明网格 + 多个缓慢移动的彩色光斑(blob),并带一点暗角。
动画通过 AnimationController 驱动,在 CustomPainter 里根据 t 计算光斑位置与绘制,因此只有背景自己在重绘,不会导致上面的按钮、列表等整页重建,适合做复杂页面的装饰层。

注意:内部使用了 SingleTickerProviderStateMixin 和 8 秒循环动画,在 dispose 中会释放 AnimationController


2.2 实现要点与代码解释

状态与动画

// lib/ui/widgets/aurora_background.dart(节选)
class _AuroraBackgroundState extends State<AuroraBackground>
    with SingleTickerProviderStateMixin {
  late final AnimationController _ctrl;

  
  void initState() {
    super.initState();
    _ctrl = AnimationController(
      vsync: this,                                    // 需要 TickerProvider,由 mixin 提供
      duration: const Duration(milliseconds: 8000),   // 一轮 8 秒
    )..repeat();                                     // 循环播放,t 从 0→1 反复
  }

  
  void dispose() {
    _ctrl.dispose();   // 必须释放,否则会泄漏
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _ctrl,
      builder: (context, _) {
        return CustomPaint(
          painter: _AuroraPainter(t: _ctrl.value),   // 把 0~1 的进度传给 Painter
          child: widget.child,                        // 背景在上,内容在下,child 叠在上面
        );
      },
    );
  }
}

绘制逻辑(节选):先画底色矩形,再画网格线,再按 t 用正弦/余弦计算每个光斑的 cx/cy,调用 _blob 画模糊圆,最后叠一层径向渐变暗角。

// 光斑位置随 t 变化,形成缓慢移动
_blob(canvas, size,
  cx: 0.18 + 0.10 * math.sin(t * math.pi * 2),
  cy: 0.28 + 0.12 * math.cos(t * math.pi * 2),
  r: 0.46,
  color: const Color(0xFF22D3EE),
);
// _blob 内部:MaskFilter.blur 实现模糊,drawCircle 画圆

特殊逻辑

  • 使用 CustomPainter 而非大量 Widget,避免整树重建;shouldRepaint 只在 t 变化时返回 true,重绘范围可控。
  • 光斑坐标用「相对宽高的比例 + 正弦/余弦」计算,适配不同屏幕;r 也是相对短边的比例,保证视觉统一。

2.3 使用方法

基本用法:把需要放在「极光背景之上」的内容作为 child 传入即可。

AuroraBackground(
  child: SafeArea(
    child: ListView(
      padding: const EdgeInsets.all(16),
      children: [
        // 你的卡片、按钮、进度条等
      ],
    ),
  ),
)

注意

  • AuroraBackground 会占满父级空间,一般作为 Scaffold.body 的最底层或 Stack 的第一子组件。
  • 若页面有大量列表或表单,建议保持背景仅此一层,避免在列表项里再包一层复杂动画背景,以利性能。

三、LoadingDialog 与 LoadingDialogController

3.1 组件说明

LoadingDialog 不是一个直接放在树里的 Widget,而是通过 LoadingDialogController.show 弹出的一个对话框。
对话框内容包含:标题、正文、环形进度、线性进度(可选)、取消/隐藏按钮。
LoadingDialogController 负责在对话框已显示的情况下,只更新标题、文案、进度,而不用关闭再打开,也不会让调用方页面整页 setState,从而保持交互流畅。

要点

  • 进度为 null 表示「不确定进度」(只显示动画);为 0.0~1.0 表示确定进度。
  • 更新通过 ValueNotifier 驱动,对话框内部用 ValueListenableBuilder 监听,因此只有对话框内对应区域重建。
  • 调用方拿到的是 LoadingDialogController,在异步任务中可多次调用 setTitlesetMessageupdate(progress: x),最后调用 close() 关闭。

3.2 实现要点与代码解释

弹出与控制器构造

// lib/ui/widgets/loading_dialog.dart(节选)
static LoadingDialogController show(
  BuildContext context, {
  String title = '加载中',
  String? message,
  double? progress,           // null = 不确定进度,0~1 = 确定进度
  bool barrierDismissible = false,
  VoidCallback? onCancel,
}) {
  final titleN = ValueNotifier<String?>(title);
  final messageN = ValueNotifier<String?>(message);
  final progressN = ValueNotifier<double?>(progress);

  final controller = LoadingDialogController._(
    context: context,
    progress: progressN,
    message: messageN,
    title: titleN,
    onCancel: onCancel,
  );

  showGeneralDialog<void>(
    context: context,
    barrierDismissible: barrierDismissible,
    barrierColor: const Color(0xCC05060A),
    transitionDuration: const Duration(milliseconds: 180),
    pageBuilder: (context, _, __) {
      return _LoadingDialogSurface(controller: controller);  // 对话框内容
    },
    // ...
  );
  return controller;   // 调用方持有 controller,用于后续更新和 close
}

控制器更新接口

void setTitle(String? v)   => _title.value = v;
void setMessage(String? v) => _message.value = v;
void setProgress(double? v) => _progress.value = v;

// 一次更新多个字段;progress 传 null 表示改为「不确定进度」
void update({Object? progress = _unset, String? message, String? title}) {
  if (!identical(progress, _unset)) _progress.value = progress as double?;
  if (message != null) _message.value = message;
  if (title != null) _title.value = title;
}

void close<T>([T? result]) {
  if (_closed) return;
  _closed = true;
  Navigator.of(_context, rootNavigator: true).pop(result);
  _progress.dispose();
  _message.dispose();
  _title.dispose();
}

特殊逻辑

  • update 里用 identical(progress, _unset) 区分「调用方没传 progress」和「传了 null(要改为不确定进度)」。
  • 关闭时除了 pop,还要对三个 ValueNotifier 执行 dispose,避免泄漏。
  • 对话框内部标题、文案、进度都用 ValueListenableBuilder 包一层,这样 setTitle/setMessage/setProgress 只会触发对应小块重建,不会整弹窗 rebuild。

3.3 使用方法

不确定进度(只有动画,无百分比)

final c = LoadingDialogController.show(
  context,
  title: '量子加载中',
  message: '正在建立安全通道…',
  progress: null,   // null = 不确定进度
);

await Future.delayed(const Duration(seconds: 2));
c.setMessage('同步缓存与策略…');
await Future.delayed(const Duration(seconds: 1));
c.close();

确定进度(如模拟下载,可取消)

bool _cancelled = false;

final c = LoadingDialogController.show(
  context,
  title: '下载中',
  message: '准备资源包…',
  progress: 0,
  onCancel: () => _cancelled = true,   // 用户点「取消」时置为 true
);

for (double p = 0; p <= 1; p += 0.02) {
  if (_cancelled) {
    c.setTitle('已取消');
    c.setMessage('操作已被用户取消。');
    c.update(progress: 0.0);
    await Future.delayed(const Duration(milliseconds: 400));
    c.close();
    return;
  }
  c.update(progress: p, message: '下载资源… ${(p * 100).toStringAsFixed(0)}%');
  await Future.delayed(const Duration(milliseconds: 50));
}
c.setTitle('完成');
c.setMessage('一切就绪。');
await Future.delayed(const Duration(milliseconds: 350));
c.close();

注意

  • 在异步逻辑里务必在合适时机调用 c.close(),否则对话框会一直存在。
  • 若传了 onCancel,用户点「取消」会调用 controller.cancel(),内部会执行 onCancelclose();业务侧可在 onCancel 里设标志位(如 _cancelled = true),循环中检查后提前结束并关闭。

四、NeonProgressIndicator 组件

4.1 线性进度条 NeonLinearProgressIndicator

作用:横向条形进度,支持「确定进度」(0~1 的数值)和「不确定进度」(来回滑动的动画)。
通过 CustomPainter 绘制轨道、渐变进度、发光和扫描线,并用 RepaintBoundary 包住,只重绘该条,不影响周围布局。

参数含义

  • valuenull = 不确定进度(动画);0.0~1.0 = 确定进度。
  • height:条的高度。
  • colors:进度段渐变色列表。
  • trackColor:轨道背景色。
  • glowColor:发光层颜色。
  • borderRadius:可选,不传则用 height/2 做圆角。

实现要点

  • didUpdateWidget 里判断从「确定→不确定」或「不确定→确定」时,对 AnimationController 执行 repeat()stop(),保证动画状态与 value 一致。
  • 确定进度:根据 value 计算宽度,用 LinearGradient 画一段矩形,再叠一层 MaskFilter.blur 的发光。
  • 不确定进度:用 _easeInOutCubic(t) 计算位移,画一段固定宽度的渐变条来回移动。
// 确定进度:宽度 = size.width * value
if (value != null) {
  final v = value!.clamp(0.0, 1.0);
  final w = size.width * v;
  // 画发光层 + 渐变矩形
}
// 不确定进度:一段条带随 t 移动
else {
  final segW = size.width * 0.35;
  final x = _easeInOutCubic(t) * (size.width + segW) - segW;
  // 画矩形 at (x, 0, segW, height)
}

4.2 环形进度条 NeonCircularProgressIndicator

作用:圆环进度,同样支持确定/不确定。确定时弧长对应 value;不确定时弧长在约 85% 附近轻微变化并旋转,形成「转圈」效果。
末端有一个小高光点,由 GradientRotation(rot) 驱动旋转。

参数valuesizestrokeWidthcolorstrackColorglowColor

实现要点

  • 轨道用 drawCircle 描边;进度弧用 drawArc(rect, start, sweep, false, ...)
  • sweep:确定进度为 value * 2π;不确定为 (0.85 + 0.1*sin(t)) * π,并配合 rot = t * 2π 让渐变旋转。
  • 高光点坐标:start + sweep + rot 对应的圆上点,用 drawCircle 画小圆并加一点模糊。

4.3 使用方法

页面内嵌:确定进度 + 滑块

final ValueNotifier<double?> _progress = ValueNotifier<double?>(0.35);

// 线性
NeonLinearProgressIndicator(
  value: _progress.value,
  height: 10,
  colors: const [Color(0xFF7C3AED), Color(0xFF22D3EE), Color(0xFFA3E635)],
)

// 环形
NeonCircularProgressIndicator(
  value: _progress.value,
  size: 54,
  strokeWidth: 6,
)

// 滑块改变进度
Slider(
  value: (_progress.value ?? 0).clamp(0.0, 1.0),
  onChanged: (v) => _progress.value = v,
)

不确定进度(只显示动画)

NeonLinearProgressIndicator(value: null, height: 10)
NeonCircularProgressIndicator(value: null, size: 54, strokeWidth: 6)

在 LoadingDialog 中的用法(对话框内部已这样使用):

ValueListenableBuilder<double?>(
  valueListenable: controller._progress,
  builder: (context, value, _) {
    return NeonCircularProgressIndicator(value: value, size: 56, strokeWidth: 6);
  },
)

注意

  • value 必须在 0.0~1.0null,否则确定进度时请先 clamp(0.0, 1.0)
  • 线性/环形都包在 RepaintBoundary 内,适合在列表或弹层里使用,避免整页重绘。

五、LoadingDemoPage 页面

5.1 页面功能

职责:演示「加载对话框」和「页面内进度条」的用法,包括不确定进度、模拟下载(可取消)、确定/不确定切换、滑块调节进度。

主要状态

  • _running:防止重复点击触发多次弹窗。
  • _cancelled:用户点「取消」时置为 true,模拟下载循环中检查并提前结束。
  • _inlineProgress:页面内进度条的数值(0~1),ValueNotifier<double?>
  • _inlineIndeterminate:是否处于「不确定进度」模式,ValueNotifier<bool>

说明:进度条和模式切换都用 ValueNotifier + ValueListenableBuilder,这样拖动滑块或切换模式时,只有进度条和说明文字那一块重建,整页不会 setState。


5.2 与各组件的配合方式

  • AuroraBackground:作为整页背景,child 里放 SafeArea + Stack,Stack 里再放列表内容和顶部栏。
  • LoadingDialogController.show:在按钮的 onPressed 里调用,拿到 LoadingDialogController 后,在 Future.delayed 或循环里调用 setTitle/setMessage/update/close
  • NeonLinearProgressIndicator / NeonCircularProgressIndicator:放在 ValueListenableBuilder<double?> 里,监听 _inlineProgress;确定/不确定的切换通过 _inlineIndeterminate 控制,不确定时传 value: null

六、LoadingDemoPage 内子组件

演示页中使用了若干私有 Widget(以下用类名指代),用于顶部栏、卡片、按钮、模式切换和提示文案。若需在其它页面复用,可将对应类提取为独立组件并改为 public。


6.1 _TopBar 顶部栏

说明:页面顶部的标题栏,左侧为 Logo + 标题/副标题,右侧为「说明」图标按钮。用于展示当前模块名称和一句简短描述,并触发说明弹层。

实现要点
Row 内依次为 _LogoSizedBoxExpanded(Column(标题 + 副标题))_NeonIconButton。标题使用 textTheme.titleLarge,副标题使用 textTheme.bodySmall,颜色与字重与整体深色霓虹风格一致。

使用方法
在页面顶部放入 _TopBar,并传入 onInfo 回调(例如弹出 BottomSheet 或对话框展示说明文案)。

_TopBar(
  onInfo: () {
    showModalBottomSheet(context: context, builder: (ctx) => YourHelpContent());
  },
)

6.2 _Logo Logo

说明:一个小型 logo 区域,圆角矩形底 + 边框 + 阴影,中间为图标(如 Icons.bolt)。用于品牌或模块标识,与顶部栏搭配使用。

实现要点
DecoratedBox 设置 borderRadiusbordergradientboxShadow,内部 SizedBox(44×44) + Center(Icon)。颜色与主题主色(如 0xFF22D3EE)统一。

使用方法
仅作展示时直接使用 const _Logo();若需点击,可在外层包 InkWellGestureDetector

const _Logo()
// 或
InkWell(onTap: () {}, child: const _Logo())

6.3 _NeonCard 霓虹卡片

说明:带毛玻璃效果的卡片容器,包含标题、副标题和内容区。背景半透明 + BackdropFilter 模糊,边框与阴影营造霓虹/玻璃质感,适合在深色背景上分组展示内容。

实现要点
ClipRRectBackdropFilter(ImageFilter.blur)DecoratedBox(BoxDecoration:color、border、boxShadow)PaddingColumn(标题、副标题、child)。标题与副标题使用 textTheme.titleMedium / bodySmall,与背景对比清晰。

使用方法
将卡片标题、副标题和主体内容通过参数传入,child 为卡片下方的 Widget(如按钮组、进度条、表单等)。

_NeonCard(
  title: '加载中对话框',
  subtitle: '可更新标题/文案/进度,支持取消;动画只在组件内重绘。',
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.stretch,
    children: [
      // 按钮、进度条等
    ],
  ),
)

6.4 _NeonButton 霓虹按钮

说明:带图标和文字的霓虹风格主按钮。深色渐变底、描边、阴影,点击区域为 InkWell,用于主要操作(如「不确定进度」「模拟下载」)。

实现要点
DecoratedBox(圆角、渐变、边框、阴影)包裹 InkWell,内部 Padding + Row(Icon, SizedBox, Text)。主色与 _Logo、进度条等一致,保证视觉统一。

使用方法
传入按钮文字 label、图标 icon 和点击回调 onPressed。适合放在卡片内或 Wrap 中与其它按钮并列。

_NeonButton(
  label: '不确定进度',
  icon: Icons.hourglass_top,
  onPressed: () async {
    // 打开加载对话框等
  },
)

6.5 _NeonIconButton 霓虹图标按钮

说明:仅图标的圆角方形按钮,半透明底 + 细边框,用于次要操作(如顶部栏的「说明」)。风格与 _NeonButton 统一,不抢主按钮视觉。

实现要点
DecoratedBox(背景色、圆角、边框)包裹 InkWell,内部 Padding + Icon。无文字,尺寸由 padding 和图标决定。

使用方法
需要图标按钮时使用,传入 icononTap。常用于工具栏、AppBar 或 _TopBar 右侧。

_NeonIconButton(
  icon: Icons.info_outline,
  onTap: () => showHelp(),
)

6.6 _ModeChip 模式芯片

说明:二选一式的模式切换芯片(如「确定进度 / 不确定进度」)。选中态与未选中态在背景色、边框上区分明显,用于在两种状态间切换并驱动父组件状态。

实现要点
DecoratedBox 根据 selected 切换背景色与边框色,内部 InkWell + Padding + Center(Text)。选中时边框更亮(如主色 0x8022D3EE),未选中时更淡(0x22FFFFFF)。

使用方法
ValueNotifiersetState 配合:父组件保存当前模式,点击芯片时更新状态并可能同步更新进度条(如不确定时传 value: null)。

_ModeChip(
  label: '确定进度',
  selected: !indeterminate,
  onTap: () => _toggleInlineMode(false),
)

6.7 _Tips 小贴士文本

说明:一段固定的小贴士文案,以多行列表形式说明性能或实现要点(如 ValueNotifier、CustomPainter、RepaintBoundary、避免在 build 中创建昂贵对象等)。纯展示,无交互。

实现要点
Text 多行字符串,每行前加「• 」,样式为浅色、固定行高,与卡片内正文区分。

使用方法
放在卡片或页面底部,用于开发说明或用户提示。若内容需动态化,可改为接收 StringList<String> 参数。

const _Tips()
// 若抽成独立组件可传参:
// TipsWidget(items: ['要点一', '要点二'])

七、要点注意

  1. 动画控制器:所有带 AnimationController 的组件必须在 dispose_ctrl.dispose(),否则会泄漏。
  2. ValueNotifier:对话框和页面内进度用的 ValueNotifier 在不再需要时(例如 controller.close 时)要 dispose
  3. LoadingDialogController:异步任务中一定要在合适分支调用 close(),避免对话框常驻;支持取消时在 onCancel 里设标志位并在循环中检查。
  4. 进度数值:确定进度时 value 应限制在 0.0~1.0;传 null 表示不确定进度。
  5. 性能:AuroraBackground、NeonProgressIndicator 均使用 CustomPainter + RepaintBoundary,对话框内部用 ValueListenableBuilder 局部刷新,可保持列表与页面其它部分流畅。

欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐