《HarmonyOS技术精讲-ArkGraphics 2D(方舟2D图形服务)》第七篇:动效来袭——与ArkUI结合实现2D动画与交互 HarmonyOS技术精讲-ArkGraphics 2D方舟2D图形服务第七篇动效来袭——与ArkUI结合实现2D动画与交互Canvas动画加上交互难在哪ArkUI 里做 Canvas 动画大多数人第一次写都会遇到两个问题状态同步和触摸坐标转换。状态同步指的是动画循环里的变量比如旋转角度如何驱动 Canvas 重新绘制同时又不让整个组件频繁重建。触摸坐标转换则是手指在屏幕上滑动时如何准确映射到 Canvas 的绘制坐标系。这两个问题单独看都不复杂但放在一起处理时生命周期管理、动画启停、交互响应这些细节很容易出 bug。官方示例通常只展示静态绘制或者简单动画一旦掺入用户交互代码就需要额外处理不少边界情况。这篇文章用一个可拖拽旋转的风车作为案例把状态管理、动画循环、触摸交互这三块内容串起来讲。风车叶片持续旋转用户拖拽时改变旋转方向和速度松手后带惯性减速。这个场景刚好覆盖了 ArkGraphics 2D 与 ArkUI 交互的核心技术点。它解决什么问题这个案例的目标是在 ArkUI 中实现一个连续动画的 2D 图形并且用户可以通过触摸直接控制动画状态。适用场景包括需要用户手势操控的图形化控件如旋钮、转盘带有惯性效果的 UI 组件如滑动选择器游戏中的拖拽旋转元素不适用场景纯静态图形展示不需要动画循环复杂 3D 场景应使用 WebGL 或专用引擎与直接用 ArkUI 的动画属性如animation相比Canvas 方式有几个明显差异特性Canvas requestAnimationFrameArkUI animation 属性绘制自由度任意 2D 图形无组件限制仅支持组件级属性动画交互控制可精细控制每帧状态通过交互事件触发动画性能开销逐帧绘制GPU 友好组件属性变化触发重绘惯性/物理模拟容易实现需要额外计算这个案例选择 Canvas 方案主要是因为风车的叶片形状扇形在 ArkUI 原生组件中无法直接实现而 Canvas 可以自由绘制。同时拖拽旋转和惯性效果需要逐帧控制角度这正是requestAnimationFrame擅长的领域。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机核心实现整个功能分解为四个部分状态管理、Canvas 绘制、动画循环、触摸交互。下面逐个实现。状态管理风车的核心状态是旋转角度和角速度。角度决定当前绘制位置角速度控制惯性旋转。// 风车状态StatecurrentAngle:number0;// 当前旋转角度弧度privateangularVelocity:number0;// 当前角速度弧度/帧privateisDragging:booleanfalse;// 是否正在拖拽State装饰器让currentAngle的变化自动触发 UI 刷新。angularVelocity和isDragging不需要驱动 UI所以用private标记避免不必要的状态追踪。这里有个设计取舍为什么不把angularVelocity也设为State因为角速度只在动画循环中使用不直接影响 Canvas 绘制内容。如果把它也设为响应式状态每次修改都会触发组件更新检查反而增加开销。Canvas 绘制风车风车由 4 个扇形叶片和一个中心圆组成。每个叶片使用arc绘制扇形填充不同颜色。// 绘制风车privatedrawWindmill(ctx:CanvasRenderingContext2D,width:number,height:number):void{constcxwidth/2;constcyheight/2;constbladeLength80;// 叶片长度constbladeAngleMath.PI/3;// 叶片张开角度 60°constcolors[#FF6B6B,#4ECDC4,#45B7D1,#FFE66D];// 清空画布ctx.clearRect(0,0,width,height);// 绘制背景辅助圆淡灰色ctx.beginPath();ctx.arc(cx,cy,bladeLength10,0,Math.PI*2);ctx.strokeStyle#E0E0E0;ctx.lineWidth1;ctx.stroke();// 绘制 4 个叶片for(leti0;i4;i){conststartAnglethis.currentAnglei*Math.PI/2-bladeAngle/2;constendAnglethis.currentAnglei*Math.PI/2bladeAngle/2;ctx.beginPath();ctx.moveTo(cx,cy);ctx.arc(cx,cy,bladeLength,startAngle,endAngle);ctx.closePath();ctx.fillStylecolors[i];ctx.fill();// 叶片边缘描边ctx.strokeStyle#333;ctx.lineWidth1.5;ctx.stroke();}// 绘制中心圆ctx.beginPath();ctx.arc(cx,cy,14,0,Math.PI*2);ctx.fillStyle#2C3E50;ctx.fill();ctx.strokeStyle#1A252F;ctx.lineWidth2;ctx.stroke();// 中心小圆点ctx.beginPath();ctx.arc(cx,cy,4,0,Math.PI*2);ctx.fillStyle#FFFFFF;ctx.fill();}这里需要注意叶片的角度范围是currentAngle加上每个叶片的偏移量i * π/2再加减半个叶片张角。这样 4 个叶片正好均匀分布整体旋转由currentAngle控制。中心圆加两层是为了视觉层次感小圆点作为装饰也让触摸拖拽时有个视觉焦点。动画循环动画循环使用requestAnimationFrame驱动每帧更新角度并重绘。privateanimatorId:number-1;privatestartAnimation(ctx:CanvasRenderingContext2D,width:number,height:number):void{constanimate(){if(!this.isDraggingMath.abs(this.angularVelocity)0.001){// 惯性旋转角度累加速度衰减this.currentAnglethis.angularVelocity;this.angularVelocity*0.985;// 阻尼系数}this.drawWindmill(ctx,width,height);this.animatorIdrequestAnimationFrame(animate);};animate();}privatestopAnimation():void{if(this.animatorId!-1){cancelAnimationFrame(this.animatorId);this.animatorId-1;}}惯性效果的核心逻辑松手后angularVelocity逐渐衰减到零每次衰减乘以 0.985。这个系数决定了惯性持续的时间0.98 到 0.99 之间手感比较自然可以根据需要调整。示例代码地址项目地址