欢迎光临
我们一直在努力

HarmonyOS7 Canvas 绘图:从画一条线到做签名板,我踩过的坑都在这

文章目录

    • 前言
    • Canvas 基础概念
    • 绘制基本图形
      • 画一条线
      • 画圆
      • 画矩形
    • 手势 + Canvas 实现签名板
      • 完整代码
      • 关键逻辑讲解
    • 导出图片
    • 性能注意
    • 写在最后

A hand-drawn doodle illustration on pure white pap

前言

说实话,刚接触 HarmonyOS7 的 Canvas 时我是懵的。Web 里写 Canvas 闭着眼睛都能画,到了鸿蒙这儿,上下文怎么创建、怎么重绘、手势怎么跟画布联动——全是坑。折腾了两个周末,从画一条线到撸出一个完整的签名板,踩过的坑比画过的线还多。今天把这些经验整理出来,希望你少走弯路。

Canvas 基础概念

HarmonyOS7 的 Canvas 跟 Web 的 Canvas API 长得很像,但用法完全不一样。最大的区别就是:你得手动创建 CanvasRenderingContext2D 实例,然后绑到 Canvas 组件上。

@State ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings({ antialias: true }))

划重点:new Settings({ antialias: true }) 这个参数一定要加,不然画出来的线全是锯齿,丑得没法看。

Canvas 组件本身的用法倒是很简单:

Canvas(this.ctx)
.width('100%')
.height(300)
.backgroundColor('#FFFFFF')
.onReady(() => {
this.draw()
})

onReady 是关键——画布准备好了才能画,跟 Web 里 window.onload 一个道理。别在 aboutToAppear 里画,那时候画布还没初始化,必报错。

绘制基本图形

画一条线

最简单的直线,3 步搞定:

drawLine() {
this.ctx.beginPath()
this.ctx.moveTo(50, 50) // 起点
this.ctx.lineTo(250, 150) // 终点
this.ctx.strokeStyle = '#FF0000'
this.ctx.lineWidth = 3
this.ctx.stroke() // 描边
}

A hand-drawn doodle illustration on pure white pap

  • beginPath():开始新路径,每次画之前都要调,不然会接着上一条路径画
  • moveTo / lineTo:移动画笔 / 画线到指定坐标
  • stroke():描边渲染,不调这个线不会显示

画圆

drawCircle() {
this.ctx.beginPath()
this.ctx.arc(150, 150, 60, 0, Math.PI * 2) // (圆心x, 圆心y, 半径, 起始角, 结束角)
this.ctx.fillStyle = '#4CAF50'
this.ctx.fill() // 填充
this.ctx.stroke() // 也可以同时描边
}

Math.PI * 2 就是 360 度,画完整圆。fill() 填充颜色,stroke() 描边,可以两个都调。

画矩形

矩形最简单,一行代码搞定:

drawRect() {
this.ctx.fillStyle = '#2196F3'
this.ctx.fillRect(50, 50, 200, 100) // (x, y, 宽, 高)
}

还有 strokeRect 画边框矩形,clearRect 清除矩形区域——这三个方法不需要 beginPath()。

手势 + Canvas 实现签名板

这是重头戏。签名板的核心就是:手指动,画笔跟着动。需要 PanGesture 配合 Canvas 实时绘制。

完整代码

A hand-drawn doodle illustration on pure white pap

@Entry
@Component
struct SignaturePad {
@State ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(new Settings({ antialias: true }))
private lastX: number = 0
private lastY: number = 0
@State penWidth: number = 4
@State penColor: string = '#000000'
@State paths: DrawPath[] = []

build() {
Column({ space: 12 }) {
// 工具栏
Row({ space: 12 }) {
Button('清空')
.onClick(() => this.clearCanvas())
Button('保存')
.onClick(() => this.saveImage())
Slider({ min: 2, max: 10, step: 1, value: this.penWidth })
.onChange((value: number) => { this.penWidth = value })
.width(120)
}
.padding(10)

// 签名画布
Canvas(this.ctx)
.width('100%')
.height(300)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#CCCCCC', style: BorderStyle.Dashed })
.gesture(
PanGesture({ direction: PanDirection.All })
.onActionStart((event: GestureEvent) => {
this.lastX = event.x
this.lastY = event.y
this.ctx.beginPath()
this.ctx.moveTo(event.x, event.y)
})
.onActionUpdate((event: GestureEvent) => {
// 用二次贝塞尔曲线让笔迹更平滑
const midX = (this.lastX + event.x) / 2
const midY = (this.lastY + event.y) / 2
this.ctx.quadraticCurveTo(this.lastX, this.lastY, midX, midY)
this.ctx.strokeStyle = this.penColor
this.ctx.lineWidth = this.penWidth
this.ctx.lineCap = 'round'
this.ctx.lineJoin = 'round'
this.ctx.stroke()
this.lastX = event.x
this.lastY = event.y
})
.onActionEnd(() => {
this.ctx.closePath()
})
)
}
.padding(16)
}

clearCanvas() {
this.ctx.clearRect(0, 0, this.ctx.width, this.ctx.height)
}

saveImage() {
// 见下一节
}
}

