码农周末:用鸿蒙 Canvas 撸了个抽奖转盘,顺便把动画原理搞明白了 码农周末用鸿蒙 Canvas 撸了个抽奖转盘顺便把动画原理搞明白了周末闲着没事寻思着学点新技术。之前一直听说鸿蒙的 Canvas API 和 Web 标准高度兼容就想着搞个实战项目验证一下。最后选了抽奖转盘——功能不复杂但该有的技术点都有Canvas绑制、动画、状态管理、用户交互。而且这玩意儿做出来还挺实用的年会、活动都能用。一、为什么选 Canvas其实鸿蒙提供了很多实现转盘的方案Image Rotation用图片做转盘旋转动画自定义组件用 ArkUI 的 Shape 组件Canvas手动绑制为什么选 Canvas几个原因灵活想画啥画啥不受限制轻量不用准备一堆图片资源学习价值Canvas 是基本功学会了一通百通二、开发环境没啥特别的就是 DevEco Studio HarmonyOS NEXT (API 23)。项目配置包名com.example.myapplication模板Empty Ability语言ArkTS三、Canvas 基础先画个圆万事开头难先画个圆试试水Canvas(this.canvasCtx).width(310).height(310).onReady((){constctxthis.canvasCtx ctx.beginPath()ctx.arc(155,155,138,0,Math.PI*2)ctx.fillStyle#FF6B6Bctx.fill()})运行一下一个红色大圆出现在屏幕中央。感觉还行和 Web Canvas 几乎一模一样。四、画转盘扇形是关键转盘本质上就是8个扇形拼成的圆。4.1 扇形怎么画关键代码就三行ctx.moveTo(cx,cy)// 移动到圆心ctx.arc(cx,cy,r,start,end)// 画弧ctx.closePath()// 闭合路径closePath()会自动把弧的终点和圆心连起来形成一个扇形。4.2 画8个扇形constcolors[#FF6B6B,#FFB347,#4ECDC4,#45B7D1,#96CEB4,#FFEAA7,#DDA0DD,#98D8C8]for(leti0;i8;i){conststarti*Math.PI/4constendstartMath.PI/4ctx.beginPath()ctx.moveTo(155,155)ctx.arc(155,155,138,start,end)ctx.closePath()ctx.fillStylecolors[i]ctx.fill()}效果一个彩色的大饼出现在屏幕上。4.3 文字怎么写这个问题困扰了我一会儿。每个扇形的文字方向不一样总不能都横着写吧后来发现 Canvas 有坐标变换ctx.save()ctx.translate(cx,cy)// 原点移到圆心ctx.rotate(startMath.PI/8)// 旋转到扇形中间ctx.fillText(一等奖,r-14,0)// 沿半径方向写ctx.restore()原理先把坐标系的原点移到圆心再旋转坐标系这样 X 轴方向就是半径方向了。五、让它转起来动画原理5.1 动画本质动画就是连续播放静态画面。在 Canvas 里就是不断清空画布、重绘、清空、重绘……5.2 减速效果现实中的转盘是减速停止的不是匀速。我用了一个简单的物理模型letvelocity0.4// 初始速度constdecel0.98// 衰减系数setInterval((){this.wheelAnglevelocity velocity*decel// 每帧速度减少 2%this.drawWheel()// 重绘if(velocity0.002){clearInterval(timer)// 停止}},20)效果转盘开始转得很快然后越来越慢最后自然停止。5.3 随机性为了公平每次转的速度应该不一样letvelocity0.35Math.random()*0.15// 0.35 ~ 0.5这样每次停止的位置都不同。六、结果判定数学题来了6.1 问题描述转盘停了怎么知道中了什么奖已知指针固定在正上方Canvas 坐标-π/2转盘转了wheelAngle弧度求指针指向哪个扇区6.2 我的推导假设转盘顺时针转了 θ 弧度指针相对于转盘的位置-π/2 - θ除以每格角度(-π/2 - θ) / (π/4)这个数可能是负数或大于8所以取模6.3 坑JavaScript 的负数取模在 JavaScript 里-3 % 8 -3不是5所以要修正letidx((raw%8)8)%8这个技巧我之前在刷 LeetCode 时学过没想到在这里用上了。6.4 最终代码determineResult():void{constpointerAngle-Math.PI/2constsegAngleMath.PI/4letraw(pointerAngle-this.wheelAngle)/segAngleletidx((raw%8)8)%8idxMath.floor(idx)this.resultthis.items[idx]}七、美化颜值即正义7.1 配色选了8个鲜艳的颜色constCOLORS[#FF6B6B,// 珊瑚红#FFB347,// 橙黄#4ECDC4,// 青绿#45B7D1,// 天蓝#96CEB4,// 薄荷绿#FFEAA7,// 柠檬黄#DDA0DD,// 粉紫#98D8C8// 浅绿]7.2 中心圆加个渐变的中心圆看着更有质感constgradctx.createRadialGradient(155,155,0,155,155,24)grad.addColorStop(0,#FFFFFF)grad.addColorStop(1,#F0F0F0)ctx.arc(155,155,24,0,Math.PI*2)ctx.fillStylegrad ctx.fill()7.3 指针画个三角形指针带阴影ctx.beginPath()ctx.moveTo(px,py-16)// 顶点ctx.lineTo(px-12,py4)// 左下ctx.lineTo(px12,py4)// 右下ctx.closePath()ctx.fillStyle#FF6B35ctx.shadowColorrgba(0,0,0,0.2)ctx.shadowBlur6ctx.fill()7.4 外圈阴影给转盘加个外圈阴影增加立体感ctx.beginPath()ctx.arc(cx,cy,r4,0,Math.PI*2)ctx.shadowColorrgba(0,0,0,0.15)ctx.shadowBlur12ctx.fillStyle#FFFFFFctx.fill()八、功能完善自定义选项光有默认奖项太单调加个编辑功能8.1 编辑弹层if(this.showEditor){Column(){Column(){Text(✏️ 自定义选项)Text(每行一项最多8项)TextArea({text:this.editText}).onChange((val){this.editTextval})Row(){Button(取消)Button(保存)}}.padding(20)}.backgroundColor(#80000000)}8.2 保存逻辑saveItems():void{constlinesthis.editText.split(\n).map(ss.trim()).filter(ss.length0)if(lines.length2)return// 至少2项this.itemslines.slice(0,8)// 最多8项this.showEditorfalsethis.drawWheel()}九、历史记录给用户反馈抽奖结果应该能查不然抽完就忘了。9.1 数据存储Statehistory:string[][]// 添加记录this.history[prize,...this.history].slice(0,50)新记录插到数组头部最多保留50条。9.2 显示记录ForEach(this.history.slice(0,6),(item,idx){Row(){Text(#${idx1})Text()Text(item)}})只显示最近6条太多了也看不完。十、踩坑记录坑1Canvas 绘制时机问题在aboutToAppear里画图报错。原因Canvas 还没初始化。解决用onReady回调。坑2定时器泄漏问题退出页面后转盘还在转。原因定时器没清理。解决在aboutToDisappear里clearInterval。坑3文字截断问题奖项太长超出扇形。解决手动截断consttextlabel.length6?label.slice(0,5)…:label坑4负数取模问题结果索引错误。原因JavaScript 的%可能返回负数。解决((raw % 8) 8) % 8。坑5重复点击问题快速点多次抽奖转盘乱转。解决用isSpinning状态判断if(this.isSpinning)returnthis.isSpinningtrue十一、运行效果十二、总结技术收获Canvas API和 Web 标准确实高度兼容会前端的同学无缝迁移动画原理动画就是连续的静态画面理解了这个就不再神秘坐标变换translate rotate 大法好解决文字旋转问题数学基础角度、弧度、取模都是基本功项目经验从小处着手先画圆再画扇形再加动画一步步来多测试边界负数取模这种坑不踩不知道清理资源定时器、监听器该清理的一定要清理代码量最终统计主文件Index.ets约 350 行。对于一个完整的应用来说这个量级刚刚好——既有足够的技术深度展示 Canvas 能力又不会因为太复杂而劝退新手。十三、后续改进这个项目还有不少可以优化的地方音效转盘转动时加点咔咔声更有感觉粒子效果中奖时放个烟花动画概率配置让后台能设置每个奖项的概率持久化用 Preferences 保存历史记录留着以后慢慢迭代吧先把基础打牢。写在最后两天时间从零撸了个抽奖转盘。虽然功能简单但把 Canvas 的核心技能点都过了一遍。最大的感受是Canvas 其实不难难的是耐心。各种坐标变换、角度计算一个细节错就全错。但只要一步步来多调试总能搞定。如果你也在学鸿蒙开发建议找个小项目动手写一遍。看再多教程都不如亲自踩一遍坑。有问题欢迎评论区交流