茶器艺科智造HarmonyOS应用实战-59-animate从true切成false,星尘定时器为何不会停:让ArkUI Prop变化真正控制Canvas生命周期
茶器艺科智造HarmonyOS应用实战-59-animate从true切成false,星尘定时器为何不会停:让ArkUI Prop变化真正控制Canvas生命周期
ChaqiStarfieldCanvas 每 40ms 绘制 90 个粒子。animate 虽然是外部传入的 Prop,却只在启动时被读取,没有 Watch;因此页面仍可见时把它从 true 切到 false,已经创建的 interval 不会因为字段值变化而自动停止。

本文以 Gitee 仓库 aiding521/chaqi-app-gitee 的 master 分支、提交 f671fcd8de277973bd7518a3c462f68384575f1e 的静态源码为事实基线。本文没有修改项目源码,也没有执行 HAP 构建、模拟器或真机验证;建议代码属于演进方案,不能描述成已经落地的功能。
一、Prop 变了,不代表 interval 会重新求值
静态源码显示,组件声明了 animate,并用 interval 周期绘制:
@Prop animate: boolean = true;
this.timerId = setInterval(() => this.drawFrame(), 40) as number;
创建 interval 的那一刻,框架只获得一个重复执行 drawFrame() 的回调。之后父组件更新 animate,字段值会变,但已注册的回调和定时器句柄不会凭空消失。若回调内部也没有再次判断开关,粒子绘制就继续运行。
这不是在断言设备已经出现耗电或掉帧,而是源码级控制流缺口:业务开关与运行时资源之间没有持续同步机制。性能影响需要 Profiler 或设备数据,不能由“40ms、90 粒子”直接推导出具体数值。
二、动画能否运行,至少取决于三道门
一个可控的 Canvas 循环不能只看 animate。还要知道画布是否可见,以及宽高是否已经可用:

