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

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

1. 开篇

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

本篇将在现有框架上注入绘制能力。我们将使用 @ohos.graphics.drawing 模块提供的自绘制API,实现矩形、正方形和圆形的绘制功能。具体包括:通过触摸事件的按下和抬起位置确定形状的包围盒,调用 drawRectdrawCircle 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 中的 CanvasBrushPenPointRect 类。这是 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.minMath.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 装饰器响应式管理 currentShapecurrentColor 状态。
  • 点击顶部形状按钮时更新 currentShape,通过 $currentShape 语法将状态同步到子组件 DrawingCanvas
  • 底部颜色栏设计为 Circle 组件加文字标签,点击时更新 currentColor
  • Circle 是内置组件,用于显示预设颜色的色块,方便用户选择。
  • DrawingCanvas 接收这些参数后,在触摸抬起时根据当前形状和颜色绘制对应图形。

3. 运行验证

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

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

4. 小结与预告

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

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

相关推荐
绝世番茄2 小时前
Column 与 if/else 条件渲染:动态子组件的布局行为
华为·harmonyos·鸿蒙
花开彼岸天~3 小时前
鸿蒙实战:AppStorage 全局状态管理
华为·harmonyos·鸿蒙系统
爱吃大芒果3 小时前
鸿蒙 7 新特性科普:什么是系统 Agent 智能体?
华为·harmonyos·智能体
listening7774 小时前
HarmonyOS 6.1 元服务实战:把电商卡片装进系统“负一屏”
华为·harmonyos
蓝速科技4 小时前
蓝速科技鸿蒙信创终端全场景落地实效展示
人工智能·科技·华为·harmonyos
子非鱼94274 小时前
10-Flutter调用鸿蒙原生能力前置课:MethodChannel和PlatformView准备
学习·flutter·华为·harmonyos
xyccstudio4 小时前
用 AI 审核 Skill 消灭“编译器不报错“的隐藏 Bug
harmonyos
星释4 小时前
鸿蒙智能体开发实战:38.鸿蒙壁纸大师 - 根据设备型号优化壁纸尺寸
华为·harmonyos
xd1855785554 小时前
鸿蒙PC时代的AI快递查询应用:基于鸿蒙Flutter框架的跨端创新实践
人工智能·flutter·华为·harmonyos·鸿蒙