#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析2:新建绘画文档 原创

蒙娜丽宁
发布于 2024-7-28 18:31
浏览
0收藏

在上一篇文章我们介绍了创意工坊的主要功能,以及整体UI的实现。在这篇文章中,会详细介绍如何创建新的绘画文档,并与fabric进行交互。

点击“新建”按钮,会弹出如下图的窗口。

#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析2:新建绘画文档-鸿蒙开发者社区

输入文档名,选择尺寸,就可以创建一个带网格的文档,如下图所示:

#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析2:新建绘画文档-鸿蒙开发者社区


显示创建文档的窗口,需要使用arkts创建一个组件,代码如下:

// 引入必要的常量和依赖
import { PANEL_BACKGROUND_COLOR, PANEL_BUTTON_BACKGROUND_COLOR } from '../common/const'  // 引入面板背景颜色和按钮背景颜色常量
import { DocumentSize, DocumentSizes } from '../data/Documents'  // 引入文档尺寸和文档尺寸列表
import EntryAbility from '../entryability/EntryAbility'  // 引入EntryAbility模块,用于获取根路径
import {documentExists,createDocumentDir} from '../common/common'  // 引入检查文档是否存在和创建文档目录的函数

@Entry
@Component
export struct NewDocument {
  // 定义新建文档选择事件,可选参数类型为函数,接收文档名、文档路径和文档尺寸作为参数
  newDocumentSelected?: (documentName: string,
                         documentPath: string,
                         documentSize: DocumentSize) => void

  // 文本输入控制器,用于控制文档名的输入
  documentNameController: TextInputController = new TextInputController()
  // 定义根路径,初始化为EntryAbility的根路径
  rootPath:string = EntryAbility.rootPath;
  // 定义文档名状态变量,初始值为空字符串
  @State documentName: string = '';

  // 构建UI
  build() {
    Column() {
      // 文本输入框,用于输入文档名
      TextInput({ text: this.documentName, placeholder: '输入文档名...', controller: this.documentNameController })
        .placeholderColor(Color.Grey)  // 设置占位符颜色为灰色
        .placeholderFont({ size: 14, weight: 400 })  // 设置占位符字体大小和粗细
        .caretColor(Color.Blue)  // 设置光标颜色为蓝色
        .backgroundColor('#FFFFFF')  // 设置背景颜色为白色
        .width('90%')  // 设置宽度为90%
        .height(40)  // 设置高度为40
        .margin(20)  // 设置外边距为20
        .fontSize(14)  // 设置字体大小为14
        .fontColor(Color.Black)  // 设置字体颜色为黑色
        .onChange((value: string) => {  // 添加输入变化事件监听器
          this.documentName = value  // 当输入变化时,更新文档名状态变量
        })

      // 可滚动区域,包含文档尺寸选项按钮
      Scroll() {
        Column() {
          // 遍历文档尺寸列表,生成按钮
          ForEach(DocumentSizes, (item: DocumentSize) => {
            Button({ type: ButtonType.Normal }) {
              Text(item[0])  // 按钮文本为文档尺寸
            }.backgroundColor(PANEL_BUTTON_BACKGROUND_COLOR)  // 设置按钮背景颜色
            .foregroundColor('#FFFFFF')  // 设置按钮前景色(文本颜色)为白色
            .margin({ top: 5 })  // 设置上边距为5
            .onClick(() => {  // 添加点击事件监听器
              let documentName = this.documentName.trim();  // 获取并去除文档名的首尾空格
              if (documentName === '') {  // 如果文档名为空,显示提示对话框
                AlertDialog.show({
                  message:'请输入文档名',  // 提示信息
                  alignment: DialogAlignment.Center,  // 对话框居中对齐
                  primaryButton: {
                    value: '关闭',  // 按钮文本为“关闭”
                    action: () => {}
                  }
                })
              } else {
                if(documentExists(documentName)) {  // 如果文档名已存在,显示提示对话框
                  AlertDialog.show({
                    message:'文档已经存在,请换一个名',  // 提示信息
                    alignment: DialogAlignment.Center,  // 对话框居中对齐
                    primaryButton: {
                      value: '关闭',  // 按钮文本为“关闭”
                      action: () => {}
                    }
                  })
                } else {
                  if (this.newDocumentSelected) {  // 如果newDocumentSelected事件已定义
                    let documentPath = createDocumentDir(documentName);  // 创建文档目录并获取路径
                    this.newDocumentSelected(documentName, documentPath, item);  // 调用newDocumentSelected事件,传入文档名、文档路径和文档尺寸
                    this.documentName = '';  // 重置文档名状态变量
                  }
                }
              }
            })
          }, (item: DocumentSize) => item[0])  // 指定唯一键为文档尺寸

        }
        .width('100%')  // 设置列宽度为100%
      }
      .scrollable(ScrollDirection.Vertical)  // 设置滚动方向为垂直
      .width('100%')  // 设置宽度为100%
      .height('100%')  // 设置高度为100%
      .backgroundColor(PANEL_BACKGROUND_COLOR)  // 设置背景颜色为面板背景颜色
      .scrollBarWidth(0)  // 设置滚动条宽度为0
      .border({ width: 0, color: '#000000', radius: 10 })  // 设置边框宽度为0,颜色为黑色,圆角半径为10
    }
    .backgroundColor(PANEL_BACKGROUND_COLOR)  // 设置背景颜色为面板背景颜色
    .border({ width: 0, color: '#000000', radius: 10 })  // 设置边框宽度为0,颜色为黑色,圆角半径为10
    .width('100%')  // 设置宽度为100%
    .height('100%')  // 设置高度为100%
  }
}