| 条件 | 为 false 时 | 变为 true 时 |
|---|---|---|
animate | 停止现有循环 | 再检查可见性和尺寸 |
canvasVisible | 停止现有循环 | 再检查业务开关和尺寸 |
wPx > 0 && hPx > 0 | 不应启动绘制 | 在另外两门开放时启动 |
这三个条件共同决定“此刻是否应该有且仅有一个 interval”。把判断散在 onAppear、尺寸回调和父组件赋值中,很容易出现某条路径只会启动不会停止,或重复进入时创建多个句柄。更稳妥的做法是所有入口都调用同一个同步函数。
三、Watch 负责把 Prop 变化接到资源控制上
建议方案为 animate 增加 Watch,并在回调里统一决定启停:
@Prop @Watch('onAnimateChanged') animate: boolean = true;
private canvasVisible: boolean = false;
private onAnimateChanged(): void {
if (!this.animate || !this.canvasVisible) { this.stopLoop(); return; }
if (this.wPx > 0 && this.hPx > 0) this.startLoop();
}
这里虽然叫 onAnimateChanged,实质上更像“同步期望状态”。它不只在 Prop 改变时调用,也可以在 Canvas 出现、尺寸就绪后调用。函数读取最新的三道门,再落到 startLoop() 或 stopLoop(),避免每个生命周期分别复制一套条件。
startLoop() 还必须幂等:已有有效句柄时直接返回。stopLoop() 同样要在清理后把句柄恢复到明确的“未运行”状态。否则连续收到相同 Prop 或多次 onAppear 时,即使有 Watch,也可能叠加多个 interval。
四、Canvas 可见性要与 Prop 同步走同一条路径
Canvas 出现和消失时更新可见标记,再调用同一个同步逻辑:
Canvas(this.ctx)
.onAppear(() => { this.canvasVisible = true; this.onAnimateChanged(); })
.onDisAppear(() => { this.canvasVisible = false; this.stopLoop(); })
onDisAppear 直接停止循环,是因为不可见已经足以否决运行;不必等待父组件把 animate 改为 false。再次出现时也不应无条件启动,而是重新检查当前 Prop 和尺寸。这样“页面可见但业务暂停”“业务允许但组件不可见”“画布刚出现但尺寸未就绪”三种状态都不会被混为一谈。
还应核对组件层与页面层生命周期的实际调用关系。本文代码是演进示意,未在模拟器或真机证明每次导航都按期望触发;最终要用日志验证出现、消失、Prop 更新和句柄变化的顺序。
五、把启停写成四状态转换更容易评审
| 当前状态 | 事件 | 条件 | 动作 | 目标状态 |
|---|---|---|---|---|
| stopped | animate=true | 可见且尺寸有效 | 创建一个 interval | running |
| stopped | Canvas appear | animate 且尺寸有效 | 创建一个 interval | running |
| running | animate=false | 无需其他条件 | clearInterval | stopped |
| running | Canvas disappear | 无需其他条件 | clearInterval | stopped |
| running | 尺寸变化 | 三门仍开放 | 保持单一循环,后续帧读新尺寸 | running |
| stopped | 尺寸就绪 | animate 且可见 | 创建一个 interval | running |
| 任意 | 重复同值事件 | 期望状态未变 | 不重复创建或清理 | 原状态 |
状态表的关键不是事件多少,而是任意路径最后都满足不变量:running 时只有一个有效句柄,stopped 时没有句柄。若 drawFrame() 还依赖粒子数组初始化,则初始化就绪也应成为经源码核对后的前置条件,而不是由本篇凭空假设。
六、测试重点是事件排列,不是单个布尔值
原有边界用例骨架仍可用来描述输入和预期:
interface BoundaryCase<T> {
name: string;
input: T;
expected: string;
}
const cases: BoundaryCase<string>[] = [
{ name: 'empty', input: '', expected: 'reject' },
{ name: 'normal', input: 'valid', expected: 'accept' },
{ name: 'repeat', input: 'valid', expected: 'idempotent' }
];
针对动画控制,应把测试输入换成事件序列,例如 appear→size ready→true、running→false、false→disappear→appear、true→true→true。断言对象不是“函数调用过”,而是活动句柄数量、绘制次数是否继续增长,以及停止后是否不再进入新一帧。
纯逻辑测试可以覆盖三道门的决策函数和 start/stop 幂等性。Canvas 是否真的停止绘制、定时器在前后台如何被调度、设备功耗是否变化,必须由模拟器或真机证据回答。
七、timerId 的所有权必须留在创建它的组件
资源清理应与创建成对,且不能依赖外部“顺手”把 Prop 复位:
private disposeOwnedResources(): void {
// timer/listener只由创建它的组件清理
// 异步结果写状态前核对taskId或pageGeneration
// 可恢复任务先保存业务事实,再释放进程句柄
}
本篇最重要的是第一行:谁调用 setInterval,谁保存并清理 timerId。页面退出、组件消失和业务开关关闭都可以触发清理,但最终都落到同一个 stopLoop()。timerId 是进程内资源,不属于需要持久化的业务事实,也不应跨组件共享。
若 interval 回调可能已经进入执行,stopLoop() 只能阻止后续调度,不能让正在运行的函数倒退。绘制函数应读取当前尺寸与可见状态,必要时在入口快速返回;是否需要代次防护,要根据实际异步边界判断。
八、验证时要同时看句柄和绘制计数
- 初次出现且
animate=true:尺寸就绪后只创建一个 interval。 - 运行中改为
false:句柄清空,之后绘制计数不再增长。 - 保持可见再改回
true:重新创建一个且只有一个 interval。 - 连续写入多次
true:不会叠加循环。 - 运行中离开页面:即使 Prop 仍为 true,也停止循环。
- 在不可见时切换 Prop:不会提前启动;重新出现后按最新值决策。
- 宽高为零时不启动,尺寸有效后再同步一次状态。
日志建议记录事件名、animate、canvasVisible、宽高、句柄是否存在和累计帧数,不记录每个粒子的完整坐标,以免调试日志本身干扰性能测量。
九、四种“看起来能停”的修补并不完整
| 误修 | 为什么不够 | 正确方向 |
|---|---|---|
| 回调里每帧判断 animate,但不清 timer | 停止绘制却仍持续唤醒回调 | 关闭时清理 interval |
| 只在页面销毁时清理 | 页面可见且 animate=false 时仍运行 | Watch 连接 Prop 与 stopLoop |
| 每次 true 都 setInterval | 重复赋值可能叠加句柄 | startLoop 幂等 |
| 用更大的间隔降低影响 | 没有修复控制关系 | 先保证启停正确,再谈频率 |
| 只看画面是否静止 | 无法确认后台回调已停止 | 观察句柄与帧计数 |
因此“星星不动了”不是充分证据。可能只是 drawFrame 早退、画布被遮挡或粒子变化不明显;只有句柄清理和调用计数一起记录,才能说明循环生命周期真正收口。
十、动画决策与绘制实现应保持分层

