HarmonyOS APP实战-画图APP - 第3篇:矩形与圆形绘制

HarmonyOS APP实战-画图APP - 第3篇:实现基本形状绘制

1. 开篇

上一篇我们完成了画图APP的主页面布局,构建了顶部工具栏、底部颜色选择栏和中间画布区域的基础框架。顶部工具栏预留了形状切换按钮,底部颜色栏提供了基本的颜色选项,画布区域能够响应触摸事件并显示触摸坐标。页面整体交互骨架已经建立,但画布还无法真正绘制图形。

本篇将在现有框架上注入绘制能力。我们将使用 @ohos.graphics.drawing 模块提供的自绘制API,实现矩形、正方形和圆形的绘制功能。具体包括:通过触摸事件的按下和抬起位置确定形状的包围盒,调用 drawRect 和 drawCircle API进行实际绘制,并支持底部颜色选择栏改变填充颜色。完成后,点击工具栏的形状按钮,在画布上拖拽就能生成对应的几何图形。

2. 核心实现

2.1 基础配置 --- 建立绘制模块依赖与初始化

在画图APP的工程目录下,我们需要新建一个专门的绘制工具模块,用于封装所有图形绘制逻辑。同时配置 Canvas 组件和 @ohos.graphics.drawing 相关的导入。

创建 ShapeDrawer.ets 文件:

typescript 复制代码
// ShapeDrawer.ets
// 本模块封装矩形、圆形等基本形状的绘制方法
// 依赖 @ohos.graphics.drawing 库中的 Canvas 和画笔相关类

import { Canvas, Brush, Pen, Point, Rect } from '@ohos.graphics.drawing';
// Canvas: 画布对象,所有绘制操作基于它进行
// Brush: 填充画笔,用于设置填充颜色、透明度等
// Pen: 描边画笔,用于设置线条颜色、宽度等
// Point: 坐标点对象
// Rect: 矩形区域对象

export class ShapeDrawer {
  private canvas: Canvas | null = null;

  /**
   * 初始化绘制器,绑定画布实例
   * @param canvas - 由 @ohos.graphics.drawing 创建的 Canvas 对象
   */
  public init(canvas: Canvas): void {
    this.canvas = canvas;
    console.info('[ShapeDrawer] Canvas初始化完成');
  }

  /**
   * 绘制填充矩形
   * @param left - 矩形左上角x坐标
   * @param top - 矩形左上角y坐标
   * @param width - 矩形宽度
   * @param height - 矩形高度
   * @param color - 填充颜色,格式为RGB十六进制字符串,如 '#FF0000'
   */
  public drawFillRect(left: number, top: number,
                      width: number, height: number,
                      color: string): void {
    if (!this.canvas) {
      console.error('[ShapeDrawer] Canvas未初始化');
      return;
    }
    
    // 创建填充画笔Brush,设置颜色
    const brush = new Brush();
    brush.setColor(this.parseColor(color));
    
    // 创建矩形区域对象,使用左上角坐标和宽高
    const rect = new Rect(left, top, left + width, top + height);
    
    // 保存当前画布状态,避免后续操作污染
    this.canvas.save();
    // 设置Brush为绘制填充时使用的画笔
    this.canvas.attachBrush(brush);
    // 调用drawRect绘制填充矩形
    this.canvas.drawRect(rect);
    // 分离画笔,恢复画布状态
    this.canvas.detachBrush();
    this.canvas.restore();
    
    console.info(`[ShapeDrawer] 绘制矩形: left=${left}, top=${top}, w=${width}, h=${height}, color=${color}`);
  }

  /**
   * 绘制填充圆形
   * @param cx - 圆心x坐标
   * @param cy - 圆心y坐标
   * @param radius - 圆半径
   * @param color - 填充颜色
   */
  public drawFillCircle(cx: number, cy: number,
                        radius: number, color: string): void {
    if (!this.canvas) {
      console.error('[ShapeDrawer] Canvas未初始化');
      return;
    }
    
    const brush = new Brush();
    brush.setColor(this.parseColor(color));
    
    this.canvas.save();
    this.canvas.attachBrush(brush);
    // 使用drawCircle API绘制圆形
    // 参数依次为:圆心坐标点、半径、描边画笔(传null使用填充)、填充画笔
    this.canvas.drawCircle(
      new Point(cx, cy), // 圆心坐标
      null,              // Pen描边画笔,不需要轮廓
      brush              // Brush填充画笔
    );
    this.canvas.detachBrush();
    this.canvas.restore();
    
    console.info(`[ShapeDrawer] 绘制圆形: cx=${cx}, cy=${cy}, r=${radius}, color=${color}`);
  }

