鸿蒙技术干货10:鸿蒙图形渲染基础,Canvas绘图与自定义组件实战

图形渲染是提升应用交互体验的核心技能,而 Canvas 组件作为鸿蒙图形渲染的基础载体,能实现从简单绘图到复杂自定义组件的各类需求。掌握 Canvas 绘图逻辑与自定义组件开发,能让你的应用在视觉呈现和功能扩展性上更上一层楼。本文将从 Canvas 基础绘图入手,逐步过渡到自定义组件开发,最终实现实用的「手写签名组件」。

一、核心认知:Canvas 与自定义组件的应用价值

1. 应用场景

  • 自定义可视化组件(如签名板、绘图工具、进度条)
  • 数据可视化图表(如折线图、饼图的底层绘制)
  • 交互型图形(如手写输入、涂鸦功能)
  • 个性化 UI 元素(如不规则按钮、动态图形效果)

2. 核心技术栈

  • 基础组件:Canvas(绘图容器)、CanvasRenderingContext2D(绘图上下文)
  • 自定义组件:继承 Component 并重写 onDraw 方法
  • 关键能力:路径绘制、样式设置、事件监听、图片导出

二、Canvas 组件基础:四大核心绘图能力

Canvas 绘图的核心是通过 CanvasRenderingContext2D 上下文对象操作,以下是线条、矩形、圆形、文字的基础绘制逻辑,代码可直接复用:

1. 初始化 Canvas 上下文

typescript 复制代码
import { Canvas, CanvasRenderingContext2D } from '@ohos.ui.canvas';

@Component
struct CanvasBasicDemo {
  private canvasContext: CanvasRenderingContext2D | null = null;

  // 初始化上下文
  private initContext(canvas: Canvas) {
    this.canvasContext = canvas.getContext('2d');
    if (!this.canvasContext) {
      console.error('Canvas 上下文初始化失败');
    }
  }

  build() {
    Canvas(this.initContext.bind(this))
      .width('100%')
      .height(400)
      .backgroundColor('#f5f5f5')
  }
}

2. 绘制线条(手写签名基础)

typescript 复制代码
// 绘制连续线条
private drawLine(startX: number, startY: number, endX: number, endY: number) {
  if (!this.canvasContext) return;
  const ctx = this.canvasContext;
  
  // 设置线条样式
  ctx.beginPath(); // 开始新路径
  ctx.moveTo(startX, startY); // 起点
  ctx.lineTo(endX, endY); // 终点
  ctx.strokeStyle = '#2f54eb'; // 线条颜色
  ctx.lineWidth = 3; // 线条宽度
  ctx.lineCap = 'round'; // 线条端点圆角
  ctx.lineJoin = 'round'; // 线条交点圆角
  ctx.stroke(); // 执行绘制
}

3. 绘制矩形与圆形

ini 复制代码
// 绘制矩形(填充+描边)
private drawRect(x: number, y: number, width: number, height: number) {
  if (!this.canvasContext) return;
  const ctx = this.canvasContext;
  
  ctx.fillStyle = 'rgba(47, 84, 235, 0.2)'; // 填充颜色(半透明)
  ctx.fillRect(x, y, width, height); // 填充矩形
  
  ctx.strokeStyle = '#2f54eb';
  ctx.lineWidth = 2;
  ctx.strokeRect(x, y, width, height); // 描边矩形
}

// 绘制圆形(填充+描边)
private drawCircle(x: number, y: number, radius: number) {
  if (!this.canvasContext) return;
  const ctx = this.canvasContext;
  
  ctx.beginPath();
  ctx.arc(x, y, radius, 0, Math.PI * 2); // 圆心(x,y),半径radius,0到360度
  ctx.fillStyle = 'rgba(255, 77, 79, 0.2)';
  ctx.fill();
  
  ctx.strokeStyle = '#ff4d4f';
  ctx.lineWidth = 2;
  ctx.stroke();
}

4. 绘制文字

ini 复制代码
private drawText(text: string, x: number, y: number) {
  if (!this.canvasContext) return;
  const ctx = this.canvasContext;
  
  ctx.font = '20px sans-serif'; // 字体大小+字体族
  ctx.fillStyle = '#333333'; // 文字颜色
  ctx.textAlign = 'center'; // 水平居中
  ctx.textBaseline = 'middle'; // 垂直居中
  ctx.fillText(text, x, y); // 绘制文字
}

三、自定义组件开发:重写 onDraw 与事件响应

自定义组件是在 Canvas 基础上封装独立功能模块,核心是重写 onDraw 方法实现绘图逻辑,并绑定触摸事件实现交互:

