实战:用 Canvas 绘制一个仪表盘

今天来做个实际的东西——用 Canvas 画一个仪表盘。仪表盘在很多 APP 里都能见到:汽车仪表、运动 APP 的速度表、智能家居的温度显示等等。

仪表盘绘制流程

下面是绘制仪表盘的完整流程:

创建Canvas上下文

清空画布

画背景弧灰色

计算进度弧角度

根据值选择颜色

画进度弧

画刻度线

画指针三角形

画中心圆

显示数值和单位

添加交互按钮

仪表盘长什么样?

一个典型的仪表盘包括:

  1. 弧形刻度盘——背景弧和进度弧
  2. 刻度线——长短不一的线段
  3. 指针——指向当前值
  4. 中心数字——显示当前数值

我们用 Canvas 来绘制这些元素。

第一步:设置基础变量

import { common } from '@kit.AbilityKit';
import { CanvasRenderingContext2D } from '@kit.ArkGraphics2D';

导入需要的模块。

@Entry
@Component
struct Dashboard {
  @State currentValue: number = 65;  // 当前值
  private maxValue: number = 100;     // 最大值
  private minValue: number = 0;       // 最小值

定义仪表盘的数据:当前值 65,范围 0-100。

  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(
    new RenderingContextSettings(true)
  );

创建 Canvas 渲染上下文。new RenderingContextSettings(true) 开启抗锯齿。

第二步:画背景弧

drawDashboard() {
  const ctx = this.context;
  const width = 300;
  const height = 300;
  const centerX = width / 2;
  const centerY = height / 2;
  const radius = 100;

  // 清空画布
  ctx.clearRect(0, 0, width, height);

先清空画布,准备重新绘制。

  // 画背景弧(灰色)
  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, Math.PI * 0.75, Math.PI * 2.25, false);
  ctx.strokeStyle = '#e0e0e0';
  ctx.lineWidth = 15;
  ctx.lineCap = 'round';
  ctx.stroke();

arc 的参数是:圆心坐标、半径、起始角度、结束角度、是否逆时针。

  • 起始角度 Math.PI * 0.75 = 135 度(左下角)
  • 结束角度 Math.PI * 2.25 = 405 度(右下角)
  • 总共 270 度的弧

lineCap = 'round' 让弧的两端是圆角,看起来更柔和。

第三步:画进度弧

  const startAngle = Math.PI * 0.75;
  const endAngle = startAngle + (this.currentValue / this.maxValue) * Math.PI * 1.5;

进度弧的结束角度 = 起始角度 + (当前值 / 最大值) × 270 度。Math.PI * 1.5 就是 270 度。

  // 根据值选择颜色(绿 -> 黄 -> 红)
  let color = '#4CAF50';  // 绿色
  if (this.currentValue > 70) {
    color = '#f44336';    // 红色
  } else if (this.currentValue > 40) {
    color = '#ff9800';    // 橙色
  }

根据值的大小选择颜色:低于 40 绿色,40-70 橙色,70 以上红色。这样用户一眼就能看出状态。

  ctx.beginPath();
  ctx.arc(centerX, centerY, radius, startAngle, endAngle, false);
  ctx.strokeStyle = color;
  ctx.lineWidth = 15;
  ctx.lineCap = 'round';
  ctx.stroke();

画进度弧,和背景弧一样的参数,只是颜色不同、角度范围不同。

第四步:画刻度线

  ctx.save();
  ctx.translate(centerX, centerY);
  ctx.rotate(Math.PI * 0.75);  // 旋转到起始角度

save 保存当前坐标系状态,translate 把原点移到圆心,rotate 旋转到起始角度。