  /**
   * 将十六进制颜色字符串转为Color对象
   * @param hex - 十六进制颜色值
   * @returns Color对象
   */
  private parseColor(hex: string): Color {
    // 兼容带#前缀的颜色值
    const colorStr = hex.startsWith('#') ? hex.substring(1) : hex;
    // 支持6位标准色和3位简写色
    if (colorStr.length === 3) {
      const r = parseInt(colorStr[0] + colorStr[0], 16);
      const g = parseInt(colorStr[1] + colorStr[1], 16);
      const b = parseInt(colorStr[2] + colorStr[2], 16);
      return new Color(r, g, b, 255);
    } else if (colorStr.length === 6) {
      const r = parseInt(colorStr.substring(0, 2), 16);
      const g = parseInt(colorStr.substring(2, 4), 16);
      const b = parseInt(colorStr.substring(4, 6), 16);
      return new Color(r, g, b, 255);
    }
    // 默认返回白色
    return new Color(255, 255, 255, 255);
  }
}

关键点说明:

  • 使用 @ohos.graphics.drawing 中的 Canvas、Brush、Pen、Point、Rect 类。这是 ArkGraphics 2D 提供自绘能力的核心模块。
  • Brush.setColor 方法接受 Color 对象,我们需要将常用的十六进制颜色字符串转为 Color。
  • drawRect 接收一个 Rect 对象定义矩形区域。
  • drawCircle 的签名比较特殊:第一个参数是 Point 圆心坐标,第二个是 Pen(描边画笔,传入 null 表示不描边),第三个是 Brush(填充画笔)。注意参数顺序和类型。
  • 每次绘制前后调用 save() attachBrush() detachBrush() restore() 是常规操作模式,确保不同绘制之间不会互相影响状态。

2.2 核心逻辑 --- 实现触摸绘制形状的交互

现在进入画布页面的核心绘制逻辑。我们需要在上一篇文章建立的 DrawingCanvas 组件中,捕获用户的触摸事件,根据按下的起始点和抬起时的终点确定图形的尺寸和位置,然后调用 ShapeDrawer 进行绘制。

在 DrawingCanvas.ets 中实现触摸绘制逻辑:

typescript 复制代码
// DrawingCanvas.ets
// 画布绘制组件,实现触摸手势绘制矩形和圆形

import { Canvas, Brush, Pen, Point, Rect } from '@ohos.graphics.drawing';
import { ShapeDrawer } from './ShapeDrawer';

@Component
export struct DrawingCanvas {
  // 当前选择的绘制模式:'rect' | 'circle' | 'square'
  @Prop shapeType: string = 'rect';
  // 当前选中的颜色值
  @Prop currentColor: string = '#FF0000';
  
  // 触摸起始坐标
  private startX: number = 0;
  private startY: number = 0;
  // 触摸结束坐标
  private endX: number = 0;
  private endY: number = 0;
  // 画布引用,用于
  private canvasRef: Canvas | null = null;
  // 绘制工具实例
  private drawer: ShapeDrawer = new ShapeDrawer();
  
  build() {
    Column() {
      // 画布区域
      Canvas()
        .width('100%')
        .height('100%')
        .backgroundColor('#FFFFFF')
        .onReady(() => {
          // Canvas组件渲染完成时获取Canvas实例
          // 实际项目中需要通过Canvas组件的onReady回调获取绘图上下文
          // 这里简化处理,假设this.canvasRef已正确赋值
          if (this.canvasRef) {
            this.drawer.init(this.canvasRef);
          }
        })
        .onTouch((event: TouchEvent) => {
          this.handleCanvasTouch(event);
        })
    }
    .width('100%')
    .height('100%')
  }

