HarmonyOS 应用开发进阶案例(九):基于Canvas实现画布

45 阅读7分钟

实现基于鸿蒙Canvas实现画布的案例,具备绘画、撤销重做、橡皮擦、清空、画笔属性设置、缩放等功能,涉及Canvas、手势事件、折叠屏适配等知识点。

一、案例效果截图

二、案例运用到的知识点

  1. 核心知识点
  • Canvas:画布组件,用于自定义绘制图形。
  • 手势事件:onTouch/gesture。
  • 折叠屏适配:display。
  1. 其他知识点
  • ArkTS 语言基础
  • V2版状态管理:@ComponentV2/@Local/@Param/@Event/!!语法/@Provider/@Consumer/@Monitor
  • 渲染控制:if/ForEach
  • 自定义组件和组件生命周期
  • 自定义构建函数@Builder
  • 内置组件:Stack/Slider/Image/Column/Row/Text/Button
  • 常量与资源分类的访问
  • MVVM模式

三、代码结构

├──entry/src/main/ets/
│  ├──common
│  │  └──CommonConstants.ets         // 公共常量类
│  ├──entryability
│  │  └──EntryAbility.ets            // 程序入口类
│  ├──pages                  
│  │  └──Index.ets                   // 首页
│  ├──view   
│  │  └──myPaintSheet.ets            // 半模态页面
│  └──viewmodel
│     ├──DrawInvoker.ets             // 绘制方法
│     ├──IBrush.ets                  // 笔刷接口
│     ├──IDraw.ets                   // 绘制类
│     └──Paint.ets                   // 绘制属性类
└──entry/src/main/resources          // 应用静态资源目录

四、公共文件与资源

本案例涉及到的常量类和工具类代码如下:

  1. 通用常量类
// main/ets/common/utils/CommonConstants.ets
export class CommonConstants {
  static readonly ZERO: number = 0
  static readonly ONE: number = 1
  static readonly NEGATIVE_ONE: number = -1
  static readonly THREE: number = 3
  static readonly TEN: number = 10
  static readonly TWENTY_ONE: number = 21
  static readonly CANVAS_WIDTH: number = 750
  static readonly ONE_HUNDRED: number = 100
  static readonly COLOR_STRING: string = ''
  static readonly SIGN: string = '%'
  static readonly BLACK: string = 'black'
  static readonly ONE_HUNDRED_PERCENT: string = '100%'
  static readonly COLOR_ARR: string[] = ['#E90808', '#63B959', '#0A59F7', '#E56224', '#F6C800', '#5445EF', '#A946F1',
    '#000000']
  static readonly WHITE: string = '#ffffff'
  static readonly DETENTS: [Length, Length] = [550, 600]
}

本案例涉及到的资源文件如下:

  1. string.json
// main/resources/base/element/string.json
{
  "string": [
    {
      "name": "module_desc",
      "value": "module description"
    },
    {
      "name": "EntryAbility_desc",
      "value": "description"
    },
    {
      "name": "EntryAbility_label",
      "value": "自定义Canvas画布"
    },
    {
      "name": "paint",
      "value": "画笔"
    },
    {
      "name": "brash",
      "value": "笔刷"
    },
    {
      "name": "ballpoint",
      "value": "圆珠笔"
    },
    {
      "name": "marker",
      "value": "马克笔"
    },
    {
      "name": "pencil",
      "value": "铅笔"
    },
    {
      "name": "fountain_pen",
      "value": "钢笔"
    },
    {
      "name": "laser_pointer",
      "value": "激光笔"
    },
    {
      "name": "color",
      "value": "颜色"
    },
    {
      "name": "opacity",
      "value": "不透明度"
    },
    {
      "name": "thicknesses",
      "value": "粗细"
    },
    {
      "name": "rubber",
      "value": "橡皮擦"
    },
    {
      "name": "redo",
      "value": "撤回"
    },
    {
      "name": "undo",
      "value": "重做"
    },
    {
      "name": "clear",
      "value": "清空"
    }
  ]
}

2. float.json

// main/resources/base/element/float.json
{
  "float": [
    {
      "name": "font_size",
      "value": "14fp"
    },
    {
      "name": "margin_bottom",
      "value": "20vp"
    },
    {
      "name": "back_width",
      "value": "32vp"
    },
    {
      "name": "image_width",
      "value": "24vp"
    },
    {
      "name": "border_radius",
      "value": "16vp"
    },
    {
      "name": "border_radius_m",
      "value": "12vp"
    },
    {
      "name": "margin_top",
      "value": "10vp"
    },
    {
      "name": "font_size_m",
      "value": "10fp"
    },
    {
      "name": "font_size_l",
      "value": "12fp"
    },
    {
      "name": "brash_width",
      "value": "58vp"
    },
    {
      "name": "brash_height",
      "value": "59vp"
    },
    {
      "name": "padding_left",
      "value": "18vp"
    },
    {
      "name": "title_bottom",
      "value": "30vp"
    },
    {
      "name": "paint_width",
      "value": "72vp"
    },
    {
      "name": "paint_height",
      "value": "52vp"
    },
    {
      "name": "slider_width",
      "value": "242vp"
    },
    {
      "name": "number",
      "value": "47vp"
    },
    {
      "name": "bottom",
      "value": "5vp"
    },
    {
      "name": "height",
      "value": "550vp"
    }
  ]
}

