#HarmonyOS NEXT体验官# 基于鸿蒙Next的App(创意工坊)解析3:自由绘画 原创
这一篇文章会实现一个比较有意思的场景,就是在画布上任意绘画。首先会用arkui实现如图的绘画设置窗口,如下图所示:
这个窗口使用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 强大的图形处理能力。它不仅支持基本的图形绘制,还能处理复杂的交互逻辑,如多边形和贝塞尔曲线的创建。通过事件驱动和实时更新,提供了流畅的用户绘图体验。
绘图效果如下图所示: