HarmonyOS APP《画伴梦工厂》开发第14篇:画与进度反馈——Progress 与帧动画
第2.6篇:动画与进度反馈——Progress 与帧动画
难度:⭐⭐ 进阶 | 前置知识:1.3 @State 与状态管理 | 涉及源文件:
products/default/src/main/ets/pages/RecognitionWaitingPage.ets
一、引言
当用户完成了涂鸦或照片采集,点击"生成动画"后,应用进入 RecognitionWaitingPage。在这个页面上,AI 正在后台处理图像、识别内容、生成视频帧。整个过程可能持续数秒到数十秒。
对儿童用户而言,一个静止的"加载中…"页面是不可接受的。没有视觉反馈,小朋友会认为应用卡住了,进而反复点击或关闭应用。
"画伴梦工厂"的等待页面通过三层进度反馈机制来解决这个问题:
- Progress 进度条 —— 展示整体进度百分比
- 帧动画气泡 —— 动态变化的 Circle 组件产生"跳动"感
- 步骤条 —— 分阶段展示任务进度,让用户知道"现在在做什么"
本文将深入分析这三层机制的设计与实现。
二、状态设计
@State private progress: number = 12;
@State private activeStep: number = 0;
@State private statusText: string = '正在准备生成任务';
@State private failed: boolean = false;
@State private completed: boolean = false;
@State private animationFrame: number = 0;
@State private waitingTip: string = WAITING_TIPS[0];
| 状态 | 类型 | 初始值 | 作用 |
|---|---|---|---|
progress |
number | 12 | 进度百分比 0-100 |
activeStep |
number | 0 | 当前激活的步骤索引(0-3) |
statusText |
string | ‘正在准备生成任务’ | 主要状态文本 |
failed |
boolean | false | 是否生成失败 |
completed |
boolean | false | 是否生成完成 |
animationFrame |
number | 0 | 帧动画的当前帧(0-3) |
waitingTip |
string | WAITING_TIPS[0] | 底部提示文案 |
这些状态通过 @State 装饰器 驱动 UI 自动刷新。ArkUI 的响应式系统会检测状态变化,只更新受影响的 UI 部分,保证了动画性能。
三、Progress 组件详解
3.1 基础用法
Progress({ value: this.progress, total: 100, type: ProgressType.Linear })
.width('84%')
.height(8)
.color(this.mint) // 前景色 #42CDA3
.backgroundColor('#ECECF6') // 背景色
.borderRadius(6)
.margin({ top: 12 })
参数说明:
| 参数 | 说明 |
|---|---|
value |
当前进度值(需在 0 到 total 之间) |
total |
总进度值 |
type |
进度条类型:ProgressType.Linear(线性)或 ProgressType.Circle(圆形) |
Progress 类型一览:
| 类型 | 说明 | 适用场景 |
|---|---|---|
Linear |
水平线性进度条 | 通用进度展示 |
Circle |
圆形进度环 | 需要节省空间或强调百分比的场景 |
3.2 进度驱动逻辑
进度由 setInterval 定时器驱动,模拟 AI 生成进度:
private startWaitingTimer() {
if (this.timerId >= 0) { clearInterval(this.timerId); }
this.timerId = setInterval(() => {
if (this.failed || this.completed) {
clearInterval(this.timerId);
this.timerId = -1;
return;
}
this.animationFrame = (this.animationFrame + 1) % 4;
this.waitingTip = WAITING_TIPS[this.animationFrame % WAITING_TIPS.length];
if (this.progress < 92) {
this.progress = Math.min(92, this.progress + 3);
} else {
this.progress = Math.min(97, this.progress + 1);
}
this.activeStep = Math.min(3, Math.floor(this.progress / 28));
}, 1200);
}
进度条设计考量:
- 起始值 12:不让用户看到"从 0 开始"的假象,让等待显得已经进行了一段时间。
- 分段速度:
progress < 92时每次 +3,>= 92时每次 +1。前端进度"慢下来"营造接近完成的真实感。 - 上限 97:预留最后的 3% 给真正的完成回调,不会出现"99% 卡住"的尴尬。
- 1200ms 间隔:约 1.2 秒更新一次,节奏舒缓不急促。
3.3 完成与失败的处理
当 AI 生成完成或失败时:
// 成功
this.progress = 100;
this.completed = true;
this.statusText = '视频已生成并保存到作品';
// 失败
this.failed = true;
this.statusText = '生成失败:' + getErrorMessage(error);
定时器会检测 failed 或 completed 状态,及时 clearInterval 停止更新。
四、帧动画气泡:LoadingBubbles
4.1 实现代码
@Builder
private LoadingBubbles() {
Row() {
Circle()
.width(this.animationFrame === 0 ? 24 : 16)
.height(this.animationFrame === 0 ? 24 : 16)
.fill(this.sunshine) // #FFB84D 黄色
.opacity(this.animationFrame === 0 ? 1 : 0.55)
Circle()
.width(this.animationFrame === 1 ? 24 : 16)
.height(this.animationFrame === 1 ? 24 : 16)
.fill(this.mint) // #42CDA3 绿色
.opacity(this.animationFrame === 1 ? 1 : 0.55)
.margin({ left: 12 })
Circle()
.width(this.animationFrame === 2 ? 24 : 16)
.height(this.animationFrame === 2 ? 24 : 16)
.fill(this.brandPurple) // #7657F3 紫色
.opacity(this.animationFrame === 2 ? 1 : 0.55)
.margin({ left: 12 })
}
.height(34)
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Center)
.margin({ top: 12 })
}
4.2 动画原理
这是一个无动画 API 的帧动画方案,通过定时器切换 animationFrame 状态,驱动 Circle 组件的大小和透明度变化:
animationFrame = 0 → 圆圈1 放大(24) + 高不透明度(1),圆圈2、3 缩小(16) + 低不透明度(0.55)
animationFrame = 1 → 圆圈2 放大 + 高不透明度,圆圈1、3 缩小 + 低不透明度
animationFrame = 2 → 圆圈3 放大 + 高不透明度,圆圈1、2 缩小 + 低不透明度
animationFrame = 3 → 全部缩小 + 低不透明度(短暂"休息"帧)
为什么不用 animateTo 或 animator?
- 简单可靠:ArkTS 的
@State+setInterval方案没有复杂的学习曲线,逻辑直观。 - 完全可控:每一帧的状态都可以精确控制,不像 Property Animation 存在中间插值。
- 性能友好:只变更尺寸和透明度,不涉及布局重排,ArkUI 可以高效地执行属性更新。
4.3 与外围动画的配合
除了气泡,还有背景圆环动画:
Circle()
.width(126 + this.animationFrame * 6)
.height(126 + this.animationFrame * 6)
.fill('#EAF8F0')
.opacity(0.72)
中央百分比数字下方的圆环会随 animationFrame 递增而周期性扩大(每次 +6),产生"呼吸"效果,与气泡形成视觉层次感。
五、步骤条:StepRow
5.1 实现代码
const WAITING_STEPS: string[] = [
'看看画里有什么',
'想一想怎么动',
'画出动画片段',
'保存到我的作品'
];
@Builder
private StepRow(step: string, index: number) {
Row() {
Text((index + 1).toString())
.fontSize(12).fontWeight(FontWeight.Bold)
.fontColor(this.activeStep >= index ? '#FFFFFF' : '#8A8FA4')
.width(30).height(30)
.backgroundColor(this.activeStep >= index ? this.mint : '#ECECF6')
.borderRadius(15)
Text(step).fontSize(13)
.fontColor(this.activeStep >= index ? this.ink : '#8A8FA4')
.layoutWeight(1).margin({ left: 12 })
Text(this.activeStep > index ? '完成' : (this.activeStep === index ? '进行中' : '等待'))
.fontSize(11)
.fontColor(this.activeStep >= index ? this.mint : '#9AA0B5')
}
.width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(14).margin({ top: 10 })
}
5.2 三种状态设计
步骤条的每个步骤有三种视觉状态,由 activeStep 与 index 的比较决定:
| 条件 | 状态 | 圆形编号 | 步骤文字 | 右侧标签 |
|---|---|---|---|---|
activeStep > index |
已完成 | 绿色底 + 白字 | 深色 | “完成” + 绿色文字 |
activeStep === index |
进行中 | 绿色底 + 白字 | 深色 | “进行中” + 绿色文字 |
activeStep < index |
等待中 | 灰色底 + 灰字 | 灰色 | “等待” + 灰色文字 |
5.3 进度与步骤的映射
this.activeStep = Math.min(3, Math.floor(this.progress / 28));
每个步骤覆盖约 28% 的进度区间:
| 进度范围 | activeStep | 步骤 |
|---|---|---|
| 0 - 27 | 0 | 看看画里有什么 |
| 28 - 55 | 1 | 想一想怎么动 |
| 56 - 83 | 2 | 画出动画片段 |
| 84 - 100 | 3 | 保存到我的作品 |
这样用户的进度感知从"一个模糊的百分比"变成了"四个清晰的阶段",体验更加透明。
六、页面中央大进度指示
Stack() {
Circle()
.width(168).height(168).fill('#FFF0D6')
Circle()
.width(126 + this.animationFrame * 6).height(126 + this.animationFrame * 6)
.fill('#EAF8F0').opacity(0.72)
Column() {
Text(this.progress.toString() + '%')
.fontSize(32).fontWeight(FontWeight.Bold).fontColor(this.brandPurple)
Text(this.completed ? '完成啦' : '制作中')
.fontSize(13).fontWeight(FontWeight.Bold).fontColor(this.mint).margin({ top: 4 })
}
.alignItems(HorizontalAlign.Center)
}
视觉层次(从底到顶):
- 底层圆(
#FFF0D6暖黄):固定大小 168×168,作为衬底 - 中层圆(
#EAF8F0浅绿):随animationFrame周期性缩放(126-144),半透明,产生呼吸感 - 顶层文字:大号百分比数字 + 状态文字
七、生命周期管理
aboutToAppear() {
// ... 获取 Router 参数 ...
this.startWaitingTimer();
this.startGeneration();
}
aboutToDisappear() {
if (this.timerId >= 0) {
clearInterval(this.timerId);
this.timerId = -1;
}
}
为什么必须清理定时器?
如果不清理,会出现以下问题:
- 内存泄漏:定时器的回调持有组件引用,阻止 GC 回收。
- 状态泄漏:离开页面后定时器仍在运行,试图更新已销毁组件的 @State,导致 ArkUI 警告或异常。
- 重复定时器:用户反复进入该页面会创建多个定时器实例,进度更新速度翻倍。
最佳实践:
- 在
aboutToAppear中创建定时器 - 在
aboutToDisappear中清理定时器 - 使用
timerId追踪定时器状态,创建前先清理旧实例
八、底部按钮的状态联动
Button(this.failed ? '重试生成' : (this.completed ? '查看视频结果' : '正在做动画...'))
.width('90%')
.height(46)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.backgroundColor((this.completed || this.failed) ? this.brandPurple : '#A9A0D8')
.borderRadius(23)
.margin({ top: 20 })
.onClick(() => {
if (this.failed) {
// 重置所有状态,重新生成
this.progress = 12;
this.activeStep = 0;
this.animationFrame = 0;
this.waitingTip = WAITING_TIPS[0];
this.startWaitingTimer();
this.startGeneration();
} else if (this.completed) {
// 跳转到结果页
this.getUIContext().getRouter().pushUrl({
url: 'pages/RecognitionResultPage',
params: { ... }
});
}
})
按钮的文本和颜色均与状态联动:
| 状态 | 按钮文字 | 按钮颜色 | 点击行为 |
|---|---|---|---|
| 进行中 | “正在做动画…” | 浅紫 #A9A0D8 |
不可点击 |
| 完成 | “查看视频结果” | 品牌紫 #7657F3 |
跳转结果页 |
| 失败 | “重试生成” | 品牌紫 #7657F3 |
重置并重新生成 |
这种设计让用户在任何状态下都知道"现在该做什么"。
九、状态与 UI 联动全景
┌─────────────────┐
│ setInterval │
│ 每 1200ms 触发 │
└────────┬────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│progress+3│ │frame+1%4 │ │activeStep│
│ (或+1) │ │ │ │ =p/28 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Progress │ │Bubbles │ │ StepRow │
│ 百分比显示│ │大小/透明度│ │三种状态 │
│ Linear │ │三色圆圈 │ │完成/进行/│
│ │ │呼吸动画 │ │等待 │
└──────────┘ └──────────┘ └──────────┘
三层进度反馈同时更新,但各自关注不同的状态变量,互不干扰。这体现了 ArkUI 响应式编程的优雅之处。
十、总结
本文深入分析了 RecognitionWaitingPage 中三层进度反馈机制的实现:
| 反馈层 | 组件 | 驱动方式 | 视觉效果 |
|---|---|---|---|
| 进度条 | Progress |
progress 状态 |
线性进度百分比 |
| 帧动画 | Circle × 3 |
animationFrame 状态 |
气泡跳动 + 圆环呼吸 |
| 步骤条 | 自定义 @Builder |
activeStep 状态 |
完成/进行中/等待三态 |
设计原则:
- 让等待有信息量:用户不仅知道"要等多久",还知道"等的是什么"。
- 动画给予安全感:动态的 UI 让用户确信应用仍在工作。
- 状态透明可预期:步骤条和百分比让进度可量化,减少焦虑。
- 生命周期安全:及时清理定时器,防止内存泄漏和状态异常。
对于儿童应用而言,这层等待体验的设计甚至比 AI 生成本身更重要——它直接决定了用户会不会在过程中放弃。
动手挑战:尝试在
LoadingBubbles中加入第四种颜色圆圈,或将气泡动画改为沿弧形轨迹运动。也可以尝试使用animateTo隐式动画替代帧切换,体验两种方案的差异。
更多推荐


所有评论(0)