3. color.json

// main/resources/base/element/color.json
{
  "color": [
    {
      "name": "start_window_background",
      "value": "#FFFFFF"
    },
    {
      "name": "theme_color",
      "value": "#0A59F7"
    },
    {
      "name": "paint_color",
      "value": "#D8D8D8"
    },
    {
      "name": "linear_start",
      "value": "#000A59F7"
    },
    {
      "name": "linear_end",
      "value": "#F70A59F7"
    },
    {
      "name": "number_color",
      "value": "#0D000000"
    }
  ]
}

其他资源请到源码中获取。

五、画布主界面

// main/ets/pages/Index.ets
// 引入 ArkUI 显示模块
import { display } from '@kit.ArkUI'
// 引入命令模式实现类(用于管理绘图命令)
import DrawInvoker from '../viewmodel/DrawInvoker'
// 引入绘制路径接口
import DrawPath from '../viewmodel/IDraw'
// 引入画笔接口及实现
import { IBrush } from '../viewmodel/IBrush'
import NormalBrush from '../viewmodel/IBrush'
// 引入颜料属性类
import Paint from '../viewmodel/Paint'
// 引入通用常量
import { CommonConstants } from '../common/CommonConstants'
// 引入自定义底部设置面板组件
import { myPaintSheet } from '../view/myPaintSheet'

@Entry
@ComponentV2
struct DrawCanvas {
  // 定义组件的状态变量
  // 本地状态变量,用于管理组件内部状态
  @Local isDrawing: boolean = false // 是否正在绘制
  @Local unDoDraw: boolean = false // 是否可以撤销
  @Local redoDraw: boolean = false // 是否可以重做
  @Local isPaint: boolean = true // 是否为画笔模式(true 为画笔,false 为橡皮擦)
  @Local isShow: boolean = false // 是否显示设置面板
  @Local isMarker: boolean = false // 是否为标记模式(未使用)
  @Local scaleValueX: number = 1 // X 轴缩放比例
  @Local scaleValueY: number = 1 // Y 轴缩放比例
  @Local pinchValueX: number = 1 // 捏合手势的 X 轴基准值
  @Local pinchValueY: number = 1 // 捏合手势的 Y 轴基准值
  @Local strokeWidth: number = 3 // 画笔粗细
  @Local alpha: number = 1 // 透明度(0-1)
  @Local color: string = '#000000' // 画笔颜色
  @Local thicknessesValue: number = 3 // 显示用的粗细值
  @Local index: number = -1 // 手势操作索引
  @Local clean: boolean = false // 是否已清空画布
  @Local percent: string = '100' // 缩放百分比
  // 提供全局访问的颜料属性对象
  @Provider() mPaint: Paint = new Paint(0, '', 1)
  // 提供全局访问的画笔工具
  @Provider() mBrush: IBrush = new NormalBrush()

