#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析2:新建绘画文档 原创
在上一篇文章我们介绍了创意工坊的主要功能,以及整体UI的实现。在这篇文章中,会详细介绍如何创建新的绘画文档,并与fabric进行交互。
点击“新建”按钮,会弹出如下图的窗口。
输入文档名,选择尺寸,就可以创建一个带网格的文档,如下图所示:
显示创建文档的窗口,需要使用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%
}
}
代码实现原理
- 组件定义和初始化
:NewDocument组件通过装饰器@Component和@Entry进行定义。它包含了几个状态变量和控制器,包括documentName用于存储文档名称,documentNameController用于控制文本输入,rootPath保存了根路径。
- UI构建
:使用了多种UI组件来构建用户界面:
- Column:作为根布局,包含其他子组件。
- TextInput:用于输入文档名称,包含占位符、光标颜色、背景颜色等属性配置。
- Scroll:用于实现滚动效果,包含一个Column,该列包含多个文档尺寸选择按钮。
- Button:根据文档尺寸生成的按钮,点击按钮时会触发相关逻辑。
- 事件处理
:
- 文本输入框的onChange事件:当用户输入文档名称时,更新documentName状态。
- 按钮的onClick事件:点击文档尺寸按钮时,首先检查文档名称是否为空或已存在。如果为空,则显示提示对话框;如果已存在,则显示提示对话框。如果通过以上检查且newDocumentSelected事件已定义,则调用此事件,并传入文档名、文档路径和文档尺寸。
- 文档目录操作
:
- 使用documentExists函数检查文档是否已存在。
- 使用createDocumentDir函数创建文档目录,并获取其路径。
- 样式设置
:通过链式调用设置了组件的多种样式属性,包括背景颜色、字体颜色、边框等。
通过以上步骤,实现了一个交互式的新建文档窗口,用户可以输入文档名称并选择文档尺寸,点击按钮后会进行相关检查,并通过回调函数传递文档数据进行进一步处理。
当创建文档后,会回调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;
}
});
}
实现原理解释
- 面板状态更新
- this.toggleNewPanel():调用此方法来隐藏新建文档面板。
- this.currentDocumentName = documentName 和 this.currentDocumentPath = documentPath:更新当前文档的名称和路径。
- this.newPanelVisibility = Visibility.None 和 this.noDocumentVisibility = Visibility.None:将新建面板和无文档提示的可见性设置为不可见。
- 画布尺寸设置
- this.currentCanvasWidth = documentSize[1] 和 this.currentCanvasHeight = documentSize[2]:设置当前画布的宽度和高度。
- this.controller1.runJavaScript(setCanvasSize("${documentSize[1]}","${documentSize[2]}")):通过执行JavaScript代码来设置画布的尺寸,调用了setCanvasSize函数,并传入宽度和高度。
- 定时器启动与画布数据保存
- if (this.intervalId < 100):检查定时器ID是否小于100,以防止重复启动定时器。
- setInterval(() => {...}, 500):启动一个定时器,每隔500毫秒执行一次内部的匿名函数。
- this.controller1.runJavaScript(getCanvasJSON()):定时获取画布的JSON数据,调用了getCanvasJSON函数。
- .then(data => {...}):当获取到画布的JSON数据后,调用this.saveCanvas(trimQuotes(data))方法保存画布数据,其中trimQuotes函数用于去除数据中的引号。
- 防止定时器重复启动
- this.intervalId = 100:将定时器ID设置为100,确保不会重复启动定时器。
详细流程
- 用户在新建文档窗口输入文档名称并选择文档尺寸。
- 调用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();
}
实现原理解释
- 尺寸转换
- convertDimensionToPixels(width) 和 convertDimensionToPixels(height):将传入的宽度和高度转换为像素值。
- 初始化画布
- initCanvas(newWidth, newHeight):使用新宽度和高度初始化画布。
- 更新全局变量
- currentCanvasWidth 和 currentCanvasHeight:更新全局变量,存储当前画布的宽度和高度。
- 设置画布尺寸
- canvas.setWidth(newWidth) 和 canvas.setHeight(newHeight):设置画布的实际宽度和高度。
- 裁剪路径(clipPath)处理
- 如果画布没有裁剪路径(canvas.clipPath为空),则创建一个新的fabric.Rect对象作为裁剪路径,并将其设置为画布的clipPath。
- 如果已有裁剪路径,则更新其宽度和高度。
- 添加网格线
- addGridLines(gridSize, smallCellNumber, 'dashed'):调用此函数在画布上添加网格线,参数包括网格尺寸、小网格单元数量和线型(虚线)。
- 重新渲染画布
- canvas.requestRenderAll():请求重新渲染画布,以应用新的尺寸和裁剪路径。
详细流程
- 转换尺寸:首先将传入的宽度和高度转换为像素值,确保画布的尺寸与实际需求匹配。
- 初始化和更新画布:初始化画布并设置新的宽度和高度,同时更新全局变量以记录当前画布尺寸。
- 处理裁剪路径:如果没有裁剪路径,则创建一个新的裁剪路径,并将其设置为画布的clipPath;如果已有裁剪路径,则更新其尺寸。
- 添加网格线:在画布上添加网格线,便于用户进行图形绘制和对齐操作。
- 重新渲染:请求画布重新渲染,以确保所有更改即时生效。
通过以上步骤,setCanvasSize函数能够动态调整画布的尺寸,并确保画布的其他相关属性和视觉元素(如裁剪路径和网格线)同步更新,从而为用户提供一个可调整的绘图环境。