关键逻辑讲解

1. 为什么用 quadraticCurveTo 而不是 lineTo?

lineTo 画出来的是折线,手指移动快的时候点间距大,效果就是一截一截的。quadraticCurveTo(二次贝塞尔曲线)取两个点的中点做控制,曲线会自然过渡,笔迹丝滑。

2. lineCap = 'round' 和 lineJoin = 'round' 必须设

不设的话,线段端点是方形的,拐角是尖角的,写出来的字像用尺子画的。设成 round 才有圆头圆角的手写感。

3. onActionStart 里记 lastX/lastY

每次手势开始,把起始点记下来。onActionUpdate 里用上一个点和当前点算中点,这样才能画贝塞尔曲线。别忘了在 update 里更新 lastX/lastY,不然曲线会画飞。

导出图片

签名画完了得保存,不然白搭。HarmonyOS7 用 toDataURL 把 Canvas 内容导出成图片:

saveImage() {
const imageData = this.ctx.toDataURL('image/png')
// imageData 是 base64 格式的字符串
// 解码后用 fs 写入文件,或者用 photoAccessHelper 保存到相册
const buffer = this.base64ToArrayBuffer(imageData.split(',')[1])

const context = getContext(this)
const filePath = context.filesDir + '/signature_' + Date.now() + '.png'
const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
fs.writeSync(file.fd, buffer)
fs.closeSync(file.fd)
}

base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryStr = atob(base64)
const buffer = new ArrayBuffer(binaryStr.length)
const uint8 = new Uint8Array(buffer)
for (let i = 0; i < binaryStr.length; i++) {
uint8[i] = binaryStr.charCodeAt(i)
}
return buffer
}

血泪教训:toDataURL 返回的格式是 data:image/png;base64,xxxxx,记得用 split(',')[1] 把前缀去掉再解码。

想保存到系统相册的话,需要用 photoAccessHelper.createAsset 创建媒体资源,配合权限 ohos.permission.WRITE_IMAGEVIDEO 使用。

性能注意

搞 Canvas 绘图,性能问题迟早会遇到。几点经验:

  • 别频繁调 clearRect 再重绘全部内容。签名板场景下,每次 update 只追加绘制就行,不需要清空重来
  • lineWidth 别太大,超过 20 在低端设备上会有明显卡顿
  • 画布尺寸别超过屏幕分辨率,太大的 Canvas 吃内存
  • 手势回调里别做耗时操作,比如文件 IO,会阻塞绘制导致丢帧

还有个坑:HarmonyOS7 的 Canvas 在某些场景下需要手动调 invalidate() 触发重绘。签名板里因为每次手势 update 都在画,所以没问题。但如果你改了状态变量想刷新画面,记得手动触发。

写在最后

从画一条线到做签名板,看着不难,实际上坑不少。上下文创建方式、重绘时机、手势联动、贝塞尔曲线平滑——每个环节都能卡你半小时。不过搞通之后,Canvas 这块就算入门了,后面做什么涂鸦板、图表绘制都有底了。

签名板的代码我精简过了,实际项目建议加上撤销功能(存每步路径重绘)和压感支持(配合 SourceTool.Pen 区分手写笔)。下一篇聊聊属性动画,让 UI 不再死气沉沉。

赞(0)
未经允许不得转载:171主机测评 » HarmonyOS7 Canvas 绘图:从画一条线到做签名板,我踩过的坑都在这
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址