#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析3:自由绘画 原创

蒙娜丽宁
发布于 2024-7-29 16:21
浏览
1收藏

这一篇文章会实现一个比较有意思的场景,就是在画布上任意绘画。首先会用arkui实现如图的绘画设置窗口,如下图所示:
#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析3:自由绘画-鸿蒙开发者社区

这个窗口使用DrawingBoard组件完成,代码如下:

import { PANEL_BACKGROUND_COLOR, PANEL_BUTTON_BACKGROUND_COLOR, COMMON_COLORS, BASIC_GRAPHICS } from '../common/const'
import { WebColorPickerDialog } from './WebColorPickerDialog';
import { documentExists, createDocumentDir, getInverseColor, rgbaToHex } from '../common/common'

@Entry
@Component
export struct DrawingBoard {
  // 回调函数,用于选择绘图数据后的操作
  drawingDataSelected?: (graphicName: string,
                         lineWidth: string,
                         lineColor: string) => void
  
  // 存储选中的图形名称和线宽
  graphicName: string='';
  lineWidth: string='';

  // 存储线条颜色
  lineColor: string = '#000000';
  
  // 线宽输入控制器
  lineWidthController: TextInputController = new TextInputController()
  
  // 颜色选择器对话框控制器
  lineColorController: CustomDialogController = new CustomDialogController({
    builder: WebColorPickerDialog({
      onClickEvent: this.onLineColorClickEvent.bind(this)
    }),
  })

  // 使用@State装饰器定义响应式状态变量
  @State strokeWidth: number = 2;
  @State currentLineColor: string = '#000000';
  @State currentLineColorName: string = '黑色';
  @State currentGraphicName: string = '直线';

  // 颜色选择回调函数
  onLineColorClickEvent(color: string) {
    if (color.trim() != '') {
      this.currentLineColor = rgbaToHex(color);
    }
    this.lineColorController.close();
  }

  // UI构建函数
  build() {
    Column() {
      // 显示当前选中的图形
      Row() {
        Text('选择绘制的图形:')
          .fontSize(16)
          .fontColor('#ffffff')
        Text(this.currentGraphicName)
          .width(100)
          .height(20)
          .fontSize(14)
          .textAlign(TextAlign.Start)
          .fontColor('#ffffff')
          .backgroundColor(PANEL_BACKGROUND_COLOR)
      }.margin({ bottom: 10, top: 5 })

      // 图形选择网格
      GridRow({ columns: 4 }) {
        ForEach(BASIC_GRAPHICS, (item: [string, Resource,string], index?: number | undefined) => {
          GridCol() {
            Row() {
              Button({ type: ButtonType.Normal }) {
                Column() {
                  Image(item[1]).width(50).height(50)
                }
              }
              .width(50)
              .height(50)
              .backgroundColor(PANEL_BACKGROUND_COLOR)
              .padding({ top: 3, bottom: 3 })
              .margin({ bottom: 4 })
              .onClick(() => {
                this.graphicName = item[2];
                this.currentGraphicName = item[0];
              })
            }.width('100%').height('50')
          }.backgroundColor(item[1])
        })
      }
      .width('100%')
      .height('100%')
      .margin({ bottom: 10 })
      .alignItems(ItemAlign.Center)
      .alignSelf(ItemAlign.Center)

      // 线条颜色选择
      Row() {
        Button({ type: ButtonType.Normal }) {
          Text('线条颜色')
            .fontSize(16)
            .fontColor('#FFFFFF')
            .width('100%')
            .height('30')
            .textAlign(TextAlign.Center)
        }
        .borderRadius(5)
        .width(100)
        .height(30)
        .onClick(() => {
          this.lineColorController.open();
        });
        Text(this.currentLineColor) {
        }
        .fontSize(16)
        .width('100')
        .height('30')
        .margin({ left: 5 })
        .foregroundColor(getInverseColor(this.currentLineColor))
        .backgroundColor(this.currentLineColor);
      }.margin({ top: 10 })

      // 线宽显示
      Row() {
        Text('线宽')
          .fontSize(16)
          .fontColor('#FFFFFF')
          .width('80')
          .height('30')
          .textAlign(TextAlign.Center)
        Text(`${this.strokeWidth}`)
          .fontSize(16)
          .fontColor('#FFFFFF')
          .width('50')
          .height('30')
          .textAlign(TextAlign.Center)
      }.margin({ top: 10 })

      // 线宽调节滑块
      Slider({
        value: this.strokeWidth,
        min: 1,
        max: 20,
        step: 1,
        style: SliderStyle.OutSet
      })
        .margin({ left: 10, right: 10 })
        .onChange((value: number, mode: SliderChangeMode) => {
          this.strokeWidth = Math.round(value);
        })

      // 确定按钮
      Button({ type: ButtonType.Normal }) {
        Column() {
          Text("确定").fontSize(14).textAlign(TextAlign.Center)
        }
      }
      .fontColor('#ffffff')
      .width(80)
      .height(24)
      .borderRadius(8)
      .align(Alignment.Center)
      .onClick(() => {
        if (this.drawingDataSelected) {
          this.drawingDataSelected(this.graphicName, `${this.strokeWidth}` ,this.currentLineColor);
        }
      })
    }
    .backgroundColor(PANEL_BACKGROUND_COLOR)
    .width('100%')
    .height('100%')
    .padding(5)
    .border({ width: 0, color: '#000000', radius: 10 })
  }
}