  /**
   * 处理画布上的触摸事件
   * 按下时记录起始点,抬起时根据形状类型绘制
   */
  private handleCanvasTouch(event: TouchEvent): void {
    const touchInfo = event.getTouches()[0];
    if (!touchInfo) {
      return;
    }
    
    const x = touchInfo.x;
    const y = touchInfo.y;
    
    if (event.type === TouchType.Down) {
      // 记录按下位置
      this.startX = x;
      this.startY = y;
      console.info(`[DrawingCanvas] 触摸按下: (${x}, ${y})`);
    } else if (event.type === TouchType.Up) {
      // 记录抬起位置
      this.endX = x;
      this.endY = y;
      
      // 计算形状的包围盒参数
      const left = Math.min(this.startX, this.endX);
      const top = Math.min(this.startY, this.endY);
      const width = Math.abs(this.endX - this.startX);
      const height = Math.abs(this.endY - this.startY);
      
      console.info(`[DrawingCanvas] 触摸抬起: (${this.endX}, ${this.endY})`);
      
      // 根据当前选中的形状类型执行绘制
      switch (this.shapeType) {
        case 'rect':
          this.drawer.drawFillRect(left, top, width, height, this.currentColor);
          break;
        case 'square':
          // 正方形:取宽高中的较大者,使长宽相等
          const side = Math.max(width, height);
          this.drawer.drawFillRect(left, top, side, side, this.currentColor);
          break;
        case 'circle':
          // 圆形:计算圆心坐标和半径
          const cx = (this.startX + this.endX) / 2;
          const cy = (this.startY + this.endY) / 2;
          const radius = Math.max(width, height) / 2;
          this.drawer.drawFillCircle(cx, cy, radius, this.currentColor);
          break;
        default:
          console.warn(`[DrawingCanvas] 未知的形状类型: ${this.shapeType}`);
      }
    }
  }
}

关键点说明:

  • onTouch 事件处理器在每次触摸时都会触发,通过 event.type 判断是按下(TouchType.Down)还是抬起(TouchType.Up)。
  • 绘制时用 Math.min 和 Math.abs 计算包围盒,确保无论用户从哪个方向拖拽,都能正确圈出区域。
  • 正方形模式强制宽高相等,取用户拖拽范围的较大边长。
  • 圆形模式使用拖拽范围的包围盒对角线,以中心点为圆心进行计算。
  • this.canvasRef 的获取在 onReady 回调中,这是获取 Canvas 上下文的标准方式。实际项目中这里需要更详细的初始化和上下文创建,为了示例清晰做了简化。

2.3 完整模块 --- 集成绘制与页面传参

最后,我们需要将绘制模块和主页面进行完整集成。主页面 Index.ets 需要传递用户选择的形状类型和颜色值到 DrawingCanvas 组件,并触发重新绘制。以下是一个集成后的完整主页面代码。

在 Index.ets 中集成绘制功能:

typescript 复制代码
// Index.ets
// 画图APP主页面,集成顶部工具栏、底部颜色栏和画布绘制区域

import { DrawingCanvas } from './DrawingCanvas';

@Entry
@Component
struct Index {
  // 当前绘制模式:'rect' | 'circle' | 'square'
  @State currentShape: string = 'rect';
  // 当前选中的颜色,初始为红色
  @State currentColor: string = '#FF0000';
  
  build() {
    Column() {
      // ========== 顶部工具栏 ==========
      Row() {
        // 矩形按钮
        Button('矩形')
          .onClick(() => {
            this.currentShape = 'rect';
            console.info(`[Index] 切换到矩形绘制模式`);
          })
        // 正方形按钮
        Button('正方形')
          .onClick(() => {
            this.currentShape = 'square';
            console.info(`[Index] 切换到正方形绘制模式`);
          })
        // 圆形按钮
        Button('圆形')
          .onClick(() => {
            this.currentShape = 'circle';
            console.info(`[Index] 切换到圆形绘制模式`);
          })
      }
      .width('100%')
      .padding({ left: 10, right: 10 })
      .backgroundColor('#F0F0F0')
      
      // ========== 画布绘制区域 ==========
      DrawingCanvas({
        shapeType: $currentShape,
        currentColor: $currentColor
      })
      .layoutWeight(1)
      
      // ========== 底部颜色选择栏 ==========
      Row() {
        // 六个常用颜色按钮
        this.colorButton('#FF0000', '红色')
        this.colorButton('#00FF00', '绿色')
        this.colorButton('#0000FF', '蓝色')
        this.colorButton('#FFFF00', '黄色')
        this.colorButton('#FF00FF', '紫色')
        this.colorButton('#000000', '黑色')
      }
      .width('100%')
      .padding({ top: 10, bottom: 10 })
      .backgroundColor('#333333')
      .justifyContent(FlexAlign.SpaceEvenly)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#E0E0E0')
  }
  
  /**
   * 底部颜色按钮构建方法
   * @param color - 颜色值
   * @param label - 按钮文字
   */
  @Builder
  colorButton(color: string, label: string) {
    Column() {
      Circle()
        .width(40)
        .height(40)
        .fill(color)
        .margin({ bottom: 4 })
      Text(label)
        .fontSize(10)
        .fontColor('#FFFFFF')
    }
    .onClick(() => {
      this.currentColor = color;
      console.info(`[Index] 选中颜色: ${label} (${color})`);
    })
  }
}

关键点说明:

  • 主页面通过 @State 装饰器响应式管理 currentShape 和 currentColor 状态。
  • 点击顶部形状按钮时更新 currentShape,通过 $currentShape 语法将状态同步到子组件 DrawingCanvas。
  • 底部颜色栏设计为 Circle 组件加文字标签,点击时更新 currentColor。
  • Circle 是内置组件,用于显示预设颜色的色块,方便用户选择。
  • DrawingCanvas 接收这些参数后,在触摸抬起时根据当前形状和颜色绘制对应图形。

3. 运行验证

在 DevEco Studio 中运行项目,预期表现如下:

  1. 启动 APP,看到熟悉的顶部工具栏、白色画布和底部颜色栏。
  2. 点击顶部的"矩形"按钮(未高亮显示为选中态,下一篇将完善交互反馈),然后在画布上从左上方到右下方拖拽手指。抬起手指后,画布上应该出现一个红色的填充矩形,且矩形区域正好覆盖你拖拽的范围。
  3. 点击"正方形"按钮,再拖拽一个任意矩形区域,画布上应该出现一个以较长边为边长的正方形。
  4. 点击"圆形"按钮,拖拽后画布上生成一个填充圆形,其直径等于拖拽范围的较长边。
  5. 点击底部颜色栏的"绿色"按钮,再绘制一个矩形,新矩形的填充色变为绿色,且不影响之前绘制的红色矩形。

4. 小结与预告

本篇在现有画布基础上,成功实现了基本形状的绘制功能。核心是创建了 ShapeDrawer 工具模块,封装了基于 @ohos.graphics.drawing 的 drawRect 和 drawCircle API;并在 DrawingCanvas 组件中集成了触摸事件处理,根据用户拖拽范围生成矩形、正方形和圆形;同时后台颜色栏的状态已经可以影响绘制填充色,虽然当前仅支持6种固定颜色。这些功能为画图APP打下了最基础的绘制能力,后续所有高级功能都将建立在这个绘制管道之上。

下一篇将聚焦"实现触摸手绘功能"。我们会脱离预定义形状,允许用户像真正的画笔一样自由绘制轨迹。逻辑上需要记录连续的触摸点,利用 Path 对象和 drawPath API 实现平滑的曲线渲染。届时,画图APP将同时支持"几何形状绘制"和"自由手绘"两种模式。

相关推荐
梦想不只是梦与想2 小时前
HarmonyOS应用分层架构设计
harmonyos·分层架构·一次开发,多端部署
MardaWang4 小时前
当滚动逃离了框架 ——HarmonyOS Web 与原生混排滚动的冲突本质与解法
harmonyos·arkts·鸿蒙·deveco studio
tsqtsqtsq03096 小时前
DevEco Studio 介绍
harmonyos
HwJack209 小时前
【共创稿事节】HarmonyOS 7文旅展陈展厅大空间 3DGS 重建的分块策略与拼接踩坑
3d·华为·harmonyos
熊猫钓鱼>_>11 小时前
Kotlin Multiplatform for OpenHarmony 实战:为 kotlin-inject 实现依赖注入适配
开发语言·华为·kotlin·ai编程·inject·鸿蒙·openharmony
m0_7381858211 小时前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体
m0_7381858212 小时前
Flutter 鸿蒙化实战:just_audio 适配 OpenHarmony,功能强大的播放器
flutter·华为·harmonyos·鸿蒙
翼辉cto12 小时前
Kotlin Multiplatform 三方库 SQLDelight 的 OpenHarmony 鸿蒙化适配实战
开发语言·kotlin·harmonyos
SuperHeroWu712 小时前
TraeCode 国内版接入 DevEco CLI:用官方知识开发鸿蒙应用
ai编程·harmonyos·知识库·trae·aicoding·skills·deveco cli