实现基于鸿蒙Canvas实现画布的案例,具备绘画、撤销重做、橡皮擦、清空、画笔属性设置、缩放等功能,涉及Canvas、手势事件、折叠屏适配等知识点。
一、案例效果截图
二、案例运用到的知识点
- 核心知识点
- Canvas:画布组件,用于自定义绘制图形。
- 手势事件:onTouch/gesture。
- 折叠屏适配:display。
- 其他知识点
- 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 // 应用静态资源目录
四、公共文件与资源
本案例涉及到的常量类和工具类代码如下:
- 通用常量类
// 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]
}
本案例涉及到的资源文件如下:
- 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)
}
}
✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接