  // 画布渲染设置(开启抗锯齿)
  private setting: RenderingContextSettings = new RenderingContextSettings(true)
  // 2D 画布上下文对象
  private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.setting)
  // 命令执行器(管理绘制命令)
  private drawInvoker: DrawInvoker = new DrawInvoker()
  // 当前绘制的路径对象
  private path2Db: Path2D = new Path2D()
  // 绘制路径数据对象(包含颜料属性和路径)
  private mPath: DrawPath = new DrawPath(this.mPaint, this.path2Db)
  // 触摸点坐标缓存数组(用于手势识别)
  private arr: number[] = []

  // 监听 isDrawing 变化的回调方法
  @Monitor('isDrawing')
  createDraw() {
    if (this.isDrawing) {
      // 用白色填充画布背景
      this.context.fillStyle = Color.White
      this.context.fillRect(CommonConstants.ZERO, CommonConstants.ZERO, this.context.width, this.context.height)
      // 执行所有绘制命令
      this.drawInvoker.execute(this.context)
      this.isDrawing = false
    }
  }

  aboutToAppear(): void {
    // 初始化默认画笔属性
    this.mPaint = new Paint(CommonConstants.ZERO, CommonConstants.COLOR_STRING, CommonConstants.ONE)
    this.mPaint.setStrokeWidth(CommonConstants.THREE) // 设置默认笔触宽度为 3
    this.mPaint.setColor(CommonConstants.BLACK) // 设置默认颜色为黑色
    this.mPaint.setGlobalAlpha(CommonConstants.ONE) // 设置默认完全不透明
    // 使用普通画笔
    this.mBrush = new NormalBrush()

    // 监听折叠屏状态变化事件
    display.on('foldStatusChange', (data: display.FoldStatus) => {
      if (data === 2) {
        this.scaleValueX = 0.5
        this.pinchValueX = 0.5
        this.scaleValueY = 1
        this.pinchValueY = 1
        this.context.clearRect(0, 0, this.context.width, this.context.height)
        this.drawInvoker.execute(this.context)
      } else if (data === 1) {
        this.scaleValueX = 1
        this.scaleValueY = 1
        this.pinchValueX = 1
        this.pinchValueY = 1
        this.context.clearRect(0, 0, this.context.width, this.context.height)
        this.drawInvoker.execute(this.context)
      }
    })
  }

  /**
   * 添加绘制路径到命令执行器
   * @param path 要添加的绘制路径对象
   */
  add(path: DrawPath): void {
    this.drawInvoker.add(path)
  }

  // 更新画笔属性(颜色/粗细/透明度)
  ToggleThicknessColor(): void {
    // 创建新 Paint 对象并更新属性
    this.mPaint = new Paint(CommonConstants.ZERO, CommonConstants.COLOR_STRING, CommonConstants.ONE)
    this.mPaint.setStrokeWidth(this.strokeWidth) // 设置笔触宽度
    this.mPaint.setColor(this.color) // 设置颜色
    this.mPaint.setGlobalAlpha(this.alpha) // 设置透明度
    // 使用普通画笔
    this.mBrush = new NormalBrush()
  }

  // 执行撤销操作
  drawOperateUndo(): void {
    this.drawInvoker.undo() // 执行撤销
    this.isDrawing = true // 标记需要重绘
    // 更新按钮状态
    if (!this.drawInvoker.canUndo()) {
      this.unDoDraw = false
    }
    this.redoDraw = true
  }

  // 执行重做操作
  drawOperateRedo(): void {
    this.drawInvoker.redo() // 执行重做
    this.isDrawing = true // 标记需要重绘
    // 更新按钮状态
    if (!this.drawInvoker.canRedo()) {
      this.redoDraw = false
    }
    this.unDoDraw = true
  }

  // 清空画布操作
  clear(): void {
    this.drawInvoker.clear() // 清除所有绘制命令
    this.isDrawing = true // 标记需要重绘
    // 重置按钮状态
    this.redoDraw = false
    this.unDoDraw = false
  }

  // 构建底部设置面板的 Builder 方法
  @Builder
  myPaintSheet() {
    Column() {
      // 使用自定义设置面板组件
      myPaintSheet({
        isMarker: this.isMarker!!, // 传递标记模式状态
        alpha: this.alpha!!, // 当前透明度
        percent: this.percent!!, // 缩放百分比
        color: this.color!!, // 当前颜色
        thicknessesValue: this.thicknessesValue!!, // 显示用粗细值
        strokeWidth: this.strokeWidth!!, // 实际笔触宽度
      })
    }
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      Canvas(this.context)
        .width(CommonConstants.CANVAS_WIDTH)
        .height(CommonConstants.CANVAS_WIDTH)
        .backgroundColor($r('sys.color.white'))
        .onTouch((event: TouchEvent) => { // 触摸事件处理
          this.clean = false // 重置清空状态
          // 多指操作或正在缩放时返回
          if (this.index === 1 || event.touches.length > 1) {
            return
          }
          // 记录触摸点坐标
          this.arr.push(event.touches[0].x + event.touches[0].y)

          // 手指按下事件处理
          if (event.touches.length === 1 && event.touches[0].id === 0 && event.type === TouchType.Down) {
            // 创建新路径
            this.mPath = new DrawPath(this.mPaint, this.path2Db)
            this.mPath.paint = this.mPaint
            this.mPath.path = new Path2D()
            // 记录起始点
            this.mBrush.down(this.mPath.path, event.touches[0].x, event.touches[0].y)
          }

          // 手指移动事件处理
          if (event.touches.length === 1 && event.touches[0].id === 0 && event.type === TouchType.Move) {
            // 更新路径
            this.mBrush.move(this.mPath.path, event.touches[0].x, event.touches[0].y)
            // 清空并重绘
            this.context.clearRect(0, 0, this.context.width, this.context.height)
            this.drawInvoker.execute(this.context)
            // 绘制当前路径(超过 4 个点后显示)
            if (this.arr.length > 4) {
              this.mPath.draw(this.context)
            }
          }

          // 手指抬起事件处理
          if (event.touches.length === 1 && event.touches[0].id === 0 && event.type === TouchType.Up) {
            this.add(this.mPath) // 添加路径到命令列表
            this.arr = [] // 清空坐标缓存
            // 更新按钮状态
            this.redoDraw = false
            this.unDoDraw = true
            this.isDrawing = true
            // 清空并重绘
            this.context.clearRect(0, 0, this.context.width, this.context.height)
            this.drawInvoker.execute(this.context)
          }
        })
          // 应用缩放变换
        .scale({
          x: this.scaleValueX,
          y: this.scaleValueY,
          z: CommonConstants.ONE
        })
          // 捏合手势处理
        .gesture(
          PinchGesture()
            .onActionStart(() => {
              this.index = 1 // 标记手势开始
            })
            .onActionUpdate((event: GestureEvent) => {
              this.context.clearRect(0, 0, this.context.width, this.context.height)
              this.drawInvoker.execute(this.context)
              if (event) {
                // 更新缩放值
                this.scaleValueX = this.pinchValueX * event.scale
                this.scaleValueY = this.pinchValueY * event.scale
              }
            })
            .onActionEnd(() => {
              // 保存当前缩放值为基准值
              this.pinchValueX = this.scaleValueX
              this.pinchValueY = this.scaleValueY
              this.context.clearRect(0, 0, this.context.width, this.context.height)
              this.drawInvoker.execute(this.context)
            })
        )

      // 手势操作层(覆盖整个画布)
      Column()
        .width(CommonConstants.ONE_HUNDRED_PERCENT)
        .height(CommonConstants.ONE_HUNDRED_PERCENT)
        .backgroundColor(Color.Transparent)
        .zIndex(this.index) // 控制层级
        .gesture(
          PinchGesture()
            .onActionStart(() => {
              this.index = 1
            })
            .onActionUpdate((event: GestureEvent) => {
              this.context.clearRect(0, 0, this.context.width, this.context.height)
              this.drawInvoker.execute(this.context)
              if (event) {
                this.scaleValueX = this.pinchValueX * event.scale
                this.scaleValueY = this.pinchValueY * event.scale
              }
            })
            .onActionEnd(() => {
              this.context.clearRect(0, 0, this.context.width, this.context.height)
              this.drawInvoker.execute(this.context)
              this.pinchValueX = this.scaleValueX
              this.pinchValueY = this.scaleValueY
            })
        )

      // 底部工具栏
      Row() {
        // 画笔工具按钮
        Stack() {
          Column() {
            // 动态切换按钮图标
            Image(this.isPaint && this.index === CommonConstants.NEGATIVE_ONE ? $r('app.media.paintbrush_active') :
            $r('app.media.paintbrush'))
              .width($r('app.float.image_width'))
              .height($r('app.float.image_width'))
              .margin({ bottom: $r('app.float.bottom') })
            // 按钮文字
            Text($r('app.string.paint'))
              .fontSize($r('app.float.font_size_m'))
              .fontColor(this.isPaint && this.index === CommonConstants.NEGATIVE_ONE ? $r('app.color.theme_color') :
              $r('sys.color.mask_secondary'))
          }
          .width(CommonConstants.ONE_HUNDRED_PERCENT)
          .height(CommonConstants.ONE_HUNDRED_PERCENT)

          // 透明按钮覆盖
          Button({ type: ButtonType.Normal })
            .backgroundColor(Color.Transparent)
            .width(CommonConstants.ONE_HUNDRED_PERCENT)
            .height(CommonConstants.ONE_HUNDRED_PERCENT)
            .onClick(() => {
              this.ToggleThicknessColor() // 更新画笔属性
              this.isPaint = true // 切换为画笔模式
              this.isShow = !this.isShow // 切换设置面板显示
              this.index = -1 // 重置手势索引
              this.arr = [] // 清空坐标缓存
            })
        }
        // 绑定底部设置面板
        .bindSheet($$this.isShow, this.myPaintSheet(), {
          height: $r('app.float.height'),
          backgroundColor: Color.White,
          title: {
            title: $r('app.string.paint')
          },
          detents: CommonConstants.DETENTS
        })
        .width($r('app.float.paint_width'))
        .height($r('app.float.paint_height'))

        // 橡皮擦工具按钮(结构类似画笔按钮)
        Stack() {
          Column() {
            Image(this.isPaint || this.index === CommonConstants.ONE ? $r('app.media.rubbers') :
            $r('app.media.rubbers_active'))
              .width($r('app.float.image_width'))
              .height($r('app.float.image_width'))
              .margin({ bottom: $r('app.float.bottom') })
            Text($r('app.string.rubber'))
              .fontSize($r('app.float.font_size_m'))
              .fontColor(this.isPaint || this.index === CommonConstants.ONE ? $r('sys.color.mask_secondary') :
              $r('app.color.theme_color'))

          }
          .width(CommonConstants.ONE_HUNDRED_PERCENT)
          .height(CommonConstants.ONE_HUNDRED_PERCENT)

          Button({ type: ButtonType.Normal })
            .backgroundColor(Color.Transparent)
            .width(CommonConstants.ONE_HUNDRED_PERCENT)
            .height(CommonConstants.ONE_HUNDRED_PERCENT)
            .onClick(() => {
              this.mPaint = new Paint(CommonConstants.ZERO, CommonConstants.COLOR_STRING, CommonConstants.ONE)
              this.mPaint.setStrokeWidth(CommonConstants.TEN)
              this.mPaint.setColor(CommonConstants.WHITE)
              this.mPaint.setGlobalAlpha(CommonConstants.ONE)
              this.isPaint = false
            })
        }
        .width($r('app.float.paint_width'))
        .height($r('app.float.paint_height'))

        // 撤销按钮
        Stack() {
          Column() {
            Image(this.unDoDraw ? $r('app.media.recall_active') : $r('app.media.recall'))
              .width($r('app.float.image_width'))
              .height($r('app.float.image_width'))
              .margin({ bottom: $r('app.float.bottom') })
            Text($r('app.string.redo'))
              .fontSize($r('app.float.font_size_m'))
              .fontColor(this.unDoDraw ? $r('app.color.theme_color') : $r('sys.color.mask_secondary'))
          }
          .width(CommonConstants.ONE_HUNDRED_PERCENT)
          .height(CommonConstants.ONE_HUNDRED_PERCENT)

          Button({ type: ButtonType.Normal })
            .backgroundColor(Color.Transparent)
            .enabled(this.unDoDraw)
            .width(CommonConstants.ONE_HUNDRED_PERCENT)
            .height(CommonConstants.ONE_HUNDRED_PERCENT)
            .onClick(async () => {
              this.drawOperateUndo()
              this.context.clearRect(0, 0, this.context.width, this.context.height)
              this.drawInvoker.execute(this.context)
            })
        }
        .width($r('app.float.paint_width'))
        .height($r('app.float.paint_height'))

        // 重做按钮(结构类似撤销按钮)
        Stack() {
          Column() {
            Image(this.redoDraw ? $r('app.media.redo_active') : $r('app.media.redo'))
              .width($r('app.float.image_width'))
              .height($r('app.float.image_width'))
              .margin({ bottom: $r('app.float.bottom') })
            Text($r('app.string.undo'))
              .fontSize($r('app.float.font_size_m'))
              .fontColor(this.redoDraw ? $r('app.color.theme_color') : $r('sys.color.mask_secondary'))
          }
          .width(CommonConstants.ONE_HUNDRED_PERCENT)
          .height(CommonConstants.ONE_HUNDRED_PERCENT)

          Button({ type: ButtonType.Normal })
            .backgroundColor(Color.Transparent)
            .enabled(this.redoDraw)
            .width(CommonConstants.ONE_HUNDRED_PERCENT)
            .height(CommonConstants.ONE_HUNDRED_PERCENT)
            .onClick(async () => {
              this.drawOperateRedo()
              this.context.clearRect(0, 0, this.context.width, this.context.height)
              this.drawInvoker.execute(this.context)
            })
        }
        .width($r('app.float.paint_width'))
        .height($r('app.float.paint_height'))

        // 清空按钮
        Stack() {
          Column() {
            Image(this.clean ? $r('app.media.clear_active') : $r('app.media.clear'))
              .width($r('app.float.image_width'))
              .height($r('app.float.image_width'))
              .margin({ bottom: $r('app.float.bottom') })
            Text($r('app.string.clear'))
              .fontSize($r('app.float.font_size_m'))
              .fontColor(this.clean ? $r('app.color.theme_color') : $r('sys.color.mask_secondary'))
          }
          .width(CommonConstants.ONE_HUNDRED_PERCENT)
          .height(CommonConstants.ONE_HUNDRED_PERCENT)

          Button({ type: ButtonType.Normal })
            .backgroundColor(Color.Transparent)
            .width(CommonConstants.ONE_HUNDRED_PERCENT)
            .height(CommonConstants.ONE_HUNDRED_PERCENT)
            .onClick(async () => {
              this.clear()
              this.context.clearRect(0, 0, this.context.width, this.context.height)
              this.drawInvoker.execute(this.context)
              this.clean = true
            })
        }
        .width($r('app.float.paint_width'))
        .height($r('app.float.paint_height'))
      }
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(VerticalAlign.Center)
      .zIndex(CommonConstants.TEN)
    }
    .backgroundColor($r('sys.color.comp_background_focus'))
    .width(CommonConstants.ONE_HUNDRED_PERCENT)
    .height(CommonConstants.ONE_HUNDRED_PERCENT)
  }
}