1. 自定义组件基础结构

kotlin 复制代码
import { Component, Canvas, CanvasRenderingContext2D, TouchEvent } from '@ohos.ui.canvas';

// 自定义手写签名组件
@Component
export struct SignaturePad extends Component {
  // 组件属性(外部可配置)
  private lineWidth: number = 3;
  private lineColor: string = '#2f54eb';
  private bgColor: string = '#ffffff';
  
  // 内部状态
  private ctx: CanvasRenderingContext2D | null = null;
  private isDrawing: boolean = false;
  private lastX: number = 0;
  private lastY: number = 0;

  // 重写onDraw方法(组件绘制入口)
  override onDraw(canvas: Canvas) {
    this.ctx = canvas.getContext('2d');
    if (!this.ctx) return;
    
    // 绘制背景
    const { width, height } = canvas.getBoundingClientRect();
    this.ctx.fillStyle = this.bgColor;
    this.ctx.fillRect(0, 0, width, height);
  }

  // 触摸事件响应(核心交互)
  private handleTouchStart(e: TouchEvent) {
    this.isDrawing = true;
    // 获取触摸起始坐标
    const { x, y } = e.touches[0];
    this.lastX = x;
    this.lastY = y;
  }

  private handleTouchMove(e: TouchEvent) {
    if (!this.isDrawing || !this.ctx) return;
    const { x, y } = e.touches[0];
    
    // 绘制当前线段(从上次坐标到当前坐标)
    this.ctx.beginPath();
    this.ctx.moveTo(this.lastX, this.lastY);
    this.ctx.lineTo(x, y);
    this.ctx.strokeStyle = this.lineColor;
    this.ctx.lineWidth = this.lineWidth;
    this.ctx.lineCap = 'round';
    this.ctx.lineJoin = 'round';
    this.ctx.stroke();
    
    // 更新上次坐标
    this.lastX = x;
    this.lastY = y;
  }

  private handleTouchEnd() {
    this.isDrawing = false;
  }

  build() {
    Canvas(this.onDraw.bind(this))
      .width('100%')
      .height(300)
      .onTouchStart(this.handleTouchStart.bind(this))
      .onTouchMove(this.handleTouchMove.bind(this))
      .onTouchEnd(this.handleTouchEnd.bind(this))
  }
}

2. 扩展功能:笔触配置与图片保存

给自定义组件添加笔触粗细切换、颜色选择、清空、保存图片功能:

kotlin 复制代码
// 续上SignaturePad组件代码
export struct SignaturePad extends Component {
  // 新增属性与状态
  @Link lineWidth: number; // 双向绑定外部粗细配置
  @Link lineColor: string; // 双向绑定外部颜色配置
  private canvasRef: Canvas | null = null;

  // 清空画布
  public clear() {
    if (!this.ctx) return;
    const { width, height } = this.canvasRef!.getBoundingClientRect();
    this.ctx.clearRect(0, 0, width, height); // 清空整个画布
    this.ctx.fillStyle = this.bgColor;
    this.ctx.fillRect(0, 0, width, height); // 重新绘制背景
  }

  // 保存签名为图片(base64格式)
  public async saveAsImage(): Promise<string | null> {
    if (!this.canvasRef) return null;
    try {
      // 导出图片(格式png,质量1.0)
      const imageData = await this.canvasRef.toDataURL('image/png', 1.0);
      console.log('签名图片保存成功');
      return imageData;
    } catch (err) {
      console.error('签名图片保存失败:', err);
      return null;
    }
  }

  // 重写onDraw,更新画布引用
  override onDraw(canvas: Canvas) {
    this.canvasRef = canvas;
    this.ctx = canvas.getContext('2d');
    // 背景绘制逻辑同上...
  }

  // build方法不变,外部通过组件实例调用clear和saveAsImage
}

四、实战整合:手写签名组件完整应用

将自定义签名组件与配置面板结合,实现完整的手写签名功能:

scss 复制代码
@Entry
@Component
struct SignatureDemoPage {
  @State lineWidth: number = 3;
  @State lineColor: string = '#2f54eb';
  private signaturePadRef: SignaturePad | null = null;

