HarmonyOS应用实战-启示散页-06-仪式感动画不要卡业务:用 DrawingPage 隔离抽取和转场
HarmonyOS应用实战-启示散页-06-仪式感动画不要卡业务用 DrawingPage 隔离抽取和转场答案之书需要一点仪式感提问、翻动、停顿、出现答案。但动画一旦和业务抽取绑在一起就很容易出问题。动画还没结束结果已经写历史用户返回了定时器还在跑抽取失败了页面仍播放到最终态。更稳的做法是把 DrawingPage 当成状态机把业务结果和转场节奏隔开。这篇文章会把问题拆成四个可落地的点用 DrawingPage 承接抽取流程不让首页直接处理动画。业务先拿结果动画按状态展示不反向决定数据。处理定时器清理、返回中断和失败态。让历史、收藏和页面显示都基于同一个抽取结果。1. 动画不能反过来驱动业务很多页面会写成“动画结束后再随机抽答案”。这会让业务结果依赖 UI 时间动画取消、页面返回、定时器丢失都会影响抽取。答案之书更稳的路径是先由 Service 得到结果再让 DrawingPage 根据结果播放揭示动画。privateasyncbegin():Promisevoid{this.phasedrawing;try{constresult:DrawAnswerResultawaitAnswerDrawService.draw({deckId:this.deckId,question:this.question});this.resultresult;this.phaserevealing;this.startRevealTimer();}catch(err){this.phasefailed;}}业务完成后才进入 revealing。动画可以慢一点但不能决定答案是否存在。2. 页面状态机要少而清楚DrawingPage 至少有 idle、drawing、revealing、done、failed 五种状态。不要用多个 boolean 拼状态例如loading showCard hasError后期很难判断组合是否合法。typeDrawingPhaseidle|drawing|revealing|done|failed;Stateprivatephase:DrawingPhaseidle;Stateprivateresult:DrawAnswerResult|nullnull;StateprivaterevealProgress:number0;单一 phase 能让 UI 分支和调试日志都更直接。出现异常时先看 phase 停在哪一步。3. 首页只传输入不参与抽取细节首页收集问题和当前题库 id然后跳转到 DrawingPage。它不应该提前随机也不应该写历史。这样首页可以保持轻抽取页成为完整流程的唯一拥有者。this.pathStack.pushPath({name:RouteName.Drawing,param:{deckId:this.currentDeckId,question:this.question.trim()}asDrawingParam});首页传的是上下文不是结果。DrawingPage 才负责拿结果、展示结果和交给历史服务。4. 参数进入页面先做兜底导航参数可能为空特别是调试、深链或后续扩展入口。DrawingPage 在aboutToAppear中解析参数缺失时给出错误态不要让空 deckId 进入 Service 后再出现难懂错误。interfaceDrawingParam{deckId:string;question?:string;}privatereadParam():DrawingParam|null{constparamthis.pathStack.getParamByName(RouteName.Drawing)asDrawingParam|undefined;if(!param?.deckId){returnnull;}returnparam;}参数校验是页面入口边界。Service 仍会校验但页面可以更早给用户可理解的反馈。5. 定时器必须跟页面生命周期绑定动画通常需要setTimeout或setInterval。如果页面返回后不清理定时器回调还会修改已离开的页面状态轻则日志异常重则状态错乱。答案之书需要在离开页面时清理所有动画句柄。privaterevealTimer:number-1;aboutToDisappear():void{if(this.revealTimer!-1){clearInterval(this.revealTimer);this.revealTimer-1;}}动画资源属于页面生命周期。页面消失后不再允许它继续推进状态。6. 历史写入放在结果确认点历史记录应该写在抽取结果生成后而不是动画彻底结束后。用户中途返回也代表这次抽取发生过如果产品希望只记录看完的答案那也要显式定义。答案之书更适合在拿到结果后记录历史。constresult:DrawAnswerResultawaitAnswerDrawService.draw(options);awaitHistoryService.appendFromDraw(result);this.resultresult;this.phaserevealing;历史的来源是领域结果不是动画帧。这样历史列表和收藏按钮都能使用同一个 result。7. 失败态要停住动画抽取失败时继续播放动画会让用户等到最后才看到空白。DrawingPage 应该在 Service 抛错后直接进入 failed并提供重试或返回。错误提示也应该区分“题库不存在”和“题库没有答案”。BuilderFailedPanel(){Column({space:12}){Text(this.errorMessage||暂时无法抽取答案)Button(返回题库选择).onClick(()this.pathStack.pop())}}失败态是流程的一部分不是兜底文案。只要业务可能失败UI 就要给出明确出口。8. 动画参数不要散落在 UI 里转场时长、进度步长、结果停顿时间最好集中成常量。这样后续调节仪式感时不会误改抽取逻辑。动画常量也能让真机调优更有依据。constREVEAL_TOTAL_MS:number1200;constREVEAL_TICK_MS:number40;privatestartRevealTimer():void{conststartedAt:numberDate.now();this.revealTimersetInterval((){this.revealProgressMath.min(1,(Date.now()-startedAt)/REVEAL_TOTAL_MS);if(this.revealProgress1){this.phasedone;clearInterval(this.revealTimer);}},REVEAL_TICK_MS);}动画参数集中后业务代码不需要因为“想慢一点”而被改动。9. 验证与排障DrawingPage 的验证要覆盖时间和中断快速返回、连续进入、抽取失败、动画未结束时收藏、切换题库后再抽。尤其要看定时器是否清理历史是否只写一次。验证点 1. 首页只负责 push DrawingPage 2. DrawingPage 进入后先拿 DrawAnswerResult 3. 返回页面后没有定时器继续打印日志 4. 抽取失败时 phasefailed不播放到 done 5. 历史记录不因动画重复触发而写两次如果页面偶发卡在 loading优先看 phase 切换和 Service 异常处理而不是只调动画时长。还有一个需要在评审时说清楚的边界返回不是业务失败。只要 Service 已经给出结果并写入历史用户中途离开只代表不再观看本次动画。相反如果 Service 还没有返回结果就离开页面页面要清理定时器和挂起状态不能在后台继续推进 UI。把这个边界写进状态机后续调动画节奏时就不会误伤数据链路。如果产品后来增加“再抽一次”也应该从 done 状态重新进入 drawing而不是复用上一轮 result 改文案。每一次抽取都有独立结果、独立历史写入和独立动画周期这样连续操作才不会把两次答案混在同一个页面状态里。验证清单清应用数据后从冷启动进入确认默认数据、页面状态和日志分支符合预期。对本文涉及的写路径准备正常、空值、重复、越界四类输入确认错误停在 Service 或 Repository。页面返回、重新进入、切换题库、收藏、历史或删除后确认对应刷新信号触发重新读取。修改资源或模块归属后重新构建确认 HAP、HAR、HSP 的依赖方向没有反转。涉及真机体验、备份恢复、发布素材的内容单独记录是否已经在设备或平台侧验证。常见问题与处理现象先看哪里处理方式返回后仍有动画日志aboutToDisappear 是否清理 timer记录并清理所有句柄抽取失败仍显示翻牌失败分支是否进入 failedService 抛错后停止动画历史重复写入是否在多个动画回调里 append只在拿到 DrawAnswerResult 后写一次首页越来越复杂首页是否承担抽取和动画首页只传 DrawingParam小结仪式感应该增强体验不能接管业务。DrawingPage 用状态机隔离抽取结果和动画节奏既能保留氛围也能让数据链路可控。