代码实现原理

  1. 组件定义和初始化

:NewDocument组件通过装饰器@Component和@Entry进行定义。它包含了几个状态变量和控制器,包括documentName用于存储文档名称,documentNameController用于控制文本输入,rootPath保存了根路径。

  1. UI构建

:使用了多种UI组件来构建用户界面:

  • Column:作为根布局,包含其他子组件。
  • TextInput:用于输入文档名称,包含占位符、光标颜色、背景颜色等属性配置。
  • Scroll:用于实现滚动效果,包含一个Column,该列包含多个文档尺寸选择按钮。
  • Button:根据文档尺寸生成的按钮,点击按钮时会触发相关逻辑。

  1. 事件处理

  • 文本输入框的onChange事件:当用户输入文档名称时,更新documentName状态。
  • 按钮的onClick事件:点击文档尺寸按钮时,首先检查文档名称是否为空或已存在。如果为空,则显示提示对话框;如果已存在,则显示提示对话框。如果通过以上检查且newDocumentSelected事件已定义,则调用此事件,并传入文档名、文档路径和文档尺寸。

  1. 文档目录操作

  • 使用documentExists函数检查文档是否已存在。
  • 使用createDocumentDir函数创建文档目录,并获取其路径。

  1. 样式设置

:通过链式调用设置了组件的多种样式属性,包括背景颜色、字体颜色、边框等。

通过以上步骤,实现了一个交互式的新建文档窗口,用户可以输入文档名称并选择文档尺寸,点击按钮后会进行相关检查,并通过回调函数传递文档数据进行进一步处理。

     当创建文档后,会回调newDocumentSelected事件函数,在这个函数中,会与arkweb交互,也就是调用js代码利用fabric创建文档,代码如下:

newDocumentSelected(documentName: string, documentPath: string, documentSize: DocumentSize) {
    // 隐藏新建面板
    this.toggleNewPanel();
    // 更新当前文档名
    this.currentDocumentName = documentName;
    // 更新当前文档路径
    this.currentDocumentPath = documentPath;
    // 隐藏新建面板的可见性
    this.newPanelVisibility = Visibility.None;
    // 隐藏无文档的提示
    this.noDocumentVisibility = Visibility.None;
    
    // 设置当前画布的宽度
    this.currentCanvasWidth = documentSize[1];
    // 设置当前画布的高度
    this.currentCanvasHeight = documentSize[2];
    
    // 通过JavaScript代码设置画布的尺寸
    this.controller1.runJavaScript(`setCanvasSize("${documentSize[1]}","${documentSize[2]}")`)
      .then(data => {
        // 如果定时器ID小于100,启动定时器
        if (this.intervalId < 100) {
          setInterval(() => {
            // 定期获取画布的JSON数据
            this.controller1.runJavaScript(`getCanvasJSON()`)
              .then(data => {
                // 保存画布数据
                this.saveCanvas(trimQuotes(data))
              });
          }, 500);
          // 设置定时器ID为100,防止重复启动定时器
          this.intervalId = 100;
        }
      });
}

实现原理解释

  1. 面板状态更新

  • this.toggleNewPanel():调用此方法来隐藏新建文档面板。
  • this.currentDocumentName = documentName 和 this.currentDocumentPath = documentPath:更新当前文档的名称和路径。
  • this.newPanelVisibility = Visibility.None 和 this.noDocumentVisibility = Visibility.None:将新建面板和无文档提示的可见性设置为不可见。

  1. 画布尺寸设置

  • this.currentCanvasWidth = documentSize[1] 和 this.currentCanvasHeight = documentSize[2]:设置当前画布的宽度和高度。
  • this.controller1.runJavaScript(setCanvasSize("${documentSize[1]}","${documentSize[2]}")):通过执行JavaScript代码来设置画布的尺寸,调用了setCanvasSize函数,并传入宽度和高度。

  1. 定时器启动与画布数据保存

  • if (this.intervalId < 100):检查定时器ID是否小于100,以防止重复启动定时器。
  • setInterval(() => {...}, 500):启动一个定时器,每隔500毫秒执行一次内部的匿名函数。
  • this.controller1.runJavaScript(getCanvasJSON()):定时获取画布的JSON数据,调用了getCanvasJSON函数。
  • .then(data => {...}):当获取到画布的JSON数据后,调用this.saveCanvas(trimQuotes(data))方法保存画布数据,其中trimQuotes函数用于去除数据中的引号。

  1. 防止定时器重复启动

  • this.intervalId = 100:将定时器ID设置为100,确保不会重复启动定时器。