这段代码的实现原理如下:
1.整体结构:
这段代码定义了一个名为DrawingBoard的自定义组件,用于实现画板设置界面。它使用了ArkTS的装饰器语法,如@Entry和@Component。
2.状态管理:
组件使用@State装饰器定义了几个响应式状态变量,如strokeWidth、currentLineColor等。这些变量的变化会自动触发UI的更新。
3.UI结构:
界面使用嵌套的Column和Row组件构建。主要包括图形选择、线条颜色选择、线宽调节等部分。
4.图形选择:
使用GridRow和ForEach循环创建一个图形选择网格。每个图形都是一个按钮,点击时更新graphicName和currentGraphicName。
5.颜色选择:
通过CustomDialogController实现颜色选择器对话框。点击"线条颜色"按钮时打开对话框,选择颜色后更新currentLineColor。
6.线宽调节:
使用Slider组件实现线宽调节。滑动时实时更新strokeWidth值。
7.数据传递:
当点击"确定"按钮时,会调用drawingDataSelected回调函数,将选中的图形、线宽和颜色信息传递给父组件。
8.样式设置:
大量使用了链式调用来设置组件的样式,如字体大小、颜色、边距等。
9.响应式设计:
通过@State装饰器和状态变量的绑定,实现了界面的响应式更新。当用户进行操作时,相关的状态变量会更新,从而触发UI的重新渲染。
总的来说,这段代码展示了如何使用ArkTS和ArkUI框架创建一个交互式的画板设置界面。它利用了声明式UI、响应式编程和组件化的概念,使得代码结构清晰,易于理解和维护。
在这段代码中,drawingDataSelected是事件函数,当选择某个绘图模式,并点击确定按钮,就会调用这个函数。
在主程序中,会使用下面的drawingDataSelected函数响应点击”确定“按钮,代码如下。其中这段代码做了一些前期的设置公众,最后调用javascript函数configDrawing完成最终的设置。因为最终的绘制要依赖fabric完成。

// 定义一个方法,用于处理绘图数据选择后的操作
// 参数包括图形名称、线宽和线条颜色,都是可选参数
drawingDataSelected(
  graphicName?: string,
  lineWidth?: string,
  lineColor?: string
) {
  // 更新当前选中的图形名称
  this.currentGraphicName = graphicName;
  
  // 更新当前选中的线宽
  this.currentLineWidth = lineWidth;
  
  // 更新当前选中的线条颜色
  this.currentLineColor = lineColor;
  
  // 转换颜色格式,可能是为了确保颜色格式符合JavaScript的要求
  lineColor = convertColorFormat(lineColor);
  
  // 调用controller1的runJavaScript方法,执行JavaScript代码
  // 这里可能是在Web组件中配置绘图设置
  this.controller1.runJavaScript(`configDrawing('${graphicName}','${lineColor}','${lineWidth}');`)
  
  // 切换绘图板面板的显示状态,可能是关闭设置面板
  this.toggleDrawingBoardPanel();
}