  for (let i = 0; i <= 10; i++) {
    ctx.beginPath();
    if (i % 5 === 0) {
      // 长刻度
      ctx.moveTo(radius - 25, 0);
      ctx.lineTo(radius - 35, 0);
      ctx.strokeStyle = '#333333';
      ctx.lineWidth = 2;
    } else {
      // 短刻度
      ctx.moveTo(radius - 25, 0);
      ctx.lineTo(radius - 30, 0);
      ctx.strokeStyle = '#999999';
      ctx.lineWidth = 1;
    }
    ctx.stroke();

画 11 个刻度(0-10)。每 5 个是长刻度,其他是短刻度。moveTolineTo 的坐标是相对于旋转后的坐标系。

    // 画刻度数值
    if (i % 5 === 0) {
      ctx.save();
      ctx.translate(radius - 45, 0);
      ctx.rotate(-Math.PI * 0.75 - i * Math.PI * 0.15);  // 反旋转文字
      ctx.fillStyle = '#666666';
      ctx.font = '12px sans-serif';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(`${i * 10}`, 0, 0);
      ctx.restore();
    }

在长刻度旁边显示数值。因为整个坐标系已经旋转了,所以文字需要反旋转才能保持水平。

    ctx.rotate(Math.PI * 0.15);  // 每个刻度间隔 27 度
  }
  ctx.restore();

每画完一个刻度,旋转 27 度(Math.PI * 0.15)。最后 restore 恢复坐标系。

第五步:画指针

  const pointerAngle = startAngle + (this.currentValue / this.maxValue) * Math.PI * 1.5;
  const pointerLength = radius - 40;

指针的角度和进度弧一样,长度比半径短 40 像素。

  ctx.save();
  ctx.translate(centerX, centerY);
  ctx.rotate(pointerAngle);

  ctx.beginPath();
  ctx.moveTo(0, -3);
  ctx.lineTo(pointerLength, 0);
  ctx.lineTo(0, 3);
  ctx.closePath();
  ctx.fillStyle = color;
  ctx.fill();

指针是一个三角形:从中心点向当前角度方向画一个细长的三角形。moveTo(0, -3)lineTo(0, 3) 控制指针的宽度。

  // 指针中心圆
  ctx.beginPath();
  ctx.arc(0, 0, 8, 0, Math.PI * 2);
  ctx.fillStyle = '#333333';
  ctx.fill();

  ctx.restore();

在中心画一个小圆,盖住指针的根部。

第六步:显示数值

  // 显示当前数值
  ctx.fillStyle = '#333333';
  ctx.font = 'bold 36px sans-serif';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(`${this.currentValue}`, centerX, centerY + 50);

在仪表盘下方显示当前数值,用大号粗体字。

  // 单位
  ctx.fillStyle = '#999999';
  ctx.font = '14px sans-serif';
  ctx.fillText('km/h', centerX, centerY + 70);
}

在数值下方显示单位。

第七步:添加交互

build() {
  Column() {
    Text('速度表')
      .fontSize(20)
      .margin({ top: 20 })

    Canvas(this.context)
      .width(300)
      .height(300)
      .onReady(() => {
        this.drawDashboard();
      })

Canvas 的 onReady 回调在画布准备好后触发,我们在这里开始绘制。

    // 模拟数值变化
    Button('加速')
      .margin({ top: 20 })
      .onClick(() => {
        this.currentValue = Math.min(this.currentValue + 10, this.maxValue);
        this.drawDashboard();
      })

    Button('减速')
      .margin({ top: 10 })
      .onClick(() => {
        this.currentValue = Math.max(this.currentValue - 10, this.minValue);
        this.drawDashboard();
      })
  }
  .width('100%')
  .height('100%')
}

添加两个按钮,点击后修改 currentValue 并重新绘制。

颜色变化逻辑

仪表盘的颜色会根据数值变化:

0-40

40-70

70-100

获取当前数值

数值范围判断

绿色正常状态

橙色警告状态

红色危险状态

应用绿色进度弧

应用橙色进度弧

应用红色进度弧

用户直观判断状态

进阶改进

如果你想做得更精致,可以:

  1. 加渐变:进度弧用渐变色,从绿色渐变到红色
  2. 加动画:用 requestAnimationFrame 让指针平滑过渡
  3. 加阴影:给指针和数字加阴影,增加立体感
  4. 加刻度文字:在长刻度旁边显示数值(0, 50, 100)

进阶改进方向

仪表盘可以通过以下方式进一步优化:

基础仪表盘

添加渐变色

添加动画效果

添加阴影立体感

添加刻度文字

绿色到红色渐变

requestAnimationFrame平滑过渡

指针和数字阴影

显示0,50,100等数值

更精致的视觉效果

小结

仪表盘的绘制核心:

  1. drawArc 画弧形背景和进度
  2. save / restore + rotate 画刻度线
  3. 三角形路径画指针
  4. drawText 显示数值

Canvas 的能力远不止这些,但掌握了 arclinesave/restore、坐标变换这几个基础,你就能画出大部分自定义 UI 了。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