详细流程

  1. 用户在新建文档窗口输入文档名称并选择文档尺寸。
  2. 调用newDocumentSelected方法:

  • 隐藏新建面板和无文档提示。
  • 更新当前文档的名称和路径。
  • 设置画布的宽度和高度。
  • 通过JavaScript代码设置画布尺寸。
  • 如果定时器尚未启动,启动定时器,每隔500毫秒获取画布数据并保存。

这样,用户可以创建一个新文档,并实时获取和保存画布的数据,实现了文档的动态管理和数据持久化。

     最后,我们来看一下setCanvasSize函数,这是用js实现的,arkts将文档数据传给setCanvasSize函数,然后创建了一个fabric画布,该函数的代码如下:

function setCanvasSize(width, height) {
    // 将传入的宽度和高度转换为像素
    var newWidth = convertDimensionToPixels(width);
    var newHeight = convertDimensionToPixels(height);

    // 初始化画布,设置新的宽度和高度
    initCanvas(newWidth, newHeight);

    // 更新当前画布宽度和高度的全局变量
    currentCanvasWidth = newWidth;
    currentCanvasHeight = newHeight;

    // 设置画布的新宽度和高度
    canvas.setWidth(newWidth);
    canvas.setHeight(newHeight);

    // 如果画布没有clipPath(裁剪路径),则创建一个新的clipPath
    if (!canvas.clipPath) {
        currentClipRect = new fabric.Rect({
            originX: 'left',           // 裁剪路径的原点X坐标设为左侧
            originY: 'top',            // 裁剪路径的原点Y坐标设为顶部
            left: 0,                   // 裁剪路径的左边距
            top: 0,                    // 裁剪路径的上边距
            width: newWidth,           // 裁剪路径的宽度
            height: newHeight * 3,     // 裁剪路径的高度
            selectable: false,         // 设置裁剪路径不可选中
            evented: false,            // 设置裁剪路径不响应事件
            absolutePositioned: true   // 设置裁剪路径为绝对定位
        });
        // 将新创建的裁剪路径设置为画布的clipPath
        canvas.clipPath = currentClipRect;
    } else {
        // 如果已有clipPath,则更新其尺寸
        canvas.clipPath.set({ width: newWidth, height: newHeight });
    }

    // 添加网格线,参数为网格尺寸、小网格单元数量和线型
    addGridLines(gridSize, smallCellNumber, 'dashed');

    // 请求重新渲染画布
    canvas.requestRenderAll();
}

实现原理解释

  1. 尺寸转换

  • convertDimensionToPixels(width) 和 convertDimensionToPixels(height):将传入的宽度和高度转换为像素值。

  1. 初始化画布

  • initCanvas(newWidth, newHeight):使用新宽度和高度初始化画布。

  1. 更新全局变量

  • currentCanvasWidth 和 currentCanvasHeight:更新全局变量,存储当前画布的宽度和高度。

  1. 设置画布尺寸

  • canvas.setWidth(newWidth) 和 canvas.setHeight(newHeight):设置画布的实际宽度和高度。

  1. 裁剪路径(clipPath)处理

  • 如果画布没有裁剪路径(canvas.clipPath为空),则创建一个新的fabric.Rect对象作为裁剪路径,并将其设置为画布的clipPath。
  • 如果已有裁剪路径,则更新其宽度和高度。

  1. 添加网格线

  • addGridLines(gridSize, smallCellNumber, 'dashed'):调用此函数在画布上添加网格线,参数包括网格尺寸、小网格单元数量和线型(虚线)。

  1. 重新渲染画布

  • canvas.requestRenderAll():请求重新渲染画布,以应用新的尺寸和裁剪路径。

详细流程

  1. 转换尺寸:首先将传入的宽度和高度转换为像素值,确保画布的尺寸与实际需求匹配。
  2. 初始化和更新画布:初始化画布并设置新的宽度和高度,同时更新全局变量以记录当前画布尺寸。
  3. 处理裁剪路径:如果没有裁剪路径,则创建一个新的裁剪路径,并将其设置为画布的clipPath;如果已有裁剪路径,则更新其尺寸。
  4. 添加网格线:在画布上添加网格线,便于用户进行图形绘制和对齐操作。
  5. 重新渲染:请求画布重新渲染,以确保所有更改即时生效。

通过以上步骤,setCanvasSize函数能够动态调整画布的尺寸,并确保画布的其他相关属性和视觉元素(如裁剪路径和网格线)同步更新,从而为用户提供一个可调整的绘图环境。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2024-7-28 18:33:58修改
1
收藏
回复
举报
回复
    相关推荐