六、管理绘图命令类

// main/ets/viewmodel/DrawInvoker.ets
import { List } from '@kit.ArkTS'
import DrawPath from './IDraw'

export default class DrawInvoker {
  // Draw list.
  private drawPathList: List<DrawPath> = new List<DrawPath>()
  // Redo list.
  private redoList: Array<DrawPath> = new Array<DrawPath>()

  add(command: DrawPath): void {
    this.drawPathList.add(command)
    this.redoList = []
  }

  clear(): void {
    if (this.drawPathList.length > 0 || this.redoList.length > 0) {
      this.drawPathList.clear()
      this.redoList = []
    }
  }

  undo(): void {
    if (this.drawPathList.length > 0) {
      let undo: DrawPath = this.drawPathList.get(this.drawPathList.length - 1)
      this.drawPathList.removeByIndex(this.drawPathList.length - 1)
      this.redoList.push(undo)
    }
  }

  redo(): void {
    if (this.redoList.length > 0) {
      let redoCommand = this.redoList[this.redoList.length - 1]
      this.redoList.pop()
      this.drawPathList.add(redoCommand)
    }
  }

  execute(context: CanvasRenderingContext2D): void {
    if (this.drawPathList !== null) {
      this.drawPathList.forEach((element: DrawPath) => {
        element.draw(context)
      })
    }
  }

