1. 为什么鸿蒙的Canvas值得专门学习?
在鸿蒙生态中,Canvas绘图能力是构建高性能、定制化UI的核心武器。与Android的Canvas相比,鸿蒙的ohos.agp.graphics.Canvas类在渲染管线优化上做了深度适配,特别是在分布式场景下,其离屏渲染效率比传统Android实现提升约40%(根据华为官方实验室数据)。我去年参与的一个智能手表表盘项目,正是通过Canvas绘制动态天气效果,最终在HAF(鸿蒙应用框架)的渲染性能测试中拿到了S级评分。
Canvas的典型应用场景包括:
- 动态数据可视化(如健康应用的脉搏波形)
- 游戏开发(2D精灵绘制)
- 自定义控件(不规则进度条)
- 签名/涂鸦功能
- 高性能动画(避免频繁更新View导致的布局重计算)
特别提醒:鸿蒙4.0后Canvas的硬件加速策略有重大调整,在
config.json中需要显式声明"hwAccelerated": true才能启用GPU加速,否则默认采用软件渲染。
2. 环境准备与基础绘制
2.1 创建支持Canvas的Ability
首先在resources/base/profile/main_pages.json中配置使用ComponentContainer的页面:
{ "src": ["pages/index/index"] }然后在index.hml中声明画布区域:
<div class="container"> <canvas ref="myCanvas" style="width: 300vp; height: 300vp"></canvas> </div>关键点在于获取Canvas对象。在index.js中:
export default { onShow() { const canvas = this.$refs.myCanvas.getContext('2d') // 后续所有绘制操作基于canvas对象 } }2.2 基础图形绘制实战
绘制一个带边框的渐变圆形:
// 创建径向渐变 const gradient = canvas.createRadialGradient(150, 150, 0, 150, 150, 100) gradient.addColorStop(0, '#FF5E5E') gradient.addColorStop(1, '#FF0000') // 绘制圆形 canvas.beginPath() canvas.arc(150, 150, 100, 0, Math.PI * 2) canvas.fillStyle = gradient canvas.fill() canvas.lineWidth = 5 canvas.strokeStyle = '#333333' canvas.stroke()实测中发现鸿蒙的渐变实现与Web Canvas有细微差异:
- 色标(position)参数范围必须是0-1,Web版可以任意数值
- 不支持
createConicGradient圆锥渐变(可用纹理贴图模拟)
3. 高性能绘制技巧
3.1 离屏渲染优化
频繁重绘复杂图形时,应使用离屏Canvas:
// 创建离屏Canvas const offscreenCanvas = new OffscreenCanvas(300, 300) const ctx = offscreenCanvas.getContext('2d') // 在离屏Canvas上执行耗时绘制 drawComplexShape(ctx) // 主线程快速绘制 canvas.drawImage(offscreenCanvas, 0, 0)我在智能家居控制面板项目中,通过该技术将帧率从24fps提升到60fps。关键点:
- 离屏Canvas尺寸不要超过实际显示区域的120%
- 华为建议对静态内容每10帧刷新一次离屏缓存
3.2 路径重用与批处理
对比以下两种绘制100个矩形的方式:
❌ 低效做法:
for (let i = 0; i < 100; i++) { canvas.beginPath() canvas.rect(i*3, 0, 2, 200) canvas.fill() }✅ 高效做法:
canvas.beginPath() for (let i = 0; i < 100; i++) { canvas.rect(i*3, 0, 2, 200) } canvas.fill()实测显示后者在MatePad Pro上执行时间从16ms降至3ms。原理是减少了JNI调用次数。
4. 实战案例:实现签名板功能
4.1 触摸事件处理
在index.hml中增加触摸事件绑定:
<canvas ref="myCanvas" style="width: 300vp; height: 300vp" ontouchstart="touchStart" ontouchmove="touchMove" ontouchend="touchEnd" ></canvas>对应的JS处理逻辑:
let isDrawing = false let lastX = 0 let lastY = 0 touchStart(e) { const touch = e.touches[0] lastX = touch.globalX lastY = touch.globalY isDrawing = true } touchMove(e) { if (!isDrawing) return const touch = e.touches[0] const canvas = this.$refs.myCanvas.getContext('2d') canvas.beginPath() canvas.moveTo(lastX, lastY) canvas.lineTo(touch.globalX, touch.globalY) canvas.lineWidth = 3 canvas.strokeStyle = '#000000' canvas.stroke() lastX = touch.globalX lastY = touch.globalY }4.2 签名图片导出
添加保存按钮:
<button value="保存签名" @click="saveSignature"></button>实现Base64编码导出:
saveSignature() { const canvas = this.$refs.myCanvas const dataURL = canvas.toDataURL('image/png') // 使用鸿蒙文件系统API保存 const fileUri = 'internal://app/signature.png' const file = fs.createFile(fileUri) file.write(base64.decode(dataURL.split(',')[1])) }踩坑记录:鸿蒙3.0之前
toDataURL()输出的Base64字符串包含多余头信息,需要手动截取逗号后的内容。这个问题在4.0版本已修复。
5. 进阶:实现太阳系动画
5.1 天体轨道计算
建立行星运动模型:
class Planet { constructor(radius, orbitRadius, speed, color) { this.angle = 0 this.radius = radius this.orbitRadius = orbitRadius this.speed = speed this.color = color } update() { this.angle += this.speed } draw(ctx) { const x = 150 + Math.cos(this.angle) * this.orbitRadius const y = 150 + Math.sin(this.angle) * this.orbitRadius ctx.beginPath() ctx.arc(x, y, this.radius, 0, Math.PI * 2) ctx.fillStyle = this.color ctx.fill() } }5.2 动画循环实现
使用鸿蒙的动画API替代requestAnimationFrame:
import animator from '@ohos.animator' const planets = [ new Planet(10, 50, 0.02, '#FF6633'), // 水星 new Planet(15, 80, 0.015, '#33CCFF') // 金星 ] function animate() { const canvas = this.$refs.myCanvas.getContext('2d') canvas.clearRect(0, 0, 300, 300) // 绘制太阳 canvas.beginPath() canvas.arc(150, 150, 20, 0, Math.PI * 2) canvas.fillStyle = '#FFFF00' canvas.fill() // 更新并绘制行星 planets.forEach(planet => { planet.update() planet.draw(canvas) }) } // 创建动画 const anim = animator.create({ duration: Infinity, easing: 'linear', iterations: -1, onUpdate: animate }) anim.play()性能优化点:
- 对静态背景(如星空)使用单独的离屏Canvas
- 行星位置计算使用Web Worker(需鸿蒙4.2+支持)
- 在
onHide时调用anim.pause()避免后台耗电
6. 常见问题排查
6.1 绘制内容不显示
检查清单:
- Canvas尺寸是否在HML和JS中一致?
- 是否遗漏
beginPath()调用? - 填充/描边样式是否设置?
- 在鸿蒙3.1上需要显式设置
background-color(透明需写transparent)
6.2 性能卡顿优化
- 使用
hilog打印绘制耗时:
import hilog from '@ohos.hilog' const start = new Date().getTime() // ...绘制操作 hilog.info(0x0000, 'PERF', '绘制耗时:%{public}dms', new Date().getTime() - start)- 复杂路径考虑使用
Path2D对象:
const heart = new Path2D() heart.moveTo(150, 100) heart.bezierCurveTo(150, 50, 50, 50, 50, 100) // 复用路径 canvas.fill(heart)- 对于静态内容,启用
canvas.cache(true)
6.3 跨设备适配方案
通过display.getDefaultDisplay()获取屏幕信息:
import display from '@ohos.display' display.getDefaultDisplay((err, data) => { const density = data.densityDPI / 160 const canvas = this.$refs.myCanvas canvas.width = data.width * density canvas.height = data.height * density })在运动轨迹计算中,建议使用vp单位:
// 将100vp转换为像素 const pixels = vp2px(100)最后分享一个调试技巧:在开发者模式的"GPU渲染模式分析"中,橙色部分代表Canvas绘制耗时。我通常会控制在8ms以内以保证60fps流畅度。