configDrawing函数的代码如下:

/**
 * 配置绘图设置
 * @param {string} mode - 绘图模式(如"线条"、"矩形"等)
 * @param {string} color - 画笔颜色
 * @param {number|string} lineWidth - 线条宽度
 */
function configDrawing(mode, color, lineWidth) {
    // 启用指定的绘图模式
    enableDrawing(mode);
    
    // 设置全局画笔颜色变量
    penColor = color;
    
    // 设置当前对象的线条宽度
    // 注意:这里的 'this' 可能指向某个特定的对象,具体取决于函数的调用方式
    this.lineWidth = lineWidth;
    
    // 设置 canvas 自由绘制笔刷的颜色
    // 这里假设 'canvas' 是一个全局变量或者是当前上下文中可访问的对象
    canvas.freeDrawingBrush.color = penColor;
    
    // 设置 canvas 自由绘制笔刷的宽度
    canvas.freeDrawingBrush.width = lineWidth;
}

这个函数名为 configDrawing,用于配置绘图的各种参数。让我详细解释一下它的功能:
1.函数接收三个参数:

  • mode:可能是一个字符串,表示绘图模式(例如,“线条”、"矩形"等)。
  • color:表示画笔颜色的字符串。
  • lineWidth:表示线条宽度的数字或字符串。
    2.首先调用 enableDrawing(mode) 函数。这个函数可能用于切换或启用特定的绘图模式。例如,它可能会改变鼠标事件的处理方式,以适应不同的绘图操作。
    3.将传入的 color 赋值给 penColor 变量。这可能是一个全局变量,用于存储当前的画笔颜色。
    4.将 lineWidth 赋值给 this.lineWidth。这里的 this 可能指向某个特定的对象,可能是整个绘图应用的一个实例或者某个特定的绘图工具对象。
    5.设置 canvas.freeDrawingBrush.color 为 penColor。这里假设 canvas 是一个全局变量或者是当前上下文中可访问的对象,可能是一个 HTML5 Canvas 元素或者是某个绘图库(如 Fabric.js)的实例。freeDrawingBrush 通常用于自由绘制模式。
    6.设置 canvas.freeDrawingBrush.width 为传入的 lineWidth。这决定了自由绘制时线条的粗细。
    总的来说,这个函数的作用是集中配置绘图的各项参数,包括绘图模式、颜色和线宽。它可能是在用户通过UI更改绘图设置后被调用,以更新实际的绘图环境。这种设计使得绘图参数的管理变得集中和统一,便于维护和扩展。

最后,需要介绍enableDrawing函数,这是绘画的核心,通过这个函数,设置了fabric的各种事件,以及绘制当前设置的各种图形,该函数的代码如下:

function enableDrawing(mode) {
    // 根据模式设置画布的绘图状态
    if(mode != "mouse") {
        canvas.enabledDrawing = true;
        if(mode === 'drawing') {
            canvas.isDrawingMode = true;  // 自由绘制模式
        } else {
            canvas.isDrawingMode = false;  // 其他绘图模式
        }
    } else {
        canvas.enabledDrawing = false;
        canvas.isDrawingMode = false;  // 鼠标模式,禁用绘图
    }

    // 移除所有现有的鼠标事件监听器
    canvas.off('mouse:down');
    canvas.off('mouse:up');
    canvas.off('mouse:move');

    // 声明变量用于存储绘图起始点和当前图形
    let startX, startY, shape;

    // 鼠标按下事件处理
    canvas.on('mouse:down', function (o) {
        var pointer = canvas.getPointer(o.e);  // 获取鼠标位置
        startX = pointer.x;
        startY = pointer.y;

        // 根据不同的绘图模式创建相应的图形对象
        if (mode === 'line') {
            // 创建线条
            shape = new fabric.Line([startX, startY, startX, startY], {
                stroke: penColor,
                strokeWidth: lineWidth,
                selectable: true,
                hasControls: true,
                hasBorders: true,
                perPixelTargetFind: true,
            });
        } else if (mode === 'circle') {
            // 创建圆形
            shape = new fabric.Circle({
                left: startX,
                top: startY,
                radius: 1,
                stroke: penColor,
                strokeWidth: lineWidth,
                selectable: true,
                hasControls: true,
                hasBorders: true,
                perPixelTargetFind: true,
                fill: ''
            });
        } else if (mode === 'rect') {
            // 创建矩形
            shape = new fabric.Rect({
                left: startX,
                top: startY,
                originX: 'left',
                originY: 'top',
                width: 1,
                height: 1,
                stroke: penColor,
                strokeWidth: lineWidth,
                fill: '',
                selectable: true,
                hasControls: true,
                hasBorders: true,
                perPixelTargetFind: true,
            });
        } else if (mode === 'triangle') {
            // 创建三角形
            shape = new fabric.Triangle({
                left: startX,
                top: startY,
                originX: 'left',
                originY: 'top',
                width: 1,
                height: 1,
                stroke: penColor,
                strokeWidth: lineWidth,
                fill: '',
                selectable: true,
                hasControls: true,
                hasBorders: true,
                perPixelTargetFind: true
            });
        } else if (mode === 'ellipse') {
            // 创建椭圆
            shape = new fabric.Ellipse({
                left: startX,
                top: startY,
                originX: 'left',
                originY: 'top',
                rx: 0.5,
                ry: 0.5,
                stroke: penColor,
                strokeWidth: lineWidth,
                fill: '',
                selectable: true,
                hasControls: true,
                hasBorders: true,
                perPixelTargetFind: true
            });
        } else if (mode === 'polyline') {
            // 多边形绘制逻辑
            if (!isDrawingPolygon) {
                isDrawingPolygon = true;
                polygonPoints = [];
                clearTempCircles();
            }
            tempCircleColor = 'red';
            if (polygonPoints.length > 0) {
                tempCircleColor = 'blue';
            }
            // 创建临时圆点标记点击位置
            tempCircle = new fabric.Circle({
                radius: 5,
                fill: tempCircleColor,
                left: pointer.x,
                top: pointer.y,
                originX: 'center',
                originY: 'center',
                selectable: false,
                hasBorders: false,
                hasControls: false,
                tempPoint: true
            });
            canvas.add(tempCircle);

            // 添加点到点列表
            polygonPoints.push({ x: pointer.x, y: pointer.y });

            // 检查是否完成多边形绘制
            if (polygonPoints.length > 4 && Math.abs(pointer.x - polygonPoints[0].x) < 10 && Math.abs(pointer.y - polygonPoints[0].y) < 10) {
                // 创建多边形
                const polygon = new fabric.Polygon(polygonPoints.slice(0, -1), {
                    stroke: penColor,
                    strokeWidth: lineWidth,
                    fill: 'transparent',
                    selectable: true,
                    hasBorders: true,
                    hasControls: true
                });
                canvas.add(polygon);

                // 清理和重置
                clearTempCircles();
                isDrawingPolygon = false;
                polygonPoints = [];
                mode = "mouse";
                canvas.enabledDrawing = false;
            }
        } else if (mode === 'bezier') {
            // 贝塞尔曲线绘制逻辑
            if (!isDrawingBezier) {
                isDrawingBezier = true;
                bezierPoints = [];
                clearTempCircles();
            }
            tempCircleColor = 'red';
            if (polygonPoints.length > 0) {
                tempCircleColor = 'blue';
            }
            let tempCircle = new fabric.Circle({
                radius: 5,
                fill: tempCircleColor,
                left: pointer.x,
                top: pointer.y,
                originX: 'center',
                originY: 'center',
                selectable: true,
                hasBorders: false,
                hasControls: false,
                tempPoint: true
            })
            canvas.add(tempCircle);
            bezierPoints.push(tempCircle);
            controlCircles.push(tempCircle);
            points.push(pointer);

            // 检查是否完成贝塞尔曲线绘制
            if (bezierPoints.length === 3) {
                const bezierCurve = new fabric.Path(`M ${bezierPoints[0].left} ${bezierPoints[0].top} Q ${bezierPoints[1].left} ${bezierPoints[1].top}, ${bezierPoints[2].left} ${bezierPoints[2].top}`, {
                    stroke: penColor,
                    strokeWidth: lineWidth,
                    fill: null,
                    selectable: true
                });
                canvas.add(bezierCurve);

                // 清理和重置
                clearTempCircles();
                controlCircles = [];
                bezierPoints = [];
                isDrawingBezier = false;
                mode = "mouse";
                canvas.enabledDrawing = false;
            }
        }

        // 将创建的图形添加到画布
        if (shape) {
            canvas.add(shape);
        }
    });

    // 鼠标移动事件处理
    canvas.on('mouse:move', function (o) {
        if (!shape) return;

        var pointer = canvas.getPointer(o.e);

        // 根据不同的绘图模式更新图形尺寸和位置
        if (['line', 'rect', 'triangle'].indexOf(mode) > -1) {
            var width = pointer.x - startX;
            var height = pointer.y - startY;
            var params = {
                width: Math.abs(width),
                height: Math.abs(height)
            };
            if (width < 0) {
                params.left = pointer.x;
            }
            if (height < 0) {
                params.top = pointer.y;
            }
            shape.set(params);
        } else if (mode === 'ellipse') {
            var rx = Math.abs(pointer.x - startX) / 2;
            var ry = Math.abs(pointer.y - startY) / 2;
            shape.set({ rx: rx, ry: ry });

            if (pointer.x < startX) {
                shape.set({ originX: 'right' });
            } else {
                shape.set({ originX: 'left' });
            }
            if (pointer.y < startY) {
                shape.set({ originY: 'bottom' });
            } else {
                shape.set({ originY: 'top' });
            }
        }
        else if (mode === 'circle') {
            var radius = Math.sqrt((pointer.x - startX) ** 2 + (pointer.y - startY) ** 2);
            shape.set({ radius: radius });
        } else if (mode === 'polyline') {
            shape.points[1] = {
                x: pointer.x,
                y: pointer.y
            };
        }

        // 重新渲染画布
        canvas.renderAll();
    });

    // 鼠标松开事件处理
    canvas.on('mouse:up', function () {
        shape = null;
        canvas.selection = true;
        canvas.enabledDrawing = false;
        canvas.isDrawingMode = false;
        if(mode != 'polyline' && mode != 'bezier') {
            mode = "mouse";
        }
    });
}