  build() {
    Column({ space: 20 })
      .width('100%')
      .height('100%')
      .padding(30)
      .backgroundColor('#f5f5f5') {
      
      Text('手写签名组件演示')
        .fontSize(32)
        .fontWeight(FontWeight.Bold)
        .textAlign(TextAlign.Center)
        .width('100%')
      
      // 自定义签名组件
      SignaturePad(
        lineWidth: $lineWidth,
        lineColor: $lineColor,
        ref: (ref) => this.signaturePadRef = ref
      )
        .width('100%')
        .height(300)
        .border({ width: 1, color: '#eee' })
        .borderRadius(12)
      
      // 配置面板
      Column({ space: 15 })
        .width('100%')
        .padding(20)
        .backgroundColor('#ffffff')
        .borderRadius(12) {
        
        // 笔触粗细调节
        Row({ space: 15, alignItems: ItemAlign.Center }) {
          Text('笔触粗细:')
            .fontSize(18)
            .width('30%')
          Slider()
            .width('70%')
            .min(1)
            .max(10)
            .value(this.lineWidth)
            .onChange((value) => this.lineWidth = value)
          Text(`${this.lineWidth}px`)
            .fontSize(16)
        }
        
        // 笔触颜色选择
        Row({ space: 15, alignItems: ItemAlign.Center }) {
          Text('笔触颜色:')
            .fontSize(18)
            .width('30%')
          Row({ space: 10 }) {
            ['#2f54eb', '#ff4d4f', '#36cfc9', '#ffc53d', '#000000'].forEach(color => {
              View()
                .width(30)
                .height(30)
                .backgroundColor(color)
                .borderRadius(15)
                .border(this.lineColor === color ? { width: 2, color: '#333' } : null)
                .onClick(() => this.lineColor = color)
            })
          }
        }
      }
      
      // 操作按钮
      Row({ space: 30 })
        .width('100%')
        .justifyContent(FlexAlign.Center) {
        Button('清空签名')
          .type(ButtonType.Capsule)
          .width(150)
          .height(50)
          .backgroundColor('#ff4d4f')
          .onClick(() => this.signaturePadRef?.clear())
        
        Button('保存签名')
          .type(ButtonType.Capsule)
          .width(150)
          .height(50)
          .backgroundColor('#2f54eb')
          .onClick(async () => {
            const imageData = await this.signaturePadRef?.saveAsImage();
            if (imageData) {
              Toast.show({ message: '签名保存成功' });
              // 后续可将imageData上传服务器或本地存储
            } else {
              Toast.show({ message: '签名保存失败' });
            }
          })
      }
    }
  }
}

五、实战踩坑指南

1. Canvas 上下文获取失败

  • 确保 Canvas 组件已渲染完成再获取上下文,避免在 build 阶段直接调用;
  • 检查 API 版本,getContext('2d') 支持 API9+,低版本需使用 getContext('2d', { compatible: true })

2. 绘制线条不流畅

  • 开启 lineCap: 'round'lineJoin: 'round',避免线条端点和交点出现锯齿;
  • 触摸事件 onTouchMove 会频繁触发,无需额外节流(鸿蒙已优化),但需确保绘制逻辑简洁。

3. 图片保存失败

  • 确保 Canvas 组件有实际绘制内容,空白画布可能导出失败;
  • 保存图片需申请文件读写权限(ohos.permission.WRITE_USER_DATA),若需上传服务器可直接使用 base64 格式。

4. 组件适配问题

  • Canvas 尺寸建议使用百分比或自适应布局,避免固定像素导致不同设备显示异常;
  • 组件重绘时需先清空画布(clearRect),再重新绘制,避免内容叠加混乱。

加入班级,学习鸿蒙开发

相关推荐
赵浩生2 小时前
鸿蒙技术干货9:deviceInfo 设备信息获取与位置提醒 APP 整合
harmonyos
BlackWolfSky2 小时前
鸿蒙暂未归类知识记录
华为·harmonyos
L、2184 小时前
Flutter 与开源鸿蒙(OpenHarmony):跨平台开发的新未来
flutter·华为·开源·harmonyos
L、2185 小时前
Flutter 与 OpenHarmony 深度融合实践:打造跨生态高性能应用(进阶篇)
javascript·flutter·华为·智能手机·harmonyos
威哥爱编程5 小时前
【鸿蒙开发案例篇】火力全开:鸿蒙6.0游戏开发战术手册
harmonyos·arkts·arkui
遇到困难睡大觉哈哈6 小时前
HarmonyOS —— Remote Communication Kit 定制数据传输(TransferConfiguration)实战笔记
笔记·华为·harmonyos
盐焗西兰花6 小时前
鸿蒙学习实战之路-HarmonyOS包转换全攻略
harmonyos
FrameNotWork6 小时前
【HarmonyOS 状态管理超全解析】从 V1 到 V2,一次讲清 @State/@Observed/@Local…等所有装饰器!附超易懂示例!
华为·harmonyos
威哥爱编程7 小时前
【鸿蒙开发案例篇】基于MindSpore Lite的端侧人物图像分割案例
harmonyos·arkts·arkui