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 用状态机隔离抽取结果和动画节奏,既能保留氛围,也能让数据链路可控。

相关推荐
黑鲨吃西瓜1 小时前
鸿蒙通用模块
harmonyos·鸿蒙
腾科IT教育8 小时前
HarmonyOS开发|ArkTS UI颜色API通用规则
ui·华为·harmonyos·harmonyos开发·鸿蒙应用开发工程师
风华圆舞12 小时前
HarmonyOS 自定义绘制实战 —— 用 ArkGraphics2D 画一个会卷曲翻动的页面网格
harmonyos·arkts·drawing·drawvertices·翻页卷曲·有限差分法线
风华圆舞14 小时前
HarmonyOS 手势与 animator 实战 —— 捏出跟手又有弹性的翻页物理
harmonyos·手势·pixelmap·pangesture·边界回弹·native 句柄
北墨NoLimit14 小时前
DevEco Code:在终端里用 AI 写鸿蒙应用
harmonyos
智塑未来15 小时前
打开快、切换顺、游戏稳:鸿蒙的日常流畅表现
游戏·华为·harmonyos
智塑未来1 天前
鸿蒙游戏体验手册:四种能力从性能到玩法逐一解锁
游戏·华为·harmonyos
math_hongfan1 天前
鸿蒙离线数据缓存高级架构:弱网预加载/离线数据优先级/同步冲突解决/上线后数据合并策略
学习·缓存·华为·架构·harmonyos·鸿蒙
math_hongfan1 天前
鸿蒙企业级数据存储高级架构:从读写分离到冷热数据分层/归档策略/数据生命周期管理最佳实践
人工智能·学习·华为·架构·harmonyos·鸿蒙
math_hongfan2 天前
鸿蒙存储异常高级排查:文件损坏检测/数据恢复/读写失败重试/磁盘空间预警系统性根治方案
学习·华为·harmonyos·鸿蒙