项目中 entry 负责产品入口,libraryhsp 负责页面和交互,libraryhar 负责模型、算法、路由与通用服务。持有 Canvas context、ArkUI 状态和 timerId 的逻辑应留在组件侧;若要提取纯函数,可以只提取“给定三道门,期望 running 还是 stopped”的判断,避免共享层反向依赖 UI 生命周期。
entry / libraryhsp -> 页面、组件、生命周期
libraryhar -> 纯类型、纯函数、可持久化模型
tests -> 边界、幂等、恢复序列
runtime evidence -> 日志、设备行为、服务读回
粒子计算是否值得继续拆分,需要结合现有源码和性能证据。本文问题仅是 Prop 变化没有控制 interval,不借此声称缓存、离屏绘制或其他优化一定有效。
十一、当前证据只到静态控制流
baseline: f671fcd8de277973bd7518a3c462f68384575f1e
scope: source-level proposal
build: not run
simulator/device: not run
network/account: not run
result: static boundary identified; implementation pending
从基线可确认的是 40ms interval、90 个粒子、animate Prop 与缺少 Watch 的关系。尚未执行构建,也没有模拟器、真机、帧率、CPU 或功耗数据。后续实现若通过编译,仍只能说明 API 与类型可接受,不能等同于生命周期在设备上已验证。
十二、用一条往返序列证明开关真的可逆
最小回归应走完整往返:组件出现、尺寸就绪、开始绘制;保持页面可见,把 animate 从 true 改成 false;等待超过若干个 40ms 周期,确认帧数不再增加;再改回 true,确认只有一个循环恢复。之后离开并返回,重复检查。
| 场景 | 操作序列 | 必须观察的证据 | 不能替代的证据 |
|---|---|---|---|
| 初次启动 | appear→size ready→true | 一个句柄、帧数增长 | 星尘可见截图 |
| 业务暂停 | running→false | 句柄清空、帧数停止 | 画面看似静止 |
| 再次启动 | stopped→true | 新建一个句柄 | 帧数偶尔变化 |
| 重复事件 | true→true→appear | 仍只有一个句柄 | 没有立即崩溃 |
| 离开页面 | running→disappear | 句柄清空 | 页面已不可见 |
| 零尺寸 | appear→0x0→true | 不启动,尺寸就绪后再启 | 没有绘制异常 |
若需要把一次启停过程加入统一追踪,可复用操作轨迹结构:
interface OperationTrace {
taskId: string;
generation: number;
startedAtMs: number;
finishedAtMs?: number;
terminalSource?: 'ui' | 'service' | 'restore' | 'cancel';
}
function canApplyResult(trace: OperationTrace, currentGeneration: number): boolean {
return trace.generation === currentGeneration &&
trace.finishedAtMs === undefined;
}
对本篇更直接的字段可以是组件实例标识、start/stop 次数和当前句柄状态。引入 generation 只有在确有迟到异步结果时才有价值,不应为了套模板增加无用状态。
最终收口标准不是“给 Prop 加了 Watch”这一行,而是三道门始终通过同一决策路径控制唯一 interval,任何关闭条件都能停止,恢复时也不会叠加。本文仍是基于固定提交的建议,运行结果待后续补证据。
更多推荐


所有评论(0)