HarmonyOS应用实战-启示散页-06-仪式感动画不要卡业务:用 DrawingPage 隔离抽取和转场

HarmonyOS应用实战-启示散页-06-仪式感动画不要卡业务:用 DrawingPage 隔离抽取和转场

答案之书需要一点仪式感:提问、翻动、停顿、出现答案。但动画一旦和业务抽取绑在一起,就很容易出问题。动画还没结束,结果已经写历史;用户返回了,定时器还在跑;抽取失败了,页面仍播放到最终态。更稳的做法是把 DrawingPage 当成状态机,把业务结果和转场节奏隔开。

这篇文章会把问题拆成四个可落地的点:

  1. 用 DrawingPage 承接抽取流程,不让首页直接处理动画。
  2. 业务先拿结果,动画按状态展示,不反向决定数据。
  3. 处理定时器清理、返回中断和失败态。
  4. 让历史、收藏和页面显示都基于同一个抽取结果。

1. 动画不能反过来驱动业务

很多页面会写成"动画结束后再随机抽答案"。这会让业务结果依赖 UI 时间:动画取消、页面返回、定时器丢失都会影响抽取。答案之书更稳的路径是先由 Service 得到结果,再让 DrawingPage 根据结果播放揭示动画。

ts 复制代码
private async begin(): Promise<void> {
  this.phase = 'drawing';
  try {
    const result: DrawAnswerResult = await AnswerDrawService.draw({
      deckId: this.deckId,
      question: this.question
    });
    this.result = result;
    this.phase = 'revealing';
    this.startRevealTimer();
  } catch (err) {
    this.phase = 'failed';
  }
}

业务完成后才进入 revealing。动画可以慢一点,但不能决定答案是否存在。

2. 页面状态机要少而清楚

DrawingPage 至少有 idle、drawing、revealing、done、failed 五种状态。不要用多个 boolean 拼状态,例如 loading && showCard && hasError,后期很难判断组合是否合法。

ts 复制代码
type DrawingPhase = 'idle' | 'drawing' | 'revealing' | 'done' | 'failed';

@State private phase: DrawingPhase = 'idle';
@State private result: DrawAnswerResult | null = null;
@State private revealProgress: number = 0;

单一 phase 能让 UI 分支和调试日志都更直接。出现异常时,先看 phase 停在哪一步。

3. 首页只传输入,不参与抽取细节

首页收集问题和当前题库 id,然后跳转到 DrawingPage。它不应该提前随机,也不应该写历史。这样首页可以保持轻,抽取页成为完整流程的唯一拥有者。

ts 复制代码
this.pathStack.pushPath({
  name: RouteName.Drawing,
  param: {
    deckId: this.currentDeckId,
    question: this.question.trim()
  } as DrawingParam
});

首页传的是上下文,不是结果。DrawingPage 才负责拿结果、展示结果和交给历史服务。

4. 参数进入页面先做兜底

导航参数可能为空,特别是调试、深链或后续扩展入口。DrawingPage 在 aboutToAppear 中解析参数,缺失时给出错误态,不要让空 deckId 进入 Service 后再出现难懂错误。

ts 复制代码
interface DrawingParam {
  deckId: string;
  question?: string;
}

private readParam(): DrawingParam | null {
  const param = this.pathStack.getParamByName(RouteName.Drawing) as DrawingParam | undefined;
  if (!param?.deckId) {
    return null;
  }
  return param;
}

参数校验是页面入口边界。Service 仍会校验,但页面可以更早给用户可理解的反馈。

5. 定时器必须跟页面生命周期绑定

动画通常需要 setTimeoutsetInterval。如果页面返回后不清理,定时器回调还会修改已离开的页面状态,轻则日志异常,重则状态错乱。答案之书需要在离开页面时清理所有动画句柄。

ts 复制代码
private revealTimer: number = -1;

aboutToDisappear(): void {
  if (this.revealTimer !== -1) {
    clearInterval(this.revealTimer);
    this.revealTimer = -1;
  }
}

动画资源属于页面生命周期。页面消失后,不再允许它继续推进状态。

6. 历史写入放在结果确认点

历史记录应该写在抽取结果生成后,而不是动画彻底结束后。用户中途返回也代表这次抽取发生过;如果产品希望只记录看完的答案,那也要显式定义。答案之书更适合在拿到结果后记录历史。

ts 复制代码
const result: DrawAnswerResult = await AnswerDrawService.draw(options);
await HistoryService.appendFromDraw(result);
this.result = result;
this.phase = 'revealing';

历史的来源是领域结果,不是动画帧。这样历史列表和收藏按钮都能使用同一个 result。

7. 失败态要停住动画

抽取失败时继续播放动画,会让用户等到最后才看到空白。DrawingPage 应该在 Service 抛错后直接进入 failed,并提供重试或返回。错误提示也应该区分"题库不存在"和"题库没有答案"。

