Administrator
发布于 2026-09-01 / 59 阅读
0
0

鸿蒙 Canvas 2D 实战:从路径绘制到离屏渲染的性能跃升

系列文章《鸿蒙开发从入门到专家》第 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 会占用显存,反而得不偿失。

五、实战:一个带平滑轨迹的波形图

把上面的能力串起来,做一个实时波形组件:

  1. 用 OffscreenCanvas 预渲染网格背景。

  2. 主线程每 50ms 采集一个数据点,加入队列。

  3. 用 requestAnimationFrame 把队列里的点连成二次贝塞尔曲线。

  4. 超过屏幕宽度的点从队列头部移除。

这个结构可以复用到心率图、音频可视化、股票分时线等场景。核心不是 API 多复杂,而是把“静态绘制、动画驱动、离屏缓存”三个环节拆清楚。

小结

Canvas 在鸿蒙开发里属于“知道就能用,不知道就卡壳”的能力。本文覆盖了三个关键用法:

  • Path2D + 贝塞尔曲线,让轨迹绘制更自然。

  • requestAnimationFrame 驱动逐帧动画。

  • OffscreenCanvas 做离屏缓存,降低主线程负载。

下一篇我们进入并发编程,看看鸿蒙的 TaskPool 和 Worker 怎么帮你把耗时任务从 UI 线程剥离开。


评论