系列文章《鸿蒙开发从入门到专家》第 21 篇 · 中级
前二十篇我们把鸿蒙应用的骨架搭得差不多了:UI、路由、数据、网络、权限、上架。但真要把产品体验做出彩,离不开自定义图形能力。签名板、手写批注、实时图表、游戏特效,这些场景用标准组件很难搞定,必须下沉到 Canvas。
Canvas 在鸿蒙里不算新 Kit,但用法和 Web 的 <canvas> 有明显差异。本文从 2D 绘制基础讲起,带你把路径动画、离屏渲染这两个高频能力跑通。
一、Canvas 在 ArkUI 中的基本形态
ArkUI 提供两种使用 Canvas 的方式:
Canvas 组件:直接在界面上绘制,适合实时展示。
OffscreenCanvas:在内存中绘制,适合预渲染或批量生成图片。
先来看一个最简的签名板骨架:
import { drawing } from '@kit.ArkUI';
@Entry
@Component
struct SignatureBoard {
private settings: RenderingContextSettings = new RenderingContextSettings(true);
private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
@State lastX: number = 0;
@State lastY: number = 0;
build() {
Column() {
Canvas(this.context)
.width('100%')
.height(300)
.backgroundColor('#F5F5F5')
.onReady(() => {
this.context.lineWidth = 3;
this.context.strokeStyle = '#000000';
this.context.lineCap = 'round';
this.context.lineJoin = 'round';
})
.onTouch((event: TouchEvent) => {
const touch = event.touches[0];
if (event.type === TouchType.Down) {
this.lastX = touch.windowX;
this.lastY = touch.windowY;
this.context.beginPath();
this.context.moveTo(this.lastX, this.lastY);
} else if (event.type === TouchType.Move) {
this.context.lineTo(touch.windowX, touch.windowY);
this.context.stroke();
this.lastX = touch.windowX;
this.lastY = touch.windowY;
}
})
Button('清空')
.margin(16)
.onClick(() => {
this.context.clearRect(0, 0, 9999, 9999);
})
}
.width('100%')
.height('100%')
}
}
.onReady() 里初始化画笔属性,.onTouch() 中按 Down / Move / Up 三阶段记录坐标。这里的关键是 moveTo 和 lineTo 的配合:每次手指按下都是一条新路径,移动时把点连成线。
二、Path 与贝塞尔曲线:让绘制更平滑
直接用 lineTo 连接触点,快速滑动时会出现折线。移动端通用的优化方案是用二次贝塞尔曲线(quadraticCurveTo)拟合中间点:
private drawSmoothLine(x: number, y: number) {
const cx = (this.lastX + x) / 2;
const cy = (this.lastY + y) / 2;
this.context.quadraticCurveTo(this.lastX, this.lastY, cx, cy);
this.context.stroke();
this.lastX = x;
this.lastY = y;
}
思路是:把上一个点和当前点的中点作为曲线终点,上一个点本身作为控制点。这样画出来的轨迹圆润很多,手写签名也不会显得生硬。
如果要做复杂图形,比如饼图或仪表盘,可以先把路径放到 Path2D 对象里,再一次性 stroke() 或 fill():
let path = new Path2D();
path.arc(150, 150, 100, 0, Math.PI * 2);
path.moveTo(150, 150);
path.lineTo(250, 150);
this.context.fill(path);
Path2D 的优势是可复用。同一份路径数据可以多次绘制,也可以传给离屏 Canvas 做预处理。
三、路径动画:用 requestAnimationFrame 驱动
静态图形只是第一步。进度环、加载动画、波形图都需要逐帧更新。鸿蒙 Canvas 支持 requestAnimationFrame,用法和浏览器一致:
@State progress: number = 0;
private drawProgress() {
this.context.clearRect(0, 0, this.width, this.height);
this.context.beginPath();
this.context.arc(150, 150, 80, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * this.progress);
this.context.strokeStyle = '#007DFF';
this.context.lineWidth = 12;
this.context.stroke();
}
private startAnimation() {
const animate = () => {
this.progress += 0.01;
if (this.progress > 1) this.progress = 0;
this.drawProgress();
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
这里有一个容易踩的坑:动画函数必须绑定到组件生命周期,否则页面退出后可能继续跑,造成内存泄漏。建议在 aboutToDisappear() 里取消动画帧,或者用 @State 控制开关。
四、离屏渲染:把重绘制从主线程剥离开
当一屏里有大量图形元素时,直接在主 Canvas 上逐帧重绘会拖慢 UI 线程。离屏渲染的思路是:先把不变的背景、网格、装饰图案画到 OffscreenCanvas,生成一张位图,主 Canvas 每一帧只贴这张图,再增量绘制变化部分。
import { OffscreenCanvas } from '@kit.ArkUI';
private offscreen: OffscreenCanvas = new OffscreenCanvas(600, 600);
private offCtx: OffscreenCanvasRenderingContext2D = this.offscreen.getContext('2d');
private renderBackground() {
// 绘制一次,复用多次
this.offCtx.fillStyle = '#FFFFFF';
this.offCtx.fillRect(0, 0, 600, 600);
for (let i = 0; i < 10; i++) {
this.offCtx.strokeStyle = '#EEEEEE';
this.offCtx.strokeRect(i * 60, 0, 60, 600);
}
}
private drawFrame() {
// 主 Canvas 只负责把离屏位图贴上来
this.context.transferFromImageBitmap(this.offscreen.transferToImageBitmap());
// 再画动态内容
this.context.fillStyle = '#FF0000';
this.context.beginPath();
this.context.arc(this.ballX, this.ballY, 20, 0, Math.PI * 2);
this.context.fill();
}
实测下来,对于背景复杂、动态元素少的场景,离屏渲染能把每帧绘制时间降一个数量级。但要注意位图尺寸,太大的 OffscreenCanvas 会占用显存,反而得不偿失。
五、实战:一个带平滑轨迹的波形图
把上面的能力串起来,做一个实时波形组件:
用
OffscreenCanvas预渲染网格背景。主线程每 50ms 采集一个数据点,加入队列。
用
requestAnimationFrame把队列里的点连成二次贝塞尔曲线。超过屏幕宽度的点从队列头部移除。
这个结构可以复用到心率图、音频可视化、股票分时线等场景。核心不是 API 多复杂,而是把“静态绘制、动画驱动、离屏缓存”三个环节拆清楚。
小结
Canvas 在鸿蒙开发里属于“知道就能用,不知道就卡壳”的能力。本文覆盖了三个关键用法:
Path2D+ 贝塞尔曲线,让轨迹绘制更自然。requestAnimationFrame驱动逐帧动画。OffscreenCanvas做离屏缓存,降低主线程负载。
下一篇我们进入并发编程,看看鸿蒙的 TaskPool 和 Worker 怎么帮你把耗时任务从 UI 线程剥离开。