下面对这段代码进行深入介绍:
1.线条绘制(Line):

shape = new fabric.Line([startX, startY, startX, startY], {
    stroke: penColor,
    strokeWidth: lineWidth,
    selectable: true,
    hasControls: true,
    hasBorders: true,
    perPixelTargetFind: true,
});

fabric.Line 创建一条线。它接受两个点的坐标(起点和终点)。初始时,起点和终点相同,后续通过鼠标移动更新终点。

  • stroke: 线条颜色
  • strokeWidth: 线条宽度
  • selectable: 允许选择
  • hasControls: 显示控制点
  • hasBorders: 显示边框
  • perPixelTargetFind: 启用像素级的点击检测
    1.圆形绘制(Circle):
shape = new fabric.Circle({
    left: startX,
    top: startY,
    radius: 1,
    stroke: penColor,
    strokeWidth: lineWidth,
    selectable: true,
    hasControls: true,
    hasBorders: true,
    perPixelTargetFind: true,
    fill: ''
});

fabric.Circle 创建一个圆。初始半径设为1,后续根据鼠标移动调整。

  • left 和 top: 圆心位置
  • radius: 半径
  • fill: 填充颜色(这里设为空字符串,表示无填充)
    1.矩形绘制(Rectangle):
shape = new fabric.Rect({
    left: startX,
    top: startY,
    originX: 'left',
    originY: 'top',
    width: 1,
    height: 1,
    stroke: penColor,
    strokeWidth: lineWidth,
    fill: '',
    selectable: true,
    hasControls: true,
    hasBorders: true,
    perPixelTargetFind: true,
});

