在这里插入图片描述

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特效:绘制复杂的不规则形状、渐变背景。

不适合的场景:如果你只是展示静态图片或简单的文字排版,ImageText 组件更合适。它们有更好的性能优化(如硬件加速,但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 函数中添加绘制逻辑。官方文档重点提到了 drawRectdrawCircledrawText 以及 fillStylestrokeStyle

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, 半径, 起始角度, 结束角度, 是否逆时针)0Math.PI * 2 代表一个完整的圆。
  • fillRectstrokeRect 是独立的API,分别对应“填充矩形”和“描边矩形”。drawRectfillRect 作用相同,推荐优先使用 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(),下一次 moveTolineTo 会建立在上一次路径的末尾。例如,画完圆后,画笔停在圆的最后一个点,你直接 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. 最佳实践

  1. 不要在 build() 中频繁创建对象CanvasRenderingContext2D 实例应该在 @Component 中创建(如果用 Canvas(this.canvasRef) 的方式),或者使用 @State 装饰器管理。千万不要在 build 里写 new CanvasRenderingContext2D(),ArkUI会频繁触发组件重建,导致画布不断重绘,性能极差。

  2. 把样式配置抽成函数:如果你的页面需要绘制多种颜色、线型的图形,不要在每个绘制步骤里重复写 ctx.fillStyle = '#000'; ctx.strokeStyle = '#FFF';。抽取一个函数,比如 applyStyle(style: {fill: string, stroke: string, lineWidth: number}),接收配置对象进行设置。这样代码更清晰,也方便复用。

  3. 异步回调里不要直接修改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 会再次执行,你需要确保你的绘制数据(比如绘制参数)没有被销毁。建议使用 @StateAppStorage 存储数据,在 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 的调用位置。这一点在实际项目调试中会反复出现。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