《HarmonyOS技术精讲-ArkGraphics 2D(方舟2D图形服务)》第一篇:初识ArkGraphics 2D——Canvas组件与基础绘制入门

HarmonyOS技术精讲-ArkGraphics 2D(方舟2D图形服务)第一篇:初识ArkGraphics 2D——Canvas组件与基础绘制入门
1. 开篇:从绘制一个矩形开始
在HarmonyOS NEXT的应用开发中,如果只是展示图片,直接上Image组件就够了。但一旦涉及到动态图表、游戏界面、自定义仪表盘或者图表标注时,你会发现,没有Canvas还真不行。
很多开发者第一次接触ArkGraphics 2D时,第一反应是:“这不就是安卓的Canvas吗?” 但实际用起来会发现,HarmonyOS的Canvas虽然功能类似,但在接口设计、生命周期管理、以及和ArkUI状态绑定的方式上,都有自己的“脾气”。官方文档虽然给出了基本API,但对于初学者来说,缺少一个能直接运行的、从零搭建的完整示例,隐藏的坑不少。
这篇文章不会讲复杂的三维渲染或粒子系统,只聚焦一个目标:让你在页面上成功落下一个“像素”,并画出点、线、矩形、圆和文字。搞定这些,2D绘图的大门就打开了。
2. ArkGraphics 2D到底解决什么问题?
简单来说,ArkGraphics 2D 提供了一个2D渲染上下文,允许开发者通过编程方式精确控制屏幕上每个像素的绘制。它解决的场景非常明确:
- 数据可视化:绘制折线图、饼图、柱状图。
- 游戏与交互:实现小型游戏画面、自定义手势反馈。
- 图像编辑:实现简单的绘图板、图像裁剪标注。
- UI特效:绘制复杂的不规则形状、渐变背景。
不适合的场景:如果你只是展示静态图片或简单的文字排版,Image 和 Text 组件更合适。它们有更好的性能优化(如硬件加速,但Canvas的渲染路径也需要手动优化),并且天然支持无障碍服务。用Canvas做全界面文字渲染,性能会成问题,也不利于盲人用户使用。
与类似方案对比
| 方案 | 适用场景 | 性能 | 易用性 | 推荐程度 |
|---|---|---|---|---|
| ArkGraphics 2D (Canvas) | 动态、程序化、复杂自定义图形 | 中(需手动优化) | 中(需要理解绘制上下文) | ⭐️⭐️⭐️⭐️⭐️ (第一选择) |
Image 组件 |
显示静态图片 | 高(硬件加速) | 高 | ⭐️⭐️⭐️⭐️ |
Shape / Path 组件 |
属于SVG类似,适合静态简单矢量图 | 高(框架层优化) | 高(声明式) | ⭐️⭐️⭐️⭐️ |
xcomponent + 自定义渲染引擎 |
高性能图形/3D | 极高 (GPU) | 极低 | ⭐️⭐️ (特定场景) |
绝大多数情况下,Canvas 是处理程序化、动态、复杂2D图形的首选方案。
3. 环境说明
- DevEco Studio 版本:DevEco Studio 6.1.0 及以上
- HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
- 目标设备:手机
- 语言:ArkTS(支持API 12+)
4. 核心实现:创建一个能画画的页面
我们将一步步构建一个页面,包含一个全屏Canvas,并在上面绘制点、线、矩形、圆和文本。
4.1 创建 Canvas 组件
首先,在 @Component 里创建一个 Canvas 组件。它需要一个 context 对象,这个对象就是我们的“画笔”——CanvasRenderingContext2D。
// pages/Index.ets
@Entry
@Component
struct Index {
// 1. 引用Canvas组件,通过这个引用可以获取到CanvasRenderingContext2D
private canvasRef: CanvasRenderingContext2D = new CanvasRenderingContext2D();
build() {
Column() {
// 2. 创建Canvas组件,并将canvasRef绑定给它
Canvas(this.canvasRef)
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF') // 设置白色背景
.onReady(() => {
// 3. Canvas准备就绪后,开始画图
this.drawAll();
})
}
.width('100%')
.height('100%')
}
drawAll() {
// 获取上下文,其实就是 this.canvasRef
const ctx = this.canvasRef;
// 在下一节,我们将填充这个函数
}
}
关键点:
CanvasRenderingContext2D是画布的核心,所有绘制指令都通过它执行。.onReady()回调非常关键。Canvas必须在组件创建并布局完成后才能绘制。如果不等待onReady,直接调用drawAll,会出现绘制无效的情况。这是初学者最容易犯的错误之一。
4.2 绘制基础形状
现在,我们在 drawAll 函数中添加绘制逻辑。官方文档重点提到了 drawRect、drawCircle、drawText 以及 fillStyle、strokeStyle。
drawAll() {
const ctx = this.canvasRef;
if (!ctx) return; // 安全判断
// --- 1. 绘制一个填充的红色矩形 ---
ctx.fillStyle = '#FF0000'; // 设置填充颜色
ctx.fillRect(50, 50, 150, 100); // drawRect和fillRect等价,这里用fillRect更直观
// 等价于 ctx.drawRect(50, 50, 150, 100);
// --- 2. 绘制一个空心蓝色矩形(边框) ---
ctx.strokeStyle = '#0000FF'; // 设置边框颜色
ctx.lineWidth = 4; // 设置边框粗细
ctx.strokeRect(250, 50, 150, 100); // 绘制边框矩形
// --- 3. 绘制一个填充的绿色圆形 ---
ctx.fillStyle = '#00FF00';
ctx.beginPath(); // 开始新路径
ctx.arc(440, 100, 50, 0, Math.PI * 2); // 圆心(440,100),半径50,从0到2Π
ctx.fill(); // 填充
// --- 4. 绘制一条线段(从起点到终点) ---
ctx.strokeStyle = '#FF00FF'; // 粉色
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(50, 220); // 画笔移动到起点
ctx.lineTo(300, 220); // 绘制到终点
ctx.stroke(); // 描边
// --- 5. 绘制带样式的文字 ---
ctx.font = '30px sans-serif'; // 字体大小和字体系列
ctx.fillStyle = '#000000'; // 黑色文字
ctx.fillText('Hello ArkGraphics 2D', 50, 350); // 填充文本
// --- 6. 绘制一个像素点(通过绘制小矩形实现,drawPoint在实际项目中更复杂) ---
ctx.fillStyle = '#333333';
ctx.fillRect(380, 350, 1, 1); // 一个1x1的像素
// --- 7. 擦除特定区域 ---
// ctx.clearRect(0, 0, 500, 500); // 擦除整个canvas,如果执行,上面画的都会消失
}
关键点:
fillStyle/strokeStyle会影响后续所有绘制操作,直到你再次修改。所以在绘制不同形状前,需要先设置好。beginPath()至关重要。只要你绘制图形前不调用beginPath(),新的路径就会和上一段路径连在一起,导致绘制结果异常(例如两个图形之间的线会被连接起来)。这是一个高频踩坑点。arc()的用法很标准:arc(x, y, 半径, 起始角度, 结束角度, 是否逆时针)。0和Math.PI * 2代表一个完整的圆。fillRect和strokeRect是独立的API,分别对应“填充矩形”和“描边矩形”。drawRect和fillRect作用相同,推荐优先使用fill开头的方法,意图更明确。
4.3 完整的可运行代码
将以上代码合并,得到一个完整可运行的页面。
// pages/Index.ets
@Entry
@Component
struct Index {
private canvasRef: CanvasRenderingContext2D = new CanvasRenderingContext2D();
build() {
Column() {
Canvas(this.canvasRef)
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
.onReady(() => {
this.drawAll();
})
}
.width('100%')
.height('100%')
}
drawAll() {
const ctx = this.canvasRef;
if (!ctx) return;
// 1. 填充红色矩形
ctx.fillStyle = '#FF0000';
ctx.fillRect(50, 50, 150, 100);
// 2. 空心蓝色矩形(边框)
ctx.strokeStyle = '#0000FF';
ctx.lineWidth = 4;
ctx.strokeRect(250, 50, 150, 100);
// 3. 填充绿色圆形
ctx.fillStyle = '#00FF00';
ctx.beginPath();
ctx.arc(440, 100, 50, 0, Math.PI * 2);
ctx.fill();
// 4. 粉色线段
ctx.strokeStyle = '#FF00FF';
ctx.lineWidth = 6;
ctx.beginPath();
ctx.moveTo(50, 220);
ctx.lineTo(300, 220);
ctx.stroke();
// 5. 填充文字
ctx.font = '30px sans-serif';
ctx.fillStyle = '#000000';
ctx.fillText('Hello ArkGraphics 2D', 50, 350);
// 6. 绘制一个像素点
ctx.fillStyle = '#333333';
ctx.fillRect(380, 350, 1, 1);
// 7. 绘制一个点(另一种更专业的方式,使用 drawPoint,但需要更复杂的上下文设置)
// ctx.drawPoint(390, 350, 5); // drawPoint 的第三个参数是点的大小,不填默认是1
}
}
运行效果:屏幕上会显示一个红色矩形、一个蓝色边框矩形、一个绿色圆形、一条粉色线段、一行黑色文字以及一个灰色的像素点。
5. 踩坑记录(这是给新手的避坑指南)
坑1:Canvas 宽度设置失效,图形显示不全或偏移
现象:你设置了 Canvas(this.canvasRef).width('100%'),然后 ctx.fillRect(50, 50, 150, 100),但在真机上图形可能显示在左上角,或者部分在画布外。
原因:width('100%') 是声明式写法,绑定的是父容器的百分比。但在 onReady 回调触发时,Canvas的实际像素宽度(逻辑像素)可能还未被最终确定,或者你绘制的坐标是基于第一次布局的物理尺寸。如果你的父容器布局发生变化(例如键盘弹起),Canvas尺寸会变,但你的绘制坐标是写死的。
解法:
- 在
onReady中,先获取 Canvas 的准确尺寸,再进行绘制。 -
.onReady(() => { // 获取Canvas组件的实际宽高 const canvas = this.canvasRef; const width = canvas.width; // 这是Canvas的逻辑像素宽度 const height = canvas.height; // 这是Canvas的逻辑像素高度 // 现在你可以根据 width 和 height 动态计算坐标 this.drawAll(width, height); }) - 如果Canvas尺寸在页面加载后变化,你需要监听
onAreaChange事件,并在onAreaChange中重新绘制。
坑2:beginPath() 漏写导致图形错乱
现象:你画了一个圆(arc),紧接着画了一条线(lineTo),然后发现圆和线之间有一条莫名其妙的直线连接着,或者下一个图形覆盖了上一个图形的描边。
原因:Canvas的绘图模型是基于“路径”的。如果你不调用 beginPath(),下一次 moveTo 或 lineTo 会建立在上一次路径的末尾。例如,画完圆后,画笔停在圆的最后一个点,你直接 moveTo(0,0),然后 lineTo(100,100),实际上是画了从圆上某点到 (100,100) 的线。
解法:绘制每个独立图形前,强制调用 ctx.beginPath()。这是一个好习惯。
ctx.beginPath(); // 每次画新图形前必写
ctx.arc(...);
ctx.fill();
ctx.beginPath(); // 画线前再写一次
ctx.moveTo(...);
ctx.lineTo(...);
ctx.stroke();
6. 最佳实践
-
不要在
build()中频繁创建对象:CanvasRenderingContext2D实例应该在@Component中创建(如果用Canvas(this.canvasRef)的方式),或者使用@State装饰器管理。千万不要在build里写new CanvasRenderingContext2D(),ArkUI会频繁触发组件重建,导致画布不断重绘,性能极差。 -
把样式配置抽成函数:如果你的页面需要绘制多种颜色、线型的图形,不要在每个绘制步骤里重复写
ctx.fillStyle = '#000'; ctx.strokeStyle = '#FFF';。抽取一个函数,比如applyStyle(style: {fill: string, stroke: string, lineWidth: number}),接收配置对象进行设置。这样代码更清晰,也方便复用。 -
异步回调里不要直接修改Canvas的UI状态:比如你在网络请求的回调里,想去给Canvas重新绘制。但此时页面可能已经销毁了(例如用户返回了上一个页面),直接操作
ctx可能会导致崩溃。一定要检查Canvas的引用是否有效,或者在onDestroy生命周期中清理掉对ctx的外部引用。
7. Demo 入口
把你的 Index.ets 文件替换成上面的代码,直接运行即可。这是一个最简单的入门示例。
// entry/src/main/ets/pages/Index.ets
// 整体代码如上所示
8. FAQ(真实开发视角)
Q:为什么真机显示正常,但模拟器上绘制的文字显示不全?
A:模拟器和真机的字体文件可能不同。在模拟器上,sans-serif 可能被映射成一个不支持中文的字体,或者字号渲染机制有差异。建议真机测试,并在 font 设置中明确指定字体,例如 ctx.font = '30px HarmonyOS Sans SC'。
Q:为什么页面返回后,再重新进入,Canvas 是空的?
A:因为页面被销毁(onPageHide)后,Canvas组件也被销毁。再次进入时,onReady 会再次执行,你需要确保你的绘制数据(比如绘制参数)没有被销毁。建议使用 @State 或 AppStorage 存储数据,在 onReady 中恢复绘制。
Q:为什么我的 Canvas.clearRect() 清除区域后,页面变黑了?
A:Canvas.clearRect() 会把指定矩形区域内的像素设置为透明(全透明黑色)。如果你的Canvas没有设置背景色(backgroundColor),就会看到黑色。解决方案:在 Canvas 组件上设置 .backgroundColor('#FFFFFF') 或者使用 ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height) 来绘制背景色。
示例代码地址:项目地址
如果你也遇到类似问题,可以重点检查Canvas的 onReady 时机和 beginPath 的调用位置。这一点在实际项目调试中会反复出现。
更多推荐

所有评论(0)