  canRedo(): boolean {
    return this.redoList.length > 0
  }

  canUndo(): boolean {
    return this.drawPathList.length > 0
  }
}

七、绘制路径接口

// main/ets/viewmodel/IDraw.ets
import Paint from './Paint'

export interface IDraw {
  draw(context: CanvasRenderingContext2D): void
}

export default class DrawPath implements IDraw {
  public paint: Paint
  public path: Path2D

  constructor(paint: Paint, path: Path2D) {
    this.paint = paint
    this.path = path
  }

  draw(context: CanvasRenderingContext2D): void {
    context.lineWidth = this.paint.lineWidth
    context.strokeStyle = this.paint.StrokeStyle
    context.globalAlpha = this.paint.globalAlpha
    context.lineCap = 'round'
    context.stroke(this.path)
  }
}

八、画笔接口

// main/ets/viewmodel/IBrush.ets
export interface IBrush {
  down(path: Path2D, x: number, y: number): void;
  move(path: Path2D, x: number, y: number): void;
  up(path: Path2D, x: number, y: number): void;
}

export default class NormalBrush implements IBrush {
  down(path: Path2D, x: number, y: number): void {
    path.moveTo(x, y);
  }
  move(path: Path2D, x: number, y: number): void {
    path.lineTo(x, y);
  }
  up(path: Path2D, x: number, y: number): void {}
}