fabric.Rect 创建一个矩形。初始宽高为1,后续根据鼠标移动调整。

  • originX 和 originY: 定义矩形的原点(这里设为左上角)
  • width 和 height: 矩形的宽和高
    1.三角形绘制(Triangle):
shape = new fabric.Triangle({
    left: startX,
    top: startY,
    originX: 'left',
    originY: 'top',
    width: 1,
    height: 1,
    stroke: penColor,
    strokeWidth: lineWidth,
    fill: '',
    selectable: true,
    hasControls: true,
    hasBorders: true,
    perPixelTargetFind: true
});

fabric.Triangle 创建一个三角形。参数与矩形类似,Fabric.js 会自动计算三角形的形状。
1.椭圆绘制(Ellipse):
shape = new fabric.Ellipse({
left: startX,
top: startY,
originX: ‘left’,
originY: ‘top’,
rx: 0.5,
ry: 0.5,
stroke: penColor,
strokeWidth: lineWidth,
fill: ‘’,
selectable: true,
hasControls: true,
hasBorders: true,
perPixelTargetFind: true
});

fabric.Ellipse 创建一个椭圆。

  • rx 和 ry: 分别表示椭圆在 x 和 y 方向上的半径
    1.多边形绘制(Polyline):
    多边形绘制较为复杂,它通过多次点击来创建点,最后形成多边形。
const polygon = new fabric.Polygon(polygonPoints.slice(0, -1), {
    stroke: penColor,
    strokeWidth: lineWidth,
    fill: 'transparent',
    selectable: true,
    hasBorders: true,
    hasControls: true
});

fabric.Polygon 创建多边形,接受一个点数组作为参数。
1.贝塞尔曲线绘制(Bezier):
贝塞尔曲线通过三个点来定义。

const bezierCurve = new fabric.Path(`M ${bezierPoints[0].left} ${bezierPoints[0].top} Q ${bezierPoints[1].left} ${bezierPoints[1].top}, ${bezierPoints[2].left} ${bezierPoints[2].top}`, {
    stroke: penColor,
    strokeWidth: lineWidth,
    fill: null,
    selectable: true
});

这里使用 fabric.Path 来创建贝塞尔曲线。路径字符串使用 SVG 路径语法:

  • M: 移动到起点
  • Q: 表示二次贝塞尔曲线,后面跟着控制点和终点的坐标
    鼠标事件处理:
    1.mouse:down
    事件:
  • 记录起始点
  • 根据当前模式创建相应的图形对象
  • 对于多边形和贝塞尔曲线,处理点的添加逻辑
    2.mouse:move
    事件:
  • 更新图形的尺寸和位置
  • 对不同图形类型有特定的更新逻辑,如更新矩形的宽高,圆的半径等
    3.mouse:up
    事件:
  • 重置绘图状态
  • 对于大多数图形,将模式切换回 “mouse”
    这个实现展示了 Fabric.js 强大的图形处理能力。它不仅支持基本的图形绘制,还能处理复杂的交互逻辑,如多边形和贝塞尔曲线的创建。通过事件驱动和实时更新,提供了流畅的用户绘图体验。
    绘图效果如下图所示:
    #HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析3:自由绘画-鸿蒙开发者社区

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
标签
2
收藏 1
回复
举报
回复
    相关推荐