第2.6篇:动画与进度反馈——Progress 与帧动画
难度:⭐⭐ 进阶 |前置知识:1.3 @State 与状态管理 |涉及源文件:
products/default/src/main/ets/pages/RecognitionWaitingPage.ets
一、引言
当用户完成了涂鸦或照片采集,点击"生成动画"后,应用进入RecognitionWaitingPage。在这个页面上,AI 正在后台处理图像、识别内容、生成视频帧。整个过程可能持续数秒到数十秒。
对儿童用户而言,一个静止的"加载中…"页面是不可接受的。没有视觉反馈,小朋友会认为应用卡住了,进而反复点击或关闭应用。
"画伴梦工厂"的等待页面通过三层进度反馈机制来解决这个问题:
- Progress 进度条—— 展示整体进度百分比
- 帧动画气泡—— 动态变化的 Circle 组件产生"跳动"感
- 步骤条—— 分阶段展示任务进度,让用户知道"现在在做什么"
本文将深入分析这三层机制的设计与实现。
二、状态设计
@Stateprivateprogress:number=12;@StateprivateactiveStep:number=0;@StateprivatestatusText:string='正在准备生成任务';@Stateprivatefailed:boolean=false;@Stateprivatecompleted:boolean=false;@StateprivateanimationFrame:number=0;@StateprivatewaitingTip: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 生成进度:
privatestartWaitingTimer(){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 实现代码
@BuilderprivateLoadingBubbles(){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 实现代码
constWAITING_STEPS:string[]=['看看画里有什么','想一想怎么动','画出动画片段','保存到我的作品'];@BuilderprivateStepRow(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();}elseif(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隐式动画替代帧切换,体验两种方案的差异。