九、绘制类

// main/ets/viewmodel/Paint.ets
export default class Paint {
  lineWidth: number
  StrokeStyle: string
  globalAlpha: number

  constructor(lineWidth: number, StrokeStyle: string, globalAlpha: number) {
    this.lineWidth = lineWidth
    this.StrokeStyle = StrokeStyle
    this.globalAlpha = globalAlpha
  }

  setColor(color: string) {
    this.StrokeStyle = color
  }

  setStrokeWidth(width: number) {
    this.lineWidth = width
  }

  setGlobalAlpha(alpha: number) {
    this.globalAlpha = alpha
  }
}

十、自定义底部设置面板组件

// main/ets/view/myPaintSheet.ets
// 引入通用常量
import { CommonConstants } from '../common/CommonConstants'
// 引入画笔接口及实现
import { IBrush } from '../viewmodel/IBrush'
import NormalBrush from '../viewmodel/IBrush'
// 引入颜料属性类
import Paint from '../viewmodel/Paint'

// 组件装饰器(V2 版本)
@ComponentV2
export struct myPaintSheet {
  // region [参数和事件定义]
  // 是否为马克笔模式(参数)
  @Param isMarker: boolean = false
  // 更新 isMarker 的事件回调
  @Event $isMarker: (val: boolean) => void = (val: boolean) => {}

  // 透明度(参数)
  @Param alpha: number = 1
  // 更新透明度的事件回调
  @Event $alpha: (val: number) => void = (val: number) => {}

  // 透明度百分比(参数)
  @Param percent: string = '100'
  // 更新百分比的事件回调
  @Event $percent: (val: string) => void = (val: string) => {}

  // 画笔颜色(参数)
  @Param color: string = '#000000'
  // 更新颜色的事件回调
  @Event $color: (val: string) => void = (val: string) => {}

  // 画笔粗细值(参数)
  @Param thicknessesValue: number = 3
  // 更新粗细值的事件回调
  @Event $thicknessesValue: (val: number) => void = (val: number) => {}

  // 画笔宽度(参数)
  @Param strokeWidth: number = 3
  // 更新画笔宽度的事件回调
  @Event $strokeWidth: (val: number) => void = (val: number) => {}

  // 消费全局的颜料属性对象
  @Consumer() mPaint: Paint = new Paint(0, '', 1)
  // 消费全局的画笔工具
  @Consumer() mBrush: IBrush = new NormalBrush()
  // endregion

