Flutter for OpenHarmony 实战:加载中对话框与进度指示器
是Google开发的开源UI工具包,支持用一套代码构建和六大平台应用,实现"一次编写,多处运行"。是由开放原子开源基金会运营的分布式操作系统,为全场景智能设备提供统一底座,具有多设备支持、模块化设计、分布式能力和开源开放等特性。
前言
Flutter是Google开发的开源UI工具包,支持用一套代码构建iOS、Android、Web、Windows、macOS和Linux六大平台应用,实现"一次编写,多处运行"。
OpenHarmony是由开放原子开源基金会运营的分布式操作系统,为全场景智能设备提供统一底座,具有多设备支持、模块化设计、分布式能力和开源开放等特性。
Flutter for OpenHarmony技术方案使开发者能够:
- 复用Flutter现有代码(Skia渲染引擎、热重载、丰富组件库)
- 快速构建符合OpenHarmony规范的UI
- 降低多端开发成本
- 利用Dart生态插件资源加速生态建设
先看效果

在鸿蒙真机 上模拟器上成功运行后的效果
本文档说明当前项目中与「加载、进度」相关的实现:入口与目录、极光背景、加载对话框、霓虹进度条,以及演示页内使用的各个 UI 子组件。每个组件均包含说明、实现要点与使用方法,便于理解与复用。
目录(锚点导航)
- 一、项目结构说明
- 二、AuroraBackground 组件
- 三、LoadingDialog 与 LoadingDialogController
- 四、NeonProgressIndicator 组件
- 五、LoadingDemoPage 页面
- 六、LoadingDemoPage 内子组件
- 七、开发与使用注意点
- 八、总结
一、项目结构说明
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,在异步任务中可多次调用setTitle、setMessage、update(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(),内部会执行onCancel再close();业务侧可在onCancel里设标志位(如_cancelled = true),循环中检查后提前结束并关闭。
四、NeonProgressIndicator 组件
4.1 线性进度条 NeonLinearProgressIndicator
作用:横向条形进度,支持「确定进度」(0~1 的数值)和「不确定进度」(来回滑动的动画)。
通过 CustomPainter 绘制轨道、渐变进度、发光和扫描线,并用 RepaintBoundary 包住,只重绘该条,不影响周围布局。
参数含义:
value:null= 不确定进度(动画);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) 驱动旋转。
参数:value、size、strokeWidth、colors、trackColor、glowColor。
实现要点:
- 轨道用
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.0或null,否则确定进度时请先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 内依次为 _Logo、SizedBox、Expanded(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 设置 borderRadius、border、gradient、boxShadow,内部 SizedBox(44×44) + Center(Icon)。颜色与主题主色(如 0xFF22D3EE)统一。
使用方法:
仅作展示时直接使用 const _Logo();若需点击,可在外层包 InkWell 或 GestureDetector。
const _Logo()
// 或
InkWell(onTap: () {}, child: const _Logo())
6.3 _NeonCard 霓虹卡片
说明:带毛玻璃效果的卡片容器,包含标题、副标题和内容区。背景半透明 + BackdropFilter 模糊,边框与阴影营造霓虹/玻璃质感,适合在深色背景上分组展示内容。
实现要点:ClipRRect → BackdropFilter(ImageFilter.blur) → DecoratedBox(BoxDecoration:color、border、boxShadow) → Padding → Column(标题、副标题、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 和图标决定。
使用方法:
需要图标按钮时使用,传入 icon 和 onTap。常用于工具栏、AppBar 或 _TopBar 右侧。
_NeonIconButton(
icon: Icons.info_outline,
onTap: () => showHelp(),
)
6.6 _ModeChip 模式芯片
说明:二选一式的模式切换芯片(如「确定进度 / 不确定进度」)。选中态与未选中态在背景色、边框上区分明显,用于在两种状态间切换并驱动父组件状态。
实现要点:DecoratedBox 根据 selected 切换背景色与边框色,内部 InkWell + Padding + Center(Text)。选中时边框更亮(如主色 0x8022D3EE),未选中时更淡(0x22FFFFFF)。
使用方法:
与 ValueNotifier 或 setState 配合:父组件保存当前模式,点击芯片时更新状态并可能同步更新进度条(如不确定时传 value: null)。
_ModeChip(
label: '确定进度',
selected: !indeterminate,
onTap: () => _toggleInlineMode(false),
)
6.7 _Tips 小贴士文本
说明:一段固定的小贴士文案,以多行列表形式说明性能或实现要点(如 ValueNotifier、CustomPainter、RepaintBoundary、避免在 build 中创建昂贵对象等)。纯展示,无交互。
实现要点:Text 多行字符串,每行前加「• 」,样式为浅色、固定行高,与卡片内正文区分。
使用方法:
放在卡片或页面底部,用于开发说明或用户提示。若内容需动态化,可改为接收 String 或 List<String> 参数。
const _Tips()
// 若抽成独立组件可传参:
// TipsWidget(items: ['要点一', '要点二'])
七、要点注意
- 动画控制器:所有带
AnimationController的组件必须在dispose里_ctrl.dispose(),否则会泄漏。 - ValueNotifier:对话框和页面内进度用的
ValueNotifier在不再需要时(例如 controller.close 时)要dispose。 - LoadingDialogController:异步任务中一定要在合适分支调用
close(),避免对话框常驻;支持取消时在onCancel里设标志位并在循环中检查。 - 进度数值:确定进度时
value应限制在 0.0~1.0;传null表示不确定进度。 - 性能:AuroraBackground、NeonProgressIndicator 均使用 CustomPainter + RepaintBoundary,对话框内部用 ValueListenableBuilder 局部刷新,可保持列表与页面其它部分流畅。
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)