ts 复制代码
@Builder FailedPanel() {
  Column({ space: 12 }) {
    Text(this.errorMessage || '暂时无法抽取答案')
    Button('返回题库选择').onClick(() => this.pathStack.pop())
  }
}

失败态是流程的一部分,不是兜底文案。只要业务可能失败,UI 就要给出明确出口。

8. 动画参数不要散落在 UI 里

转场时长、进度步长、结果停顿时间最好集中成常量。这样后续调节仪式感时,不会误改抽取逻辑。动画常量也能让真机调优更有依据。

ts 复制代码
const REVEAL_TOTAL_MS: number = 1200;
const REVEAL_TICK_MS: number = 40;

private startRevealTimer(): void {
  const startedAt: number = Date.now();
  this.revealTimer = setInterval(() => {
    this.revealProgress = Math.min(1, (Date.now() - startedAt) / REVEAL_TOTAL_MS);
    if (this.revealProgress >= 1) {
      this.phase = 'done';
      clearInterval(this.revealTimer);
    }
  }, REVEAL_TICK_MS);
}

动画参数集中后,业务代码不需要因为"想慢一点"而被改动。

9. 验证与排障

DrawingPage 的验证要覆盖时间和中断:快速返回、连续进入、抽取失败、动画未结束时收藏、切换题库后再抽。尤其要看定时器是否清理,历史是否只写一次。

text 复制代码
验证点:
1. 首页只负责 push DrawingPage
2. DrawingPage 进入后先拿 DrawAnswerResult
3. 返回页面后没有定时器继续打印日志
4. 抽取失败时 phase=failed,不播放到 done
5. 历史记录不因动画重复触发而写两次

如果页面偶发卡在 loading,优先看 phase 切换和 Service 异常处理,而不是只调动画时长。

还有一个需要在评审时说清楚的边界:返回不是业务失败。只要 Service 已经给出结果并写入历史,用户中途离开只代表不再观看本次动画。相反,如果 Service 还没有返回结果就离开页面,页面要清理定时器和挂起状态,不能在后台继续推进 UI。把这个边界写进状态机,后续调动画节奏时就不会误伤数据链路。

如果产品后来增加"再抽一次",也应该从 done 状态重新进入 drawing,而不是复用上一轮 result 改文案。每一次抽取都有独立结果、独立历史写入和独立动画周期,这样连续操作才不会把两次答案混在同一个页面状态里。

验证清单

  • 清应用数据后从冷启动进入,确认默认数据、页面状态和日志分支符合预期。
  • 对本文涉及的写路径准备正常、空值、重复、越界四类输入,确认错误停在 Service 或 Repository。
  • 页面返回、重新进入、切换题库、收藏、历史或删除后,确认对应刷新信号触发重新读取。
  • 修改资源或模块归属后重新构建,确认 HAP、HAR、HSP 的依赖方向没有反转。
  • 涉及真机体验、备份恢复、发布素材的内容,单独记录是否已经在设备或平台侧验证。

常见问题与处理

现象 先看哪里 处理方式
返回后仍有动画日志 aboutToDisappear 是否清理 timer 记录并清理所有句柄
抽取失败仍显示翻牌 失败分支是否进入 failed Service 抛错后停止动画
历史重复写入 是否在多个动画回调里 append 只在拿到 DrawAnswerResult 后写一次
首页越来越复杂 首页是否承担抽取和动画 首页只传 DrawingParam

小结

仪式感应该增强体验,不能接管业务。DrawingPage 用状态机隔离抽取结果和动画节奏,既能保留氛围,也能让数据链路可控。

相关推荐
qizayaoshuap7 小时前
# [特殊字符] 密码生成器 — 鸿蒙ArkTS安全算法与密码强度评估系统
java·算法·安全·华为·harmonyos
●VON8 小时前
鸿蒙 PC Markdown 编辑器自动化测试:Playwright、ohosTest 与构建门禁
华为·编辑器·harmonyos·鸿蒙
AD02279 小时前
HarmonyOS应用实战-启示散页-08-提问历史别无限长:用去重、截断和 Sheet 入口做轻量记忆
harmonyos·arkts·鸿蒙开发
世人万千丶9 小时前
鸿蒙Flutter Flex嵌套布局技巧
学习·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
绝世番茄9 小时前
HarmonyOS NEXT 实战:SideBar + Navigation 侧边导航布局完全指南
华为·harmonyos·鸿蒙
xd18557855510 小时前
家电选购参谋 —— 鸿蒙AI智能助手开发全流程解析
人工智能·华为·harmonyos·鸿蒙
FrameNotWork10 小时前
HarmonyOS 6.0 相机开发——拍照与录像
数码相机·华为·harmonyos
FrameNotWork10 小时前
HarmonyOS 6.0 自定义TabBar——从基础到动效
华为·harmonyos
世人万千丶10 小时前
鸿蒙Flutter Flex布局性能优化
学习·flutter·性能优化·harmonyos·鸿蒙·鸿蒙系统