  // 更新画笔属性的方法
  ToggleThicknessColor() {
    // 创建新的 Paint 对象并更新属性
    this.mPaint = new Paint(CommonConstants.ZERO, CommonConstants.COLOR_STRING, CommonConstants.ONE)
    this.mPaint.setStrokeWidth(this.strokeWidth) // 设置笔触宽度
    this.mPaint.setColor(this.color) // 设置颜色
    this.mPaint.setGlobalAlpha(this.alpha) // 设置透明度
    // 使用普通画笔
    this.mBrush = new NormalBrush()
  }

  // 主构建方法
  build() {
    Column() {
      // 画笔类型选择区域
      Column() {
        // 标题
        Text($r('app.string.brash'))
          .textAlign(TextAlign.Start)
          .fontSize($r('app.float.font_size'))
          .fontColor($r('sys.color.mask_secondary'))
          .margin({ bottom: $r('app.float.margin_bottom') })

        // 画笔类型选项
        Row() {
          // 圆珠笔选项
          Column() {
            Stack() {
              // 背景
              Text()
                .width($r('app.float.back_width'))
                .height($r('app.float.back_width'))
                .backgroundColor(this.isMarker ? $r('app.color.paint_color') : $r('app.color.theme_color'))
                .borderRadius($r('app.float.border_radius'))
              // 图标
              Image(this.isMarker ? $r('app.media.Ballpoint') : $r('app.media.Ballpoint_active'))
                .width($r('app.float.image_width'))
                .height($r('app.float.image_width'))
              // 透明按钮
              Button({ type: ButtonType.Normal })
                .width($r('app.float.back_width'))
                .height($r('app.float.back_width'))
                .borderRadius($r('app.float.border_radius'))
                .backgroundColor(Color.Transparent)
                .onClick(() => {
                  this.$isMarker(false) // 设置为非马克笔模式
                  this.$alpha(1) // 设置透明度为 100%
                  this.$percent('100') // 设置百分比为 100%
                  this.ToggleThicknessColor() // 更新画笔属性
                })
            }
            // 标签
            Text($r('app.string.ballpoint'))
              .fontSize($r('app.float.font_size'))
              .fontColor(this.isMarker ? $r('sys.color.mask_secondary') : $r('app.color.theme_color'))
              .margin({ top: $r('app.float.margin_top') })
          }
          .width($r('app.float.brash_width'))
          .height($r('app.float.brash_height'))

          // 马克笔选项(结构与圆珠笔类似)
          Column() { /* ... */ }

          // 铅笔选项(仅显示,无交互)
          Column() { /* ... */ }

          // 钢笔选项(仅显示,无交互)
          Column() { /* ... */ }

          // 激光笔选项(仅显示,无交互)
          Column() { /* ... */ }
        }
        .padding({
          left: $r('app.float.margin_top'),
          right: $r('app.float.margin_top')
        })
        .width(CommonConstants.ONE_HUNDRED_PERCENT)
        .justifyContent(FlexAlign.SpaceBetween)
      }
      .padding({
        left: $r('app.float.padding_left'),
        right: $r('app.float.padding_left'),
        top: $r('app.float.margin_bottom')
      })
      .alignItems(HorizontalAlign.Start)
      .width(CommonConstants.ONE_HUNDRED_PERCENT)
      .margin({ bottom: $r('app.float.title_bottom') })

      // 颜色选择区域
      Column() {
        // 标题
        Text($r('app.string.color'))
          .textAlign(TextAlign.Start)
          .fontSize($r('app.float.font_size'))
          .fontColor($r('sys.color.mask_secondary'))
          .margin({ bottom: $r('app.float.margin_bottom') })

        // 颜色选项
        Row() {
          // 遍历颜色数组,生成颜色选项
          ForEach(CommonConstants.COLOR_ARR, (item: string) => {
            Text()
              .width($r('app.float.image_width'))
              .height($r('app.float.image_width'))
              .borderRadius($r('app.float.border_radius_m'))
              .backgroundColor(item) // 设置背景颜色
              .onClick(() => {
                this.$color(item) // 更新颜色
                setTimeout(() => {
                  this.ToggleThicknessColor() // 更新画笔属性
                }, 100)
              })
          }, (item: string) => JSON.stringify(item)) // 使用颜色值作为唯一键
        }
        .padding({
          left: $r('app.float.margin_top'),
          right: $r('app.float.margin_top')
        })
        .justifyContent(FlexAlign.SpaceBetween)
        .width(CommonConstants.ONE_HUNDRED_PERCENT)
      }
      .padding({
        left: $r('app.float.padding_left'),
        right: $r('app.float.padding_left')
      })
      .alignItems(HorizontalAlign.Start)
      .width(CommonConstants.ONE_HUNDRED_PERCENT)
      .margin({ bottom: $r('app.float.margin_bottom') })

      // 透明度调整区域
      Column() {
        // 标题
        Text($r('app.string.opacity'))
          .textAlign(TextAlign.Start)
          .fontSize($r('app.float.font_size'))
          .fontColor($r('sys.color.mask_secondary'))
          .margin({ bottom: $r('app.float.margin_top') })

        // 滑块和百分比显示
        Row() {
          Stack() {
            // 透明度滑块
            Slider({
              style: SliderStyle.InSet,
              value: this.alpha * CommonConstants.ONE_HUNDRED
            })
              .height($r('app.float.brash_width'))
              .width($r('app.float.slider_width'))
              .selectedColor(Color.Transparent)
              .minResponsiveDistance(CommonConstants.ONE)
              .trackColor(new LinearGradient([
                { color: $r('app.color.linear_start'), offset: CommonConstants.ZERO },
                { color: $r('app.color.linear_end'), offset: CommonConstants.ONE }
              ]))
              .onChange((value: number) => {
                if (this.isMarker) { // 仅在马克笔模式下生效
                  this.$alpha(value / 100) // 更新透明度
                  this.$percent(value.toFixed(0)) // 更新百分比
                  this.ToggleThicknessColor() // 更新画笔属性
                }
              })

            // 非马克笔模式下禁用滑块
            if (!this.isMarker) {
              Row()
                .backgroundColor(Color.Transparent)
                .width($r('app.float.slider_width'))
                .height($r('app.float.brash_width'))
            }
          }

          // 百分比显示
          Text(this.percent + CommonConstants.SIGN)
            .width($r('app.float.number'))
            .height($r('app.float.image_width'))
            .fontSize($r('app.float.font_size_l'))
            .borderRadius($r('app.float.border_radius_m'))
            .textAlign(TextAlign.Center)
            .backgroundColor($r('app.color.number_color'))
        }
        .padding({
          left: $r('app.float.margin_top'),
          right: $r('app.float.margin_top')
        })
        .justifyContent(FlexAlign.SpaceBetween)
        .width(CommonConstants.ONE_HUNDRED_PERCENT)
      }
      .padding({
        left: $r('app.float.padding_left'),
        right: $r('app.float.padding_left')
      })
      .alignItems(HorizontalAlign.Start)
      .width(CommonConstants.ONE_HUNDRED_PERCENT)
      .margin({ bottom: $r('app.float.margin_bottom') })

      // 画笔粗细调整区域
      Column() {
        // 标题
        Text($r('app.string.thicknesses'))
          .textAlign(TextAlign.Start)
          .fontSize($r('app.float.font_size'))
          .fontColor($r('sys.color.mask_secondary'))
          .margin({ bottom: $r('app.float.margin_bottom') })

        // 粗细调整控件
        Row() {
          // 减号按钮
          Image($r('app.media.minuses'))
            .width($r('app.float.image_width'))
            .height($r('app.float.image_width'))
            .onClick(() => {
              this.$thicknessesValue(this.thicknessesValue - 1) // 减小粗细值
              this.$strokeWidth(this.thicknessesValue) // 更新画笔宽度
              this.ToggleThicknessColor() // 更新画笔属性
            })

          // 粗细滑块
          Slider({
            value: this.thicknessesValue,
            min: CommonConstants.THREE,
            max: CommonConstants.TWENTY_ONE
          })
            .width($r('app.float.slider_width'))
            .minResponsiveDistance(CommonConstants.ONE)
            .onChange((value: number, _mode: SliderChangeMode) => {
              this.$thicknessesValue(value) // 更新粗细值
              this.$strokeWidth(value) // 更新画笔宽度
              this.ToggleThicknessColor() // 更新画笔属性
            })

          // 加号按钮
          Image($r('app.media.add'))
            .width($r('app.float.image_width'))
            .height($r('app.float.image_width'))
            .onClick(() => {
              this.$thicknessesValue(this.thicknessesValue + 1) // 增加粗细值
              this.$strokeWidth(this.thicknessesValue) // 更新画笔宽度
              this.ToggleThicknessColor() // 更新画笔属性
            })
        }
        .padding({
          left: $r('app.float.margin_bottom'),
          right: $r('app.float.margin_bottom')
        })
        .justifyContent(FlexAlign.SpaceBetween)
        .width(CommonConstants.ONE_HUNDRED_PERCENT)
      }
      .padding({
        left: $r('app.float.padding_left'),
        right: $r('app.float.padding_left')
      })
      .alignItems(HorizontalAlign.Start)
      .width(CommonConstants.ONE_HUNDRED_PERCENT)
    }
    .width(CommonConstants.ONE_HUNDRED_PERCENT)
    .height(CommonConstants.ONE_HUNDRED_PERCENT)
  }
}

✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接