第2.6篇:动画与进度反馈——Progress 与帧动画

难度:⭐⭐ 进阶 | 前置知识:1.3 @State 与状态管理 | 涉及源文件products/default/src/main/ets/pages/RecognitionWaitingPage.ets


一、引言

当用户完成了涂鸦或照片采集,点击"生成动画"后,应用进入 RecognitionWaitingPage。在这个页面上,AI 正在后台处理图像、识别内容、生成视频帧。整个过程可能持续数秒到数十秒。

对儿童用户而言,一个静止的"加载中…"页面是不可接受的。没有视觉反馈,小朋友会认为应用卡住了,进而反复点击或关闭应用。

"画伴梦工厂"的等待页面通过三层进度反馈机制来解决这个问题:

  1. Progress 进度条 —— 展示整体进度百分比
  2. 帧动画气泡 —— 动态变化的 Circle 组件产生"跳动"感
  3. 步骤条 —— 分阶段展示任务进度,让用户知道"现在在做什么"

本文将深入分析这三层机制的设计与实现。


二、状态设计

@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);
}

进度条设计考量

  1. 起始值 12:不让用户看到"从 0 开始"的假象,让等待显得已经进行了一段时间。
  2. 分段速度progress < 92 时每次 +3,>= 92 时每次 +1。前端进度"慢下来"营造接近完成的真实感。
  3. 上限 97:预留最后的 3% 给真正的完成回调,不会出现"99% 卡住"的尴尬。
  4. 1200ms 间隔:约 1.2 秒更新一次,节奏舒缓不急促。

3.3 完成与失败的处理

当 AI 生成完成或失败时:

// 成功
this.progress = 100;
this.completed = true;
this.statusText = '视频已生成并保存到作品';

// 失败
this.failed = true;
this.statusText = '生成失败:' + getErrorMessage(error);

定时器会检测 failedcompleted 状态,及时 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 三种状态设计

步骤条的每个步骤有三种视觉状态,由 activeStepindex 的比较决定:

条件 状态 圆形编号 步骤文字 右侧标签
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)
}

视觉层次(从底到顶):

  1. 底层圆#FFF0D6 暖黄):固定大小 168×168,作为衬底
  2. 中层圆#EAF8F0 浅绿):随 animationFrame 周期性缩放(126-144),半透明,产生呼吸感
  3. 顶层文字:大号百分比数字 + 状态文字

七、生命周期管理

aboutToAppear() {
  // ... 获取 Router 参数 ...
  this.startWaitingTimer();
  this.startGeneration();
}

aboutToDisappear() {
  if (this.timerId >= 0) {
    clearInterval(this.timerId);
    this.timerId = -1;
  }
}

为什么必须清理定时器

如果不清理,会出现以下问题:

  1. 内存泄漏:定时器的回调持有组件引用,阻止 GC 回收。
  2. 状态泄漏:离开页面后定时器仍在运行,试图更新已销毁组件的 @State,导致 ArkUI 警告或异常。
  3. 重复定时器:用户反复进入该页面会创建多个定时器实例,进度更新速度翻倍。

最佳实践

  • 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 状态 完成/进行中/等待三态

设计原则

  1. 让等待有信息量:用户不仅知道"要等多久",还知道"等的是什么"。
  2. 动画给予安全感:动态的 UI 让用户确信应用仍在工作。
  3. 状态透明可预期:步骤条和百分比让进度可量化,减少焦虑。
  4. 生命周期安全:及时清理定时器,防止内存泄漏和状态异常。

对于儿童应用而言,这层等待体验的设计甚至比 AI 生成本身更重要——它直接决定了用户会不会在过程中放弃。


动手挑战:尝试在 LoadingBubbles 中加入第四种颜色圆圈,或将气泡动画改为沿弧形轨迹运动。也可以尝试使用 animateTo 隐式动画替代帧切换,体验两种方案的差异。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