50 Canvas 动画:@State + @Watch + animateTo 驱动模式 📅 2026/7/11 22:14:08 50 Canvas 动画State Watch animateTo 驱动模式前言图50 Canvas 动画State Watch animateTo 驱动模式 运行效果截图HarmonyOS NEXT在鹿鹿·笔迹心理分析App 的三个 Canvas 组件雷达图、折线图、圆环图中所有动画效果都基于同一个核心模式状态变量驱动 Canvas 重绘。这个模式将 ArkUI 的声明式状态管理State与 Canvas 命令式绘制结合在一起——通过animateTo改变一个drawProgress变量再用Watch监听变化触发draw()重绘。本文系统解析这种动画模式的原理和最佳实践。鸿蒙官方·animateTo 文档developer.huawei.com项目源码[AppAnimations.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppAnimations.ets)图State Watch animateTo 驱动 Canvas 重绘的完整闭环Canvas 重绘draw() 方法Watch onProgressChangeState drawProgressanimateTo组件 aboutToAppearCanvas 重绘draw() 方法Watch onProgressChangeState drawProgressanimateTo组件 aboutToAppear每一帧都重复此循环直到动画结束触发入场动画drawProgress: 0 → 1状态变化通知调用 draw()ctx.clearRect 重新绘制一、动画核心模式1.1 三行代码驱动动画// 第 1 行声明驱动变量StateWatch(onDrawProgressChange)drawProgress:number0// 第 2 行触发动画aboutToAppear():void{animateTo({duration:1000,curve:Curve.FastOutSlowIn},(){this.drawProgress1// 0 → 1 过渡})}// 第 3 行监听变化 → 重绘onDrawProgressChange():void{this.draw()}执行流程aboutToAppear() │ ▼ animateTo({ duration: 1000ms }) │ ├── drawProgress 0.0 → Watch 触发 → draw() ├── drawProgress 0.1 → Watch 触发 → draw() ├── drawProgress 0.2 → Watch 触发 → draw() ├── ... └── drawProgress 1.0 → Watch 触发 → draw()最终状态1.2 工作原理┌─────────────────────────────────────────────────────┐ │ ① animateTo 驱动 State │ │ ├── 产生插值0→1 逐帧过渡 │ │ └── 每帧更新 drawProgress │ │ │ │ ② Watch 监听变化 │ │ ├── detect: drawProgress 变化 │ │ └── call: onDrawProgressChange() → draw() │ │ │ │ ③ draw() 读取 drawProgress │ │ ├── 控制雷达数据多边形的半径比例 │ │ ├── 控制折线图的可见点数 │ │ └── 控制圆环的弧角度 │ └─────────────────────────────────────────────────────┘二、三个组件的动画对比2.1 动画参数组件动画时长动画曲线drawProgress 的作用HandwritingRadar1000msFastOutSlowIn控制数据多边形大小0→1EmotionTrendChart1200msFastOutSlowIn控制可见数据点数0→NMatchRing1200msFastOutSlowIn控制弧角度0→2π×match/1002.2 动画效果// ① 雷达图从中心向外扩散constrradius*(p.value/100)*this.drawProgress// drawProgress0 → 所有顶点汇聚在中心// drawProgress1 → 数据多边形到达最终位置// ② 折线图从左到右逐段绘制constvisibleCountMath.floor(this.drawProgress*this.data.length)// drawProgress0 → visibleCount0空// drawProgress0.5 → 显示一半数据点// drawProgress1 → 全部显示// ③ 圆环图从 12 点顺时针展开constendstart(Math.PI*2*this.matchValue/100)*this.drawProgress// drawProgress0 → endstart圆弧长度为 0// drawProgress0.5 → 展开一半弧// drawProgress1 → 最终弧角度2.3 动画时序动画进度 (drawProgress)雷达图折线图圆环图0% (0ms)数据点全部在中心空白画布只有起点25% (250ms/300ms)多边形展开 25%前 1/4 数据点弧展开 25%50% (500ms/600ms)多边形展开一半一半数据点显示弧展开一半圆点出现75% (750ms/900ms)多边形接近完成大部分点可见弧接近完成100% (1000ms/1200ms)完整雷达图全部数据 最终高亮完整圆环 圆点三、AppAnimations 常量管理3.1 Canvas 相关常量项目中所有动画时长和曲线通过 [AppAnimations.ets](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppAnimations.ets) 统一管理exportclassAppAnimations{// Canvas 绘制动画时长staticreadonlyRADAR_DRAW_DURATION:number1000// 雷达图展开staticreadonlyCHART_DRAW_DURATION:number1200// 折线图绘制staticreadonlyRING_DRAW_DURATION:number1200// 匹配环展开staticreadonlyRING_FADE_DURATION:number600// 渐变环入场// 公用曲线staticreadonlyCURVE_SMOOTH:CurveCurve.FastOutSlowIn}3.2 为什么用常量而不用硬编码// ❌ 硬编码难以维护难以统一调整animateTo({duration:1000,curve:Curve.FastOutSlowIn},(){this.drawProgress1})// ✅ 常量统一修改语义清晰animateTo({duration:AppAnimations.RADAR_DRAW_DURATION,curve:AppAnimations.CURVE_SMOOTH},(){this.drawProgress1})四、Watch 的最佳实践4.1 监听函数格式// Watch 的函数名要与装饰器中的字符串一致StateWatch(onDrawProgressChange)drawProgress:number0onDrawProgressChange():void{this.draw()}注意事项// ✅ 正确函数名匹配StateWatch(myCallback)x:number0myCallback():void{...}// ❌ 错误函数名不匹配StateWatch(myCallback)x:number0myDifferentCallback():void{...}// 永远不会被调用4.2 避免在 Watch 中做耗时操作// ❌ 不推荐Watch 中做复杂计算onDrawProgressChange():void{this.heavyComputation()// 每帧都执行卡顿this.draw()}// ✅ 推荐计算放在 draw() 前一次完成onDrawProgressChange():void{this.draw()}privatedraw():void{// 所有计算集中在 draw() 中首次 Watch 时才执行this.calculateAngles()// 如果数据未变可缓存this.renderGraphics()}五、首次绘制与动画的配合5.1 onReady aboutToAppear 时序项目中的首次绘制由onReady触发动画由aboutToAppear触发aboutToAppear():void{// ② animateTo → drawProgress 0→1触发 Watch → drawanimateTo({...},(){this.drawProgress1})}onDrawProgressChange():void{this.draw()// ③ 动画每帧触发 draw()}build(){Canvas(this.context).onReady(()this.draw())// ① 首次绘制drawProgress0 的初始状态}执行时序细化T0ms 组件创建 │ ├── aboutToAppear() 开始 animateTo... │ T5ms Canvas 布局完成 │ ├── onReady() → this.draw() 首次绘制 │ drawProgress0 → 网格可见数据多边形为点 │ T5~1005ms 动画进行中 │ ├── Watch 持续触发 → draw() 不断更新 │ drawProgress 逐渐从 0 增加到 1 │ T1005ms 动画完成 │ └── drawProgress1最终状态渲染完毕5.2 为什么不只靠 onReady// ❌ 如果只靠 onReadybuild(){Canvas(this.context).onReady((){this.draw()// 只画一次// 没有动画直接显示最终状态})}// ✅ 正确做法onReady 画初始状态 animateTo 驱动动画// 用户会看到空白 → 网格出现 → 数据渐变填充六、三种动画模式6.1 模式一缩放动画雷达图// 数据多边形从中心向外扩散// 通过缩放顶点到中心的距离实现constrradius*(p.value/100)*this.drawProgress6.2 模式二分段显现折线图// 数据点从左到右逐段出现// 通过控制可见点数实现constvisibleCountMath.floor(this.drawProgress*this.data.length)6.3 模式三弧展开圆环图// 圆弧从起点到终点逐步展开// 通过控制结束角度实现constendstart(Math.PI*2*this.matchValue/100)*this.drawProgress6.4 对比模式动画类型数学本质适用场景缩放大小渐变半径 × progress多边形、矩形分段部分→完整count × progress折线、柱状图弧展开角度渐变angle × progress环形、饼图七、动画性能考量7.1 Canvas 重绘频率animateTo驱动State变化的频率取决于动画时长和帧率// 1000ms 动画60fps → ~60 次重绘// 每次 draw() 执行完整绘制流程// 避免在 draw() 中// 1. 创建大量临时对象// 2. 执行不必要的清除和重绘// 3. 连续调用多次 fill/stroke// ✅ draw() 中推荐// 1. 预计算不变数据如 angles 数组// 2. 聚合路径一个 beginPath 多个 moveTo/lineTo 一次 stroke// 3. 必需的清除操作clearRect7.2 预计算优化privatedraw():void{// ❌ 不推荐每帧都在循环中计算 cos/sinfor(leti0;isides;i){constangle(Math.PI*2*i)/sides-Math.PI/2constxcxr*Math.cos(angle)// 每帧都算}// ✅ 推荐angles 数组只在数据变化时计算一次// 在 draw 开始时预计算或缓存为成员变量constanglesthis.cachedAngles??this.computeAngles()}八、State 与 Canvas 的协作总结8.1 关键要点要点说明State 驱动声明式状态管理触发 Canvas 命令式绘制animateTo 渡值平滑地将 drawProgress 从 0 过渡到 1Watch 桥接检测状态变化调用 draw() 重绘onReady 兜底Canvas 就绪时的首次绘制绘制函数纯净draw() 只读取状态不修改状态8.2 常见问题排查现象原因解决动画不执行animateTo 未调用检查 aboutToAppear 是否正确Canvas 空白onReady 未触发检查 Canvas 宽高是否 0绘制闪烁draw 中未 clearRect每次 draw 先清除画布动画卡顿Watch 中耗时操作将计算移到 draw 外部最终状态不对drawProgress 逻辑有误debug 输出 drawProgress 值总结本文系统解析了 Canvas 动画的 State Watch animateTo 驱动模式核心模式animateTo驱动drawProgress(0→1)→Watch触发draw()三种动画效果缩放雷达图、分段折线图、弧展开圆环图AppAnimations 常量统一管理动画时长和曲线首次绘制onReady画初始状态 animateTo驱动动画性能优化预计算 聚合路径 避免 Watch 耗时操作下一篇文章将介绍双色叠加雷达图——在 DuetReportPage 中绘制两个人的对比雷达图。如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力参考资源animateTo 显式动画Watch 装饰器State 装饰器[HandwritingRadar 动画源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/HandwritingRadar.ets)[EmotionTrendChart 动画源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/EmotionTrendChart.ets)[MatchRing 动画源码](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/components/MatchRing.ets)[AppAnimations 动画常量](file:///Users/fiona/Downloads/bijixinli/harmony-app/entry/src/main/ets/common/theme/AppAnimations.ets)HarmonyOS 开发文档互动投票你更喜欢哪种 Canvas 动画实现方式A. State Watch animateTo本项目方案B. requestAnimationFrame 手动循环C. CSS 过渡 / Web 动画 API其他平台D. 直接用三方图表库ECharts 等欢迎在评论区分享你的选择如果这篇文章对你有帮助欢迎点赞、收藏⭐、关注你的支持是我持续创作的动力