如何利用OpenHarmony ArkUI的Canvas组件实现涂鸦功能?
简介
ArkUI是一套UI开发框架,提供了开发者进行应用UI开发时所需具备的能力。随着OpenAtom OpenHarmony(以下简称“OpenHarmony”)不断更新迭代,ArkUI也提供了很多新的组件,例如Canvas、OffscreenCanvas、XComponent组件等。
新增的功能可以帮助开发者开发出更流畅、更美观的应用。本篇文章将为大家分享如何通过Canvas组件实现涂鸦功能,用户可以选择空白画布或者简笔图进行自由绘画。
效果展示
以下为效果图:



首页显示了涂鸦的图片以及最后一张空白图片,在点击图片进入涂鸦页面后,可以对画笔的颜色、粗细进行设置。如果涂鸦过程中有错误,可以用橡皮擦将画面擦除,也可点击清除按钮,清空涂鸦的内容,重新进行涂鸦操作。
相关代码已经上传至SIG仓库,链接如下:
https://gitee.com/openharmony-sig/knowledge_demo_entainment/tree/master/FA/FreeDraw
目录结构

源码分析
一、Canvas组件介绍
本篇样例主要利用ArkUI的Canvas组件实现涂鸦的功能,首先介绍一下Canvas组件。
Canvas组件主要包含了Canvas和CanvasRenderingContext2D,Canvas提供了画布功能,CanvasRenderingContext2D提供了绘画的属性和方法。通过CanvasRenderingContext2D可以修改画笔的样色、粗细等属性,从而画出各式各样的图形。
以下是Canvas和CanvasRenderingContext2D在样例开发中使用的相关接口信息。

CanvasRenderingContext2D

二、分析源码页面布局
第一个模块是首页布局,首页显示所有涂鸦包含的图片,点击图片可以进入页面;第二个模块是涂鸦模块,可以设置画笔的颜色、边条宽度等。
1. 首页布局
Column() {
Text('选择涂鸦的图片:').margin('10vp').fontSize('30fp').fontColor(Color.Blue).height('5%')
Grid() {
ForEach(this.images, (item, index) => {
GridItem() {
Image(this.images[index])
.onClick((event) => {
router.push(
{
url: "pages/detailPage",
params: {
imgSrc: this.images[index],
},
}
)
})
.width('100%')
.height('100%')
.objectFit(ImageFit.Contain)
}
})
}
.padding({left: this.columnSpace, right: this.columnSpace})
.columnsTemplate("1fr 1fr 1fr") // Grid宽度均分成3份
.rowsTemplate("1fr 1fr") // Grid高度均分成2份
.rowsGap(this.rowSpace) // 设置行间距
.columnsGap(this.columnSpace) // 设置列间距
.width('100%')
.height('95%')
}
.backgroundColor(Color.Pink)
2. 涂鸦页面 - 画布Canvas的布局通过Stack组件进行包裹,并将Canvas画布覆盖在选择的背景图片之上,这些背景图片主要是水果简笔画。
Stack() {
Image(this.imgSrc).width('100%').height('100%').objectFit(ImageFit.Contain)
Canvas(this.context)
.width('100%')
.height('100%')
// .backgroundColor('#00ffff00')
.onReady(() => {
})
.onTouch((event) => {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
this.drawing = true;
[this.x, this.y] = [event.touches[0].x, event.touches[0].y];
this.context.beginPath();
this.context.lineCap = 'round';
if (this.isEraserMode) {
//橡皮擦模式
this.context.clearRect(this.x, this.y, 20, 20);
}
console.log('gyf Down');
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
this.drawing = false;
console.log('gyf Up!');
this.context.closePath();
}
if (event.type === TouchType.Move) {
if (!this.drawing) return;
this.eventType = 'Move';
console.log('gyf Move');
if (this.isEraserMode) {
//橡皮擦模式
this.context.clearRect(event.touches[0].x, event.touches[0].y, 20, 20);
} else {
this.context.lineWidth = this.lineWidth;
this.context.strokeStyle = this.color;
this.context.moveTo(this.x, this.y);
this.x = event.touches[0].x;
this.y = event.touches[0].y;
this.context.lineTo(this.x, this.y);
this.context.stroke();
}
}
})
}.width('100%').height('75%')
3.涂鸦页面 - 画笔设置区域的布局
Column() {
Row() {
Text('粗细:')
Button('小').onClick(() => {
//设置画笔的宽度
this.lineWidth = 5;
this.context.lineWidth = this.lineWidth;
this.isEraserMode = false;
console.log('gyf small button');
}).margin($r('app.float.wh_value_10'))
Button('中').onClick(() => {
//设置画笔的宽度
this.lineWidth = 15;
this.context.lineWidth = this.lineWidth;
this.isEraserMode = false;
console.log('gyf middle button');
}).margin($r('app.float.wh_value_10'))
Button('大').onClick(() => {
//设置画笔的宽度
this.lineWidth = 25;
this.context.lineWidth = this.lineWidth;
this.isEraserMode = false;
console.log('gyf big button');
}).margin($r('app.float.wh_value_10'))
Button('超大').onClick(() => {
//设置画笔的宽度
this.lineWidth = 40;
this.context.lineWidth = this.lineWidth;
this.isEraserMode = false;
console.log('gyf super big button');
})
}.padding($r('app.float.wh_value_10')).margin($r('app.float.wh_value_5'))
//画笔颜色
Scroll() {
Row() {
Text('颜色:')
Button(' ', { type: ButtonType.Circle })
.onClick(() => {
//黑色
this.color = '#000000';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
console.log('gyf black button');
})
.backgroundColor('#000000')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ', { type: ButtonType.Circle })
.onClick(() => {
//红色
this.color = '#FF0000';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
console.log('gyf red button');
})
.backgroundColor('#FF0000')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ', { type: ButtonType.Circle })
.onClick(() => {
//绿色
this.color = '#00FF00';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
console.log('gyf green button');
})
.backgroundColor('#00FF00')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ', { type: ButtonType.Circle })
.onClick(() => {
//蓝色
this.color = '#0000FF';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#0000FF')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ', { type: ButtonType.Circle })
.onClick(() => {
//棕色
this.color = '#A52A2A';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#A52A2A')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ',{type: ButtonType.Circle })
.onClick(() =>{
//紫色
this.color = '#800080';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#800080')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ',{type: ButtonType.Circle })
.onClick(() =>{
//紫红色
this.color = '#FF00FF';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#FF00FF')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ',{type: ButtonType.Circle })
.onClick(() =>{
//深蓝色
this.color = '#00008B';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#00008B')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ',{type: ButtonType.Circle })
.onClick(() =>{
//深天蓝
this.color = '#00BFFF';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#00BFFF')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ',{type: ButtonType.Circle })
.onClick(() =>{
//绿色
this.color = '#008000';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#008000')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ',{type: ButtonType.Circle })
.onClick(() =>{
//青绿色
this.color = '#32CD32';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#32CD32')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ',{type: ButtonType.Circle })
.onClick(() =>{
//橙色
this.color = '#FFA500';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#FFA500')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
Button(' ',{type: ButtonType.Circle })
.onClick(() =>{
//黄色
this.color = '#FFFF00';
this.context.strokeStyle = this.color;
this.isEraserMode = false;
})
.backgroundColor('#FFFF00')
.width('40vp')
.width('40vp')
.margin($r('app.float.wh_value_10'))
}.padding('10vp')
}
.scrollable(ScrollDirection.Horizontal)// 设置滚动条水平方向滚动
.margin($r('app.float.wh_value_5'))
Row(){
Image('/common/images/eraser.png')
.onClick(() =>{
//橡皮擦模式
this.isEraserMode = true;
console.log('gyf eraser button');
})
.width('50vp')
.height('50vp')
.margin('10vp')
Button('清理画板').onClick(() =>{
this.context.clearRect(0, 0, 1000, 1000);
})
}
.margin($r('app.float.wh_value_5'))
}
.width('100%')
.height('25%')
.alignItems(HorizontalAlign.Start)
三、逻辑代码
逻辑代码存在于Canvas的onTouch事件中,通过TouchType的Down、Up、Move来判断开始、移动和结束的动作。一笔完整的绘制包含一次Down和Up,其中有若干次的Move。橡皮擦模式通过clearRect接口实现擦除的功能。
.onTouch((event) => {
if (event.type === TouchType.Down) {
this.eventType = 'Down';
this.drawing = true;
[this.x, this.y] = [event.touches[0].x, event.touches[0].y];
this.context.beginPath();
this.context.lineCap = 'round';
if (this.isEraserMode) {
//橡皮擦模式
this.context.clearRect(this.x, this.y, 20, 20);
}
console.log('gyf Down');
}
if (event.type === TouchType.Up) {
this.eventType = 'Up';
this.drawing = false;
console.log('gyf Up!');
this.context.closePath();
}
if (event.type === TouchType.Move) {
if (!this.drawing) return;
this.eventType = 'Move';
console.log('gyf Move');
if (this.isEraserMode) {
//橡皮擦模式
this.context.clearRect(event.touches[0].x, event.touches[0].y, 20, 20);
} else {
this.context.lineWidth = this.lineWidth;
this.context.strokeStyle = this.color;
this.context.moveTo(this.x, this.y);
this.x = event.touches[0].x;
this.y = event.touches[0].y;
this.context.lineTo(this.x, this.y);
this.context.stroke();
}
}
})
总结
本文介绍了如何使用ArkUI框架提供的Canvas组件实现涂鸦功能。首先,通过Canvas的onTouch事件来跟踪Down、Move和Up的事件,再设置CanvasRenderingContext2D的相关属性并调用相关的方法,最终实现涂鸦的功能。除了文中分享的涂鸦样例,开发者还可以通过拓展其他相关的属性和方法,实现更多好玩的、高性能的样例。

如何利用OpenHarmony ArkUI的Canvas组件实现涂鸦功能?的更多相关文章
- vue组件:canvas实现图片涂鸦功能
方案背景 需求 需要对图片进行标注,导出图片. 需要标注N多图片最后同时保存. 需要根据多边形区域数据(区域.颜色.名称)标注. 对应方案 用canvas实现涂鸦.圆形.矩形的绘制,最终生成图片bas ...
- OpenHarmony 3.1 Beta 版本关键特性解析——ArkUI canvas组件
(以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点) 江英杰 华为技术有限公司 canvas 是 ArkUI 开发框架里的画布组件,常用于自定义绘制图形.因为其轻量.灵活. ...
- Canvas组件:画布,可以实现动画操作。
Module 10 Canvas组件:画布,可以实现动画操作. TextArea:文本域. 在单行文本域中回车会激发ActionEvent. 用CheckBoxGroup实现单选框功能. Java中 ...
- Canvas组件:画布,可以实现动画操作
Canvas组件:画布,可以实现动画操作. TextArea:文本域. 在单行文本域中回车会激发ActionEvent. 用CheckBoxGroup实现单选框功能. Java中,单选框和复选框都是使 ...
- GUI的最终选择 Tkinter(六):Canvas组件
Canvas组件,是一个可以让你任性的组件,一个可以让你随心所欲地绘制界面的组件.Canvas是一个通用的组件,它通常用于显示和编辑图形,可以用它来绘制直线,圆形,多边形,甚至是绘制其他组件. 在Ca ...
- 关于微信小程序前端Canvas组件教程
关于微信小程序前端Canvas组件教程 微信小程序Canvas接口函数 上述为微信小程序Canvas的内部接口,通过熟练使用Canvas,即可画出较为美观的前端页面.下面是使用微信小程序画图的一些 ...
- 如何利用阿里视频云开源组件,快速自定义你的H5播放器?
摘要: Aliplayer希望提供一种方便.简单.灵活的机制,让客户能够扩展播放器的功能,并且Aliplayer提供一些组件的基本实现,用户可以基于这些开源的组件实现个性化功能,比如自定义UI和自己A ...
- canvas模仿微信抢红包功能
1.原理:先创建一张img图片,用filter滤镜制作毛玻璃效果. 2.利用绝对定位,使canvas刚好盖在img上面. 3.利用canvas原生clip函数剪辑一个圆形. 地址:http://san ...
- 利用Bootstrap Paginator插件和KnockoutJS完成分页功能
在最近一个项目中,需要结合一堆条件查询并对查询的结果数据完成一个简单分页功能,可是做着做着,自己的思路越来越模糊,做到心态崩溃!!! 哈哈,特此花点时间重新总结,并从最简单的分页,然后向多条件查询分页 ...
- 042——VUE中组件之子组件使用$on与$emit事件触发父组件实现购物车功能
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
随机推荐
- Unity3D常用方法
1.StartCoroutine(Thread1()) 启动协程运行Thread1()方法. 注意是协程,不是线程,详情见:https://www.jianshu.com/p/6d923cb0c900 ...
- 【Azure 应用服务】Azure Function Python函数部署到Azure后遇见 Value cannot be null. (Parameter 'receiverConnectionString') 错误
问题描述 使用VS Code创建Python Function,处理Event Hub中的数据.当部署到Azure Function App后,函数无法执行,查看 Function 日志出现 Valu ...
- 【Azure 环境】自动化账号生成的时候怎么生成连接与证书
问题描述 自动化账号生成的时候怎么生成连接与证书? 什么是自动化? Azure 自动化提供基于云的自动化和配置服务,用于支持 Azure 环境和非 Azure 环境之间的一致管理. Azure 自动化 ...
- TensorFlow 回归模型
TensorFlow 回归模型 首先,导入所需的库和模块.代码中使用了numpy进行数值计算,matplotlib进行数据可视化,tensorflow进行机器学习模型的构建和训练,sklearn进行多 ...
- Nebula Graph 特性讲解——RocksDB 统计信息的收集和展示
由于 Nebula Graph 的底层存储使用了 RocksDB,出于运维管理需要,我们的社区用户 @chenxu14 在 pr#2243 为 Nebula Graph 贡献了 RocksDB 统计信 ...
- 【Openxml】如何为OpenXml元素创建超链接
已知在OpenXml有以下几种超链接 功能 说明 跳转页面 跳转某一页:ppaction://hlinksldjump跳转第一页:ppaction://hlinkshowjump?jump=first ...
- python中json.dumps() 与json.dump(),json.load()与json.loads()区别?
json.dumps() 将 Python 对象转换为 JSON 字符串,并返回该字符串.而 json.dump() 将 Python 对象转换为 JSON 字符串,并将该字符串写入文件. json. ...
- 8、mysql的内存管理及优化
内存优化原则 1) 将尽量多的内存分配给MySQL做缓存,但要给操作系统和其他程序预留足够内存. 2) MyISAM 存储引擎的数据文件读取依赖于操作系统自身的IO缓存,因此,如果有MyISAM表,就 ...
- k8s实战之MySQL单实例部署
前面我们学习了k8s入门系列文章,了解了k8s的一些基础概念以及怎么使用.本篇文章将进行一个小小的实战,使用k8s来部署单机版的mysql数据库,基本涵盖到前面讲到的Namespace.Pod.Dep ...
- MySQL之过滤条件
[一]筛选过滤条件 [1]查询语句 -- 查询当前表中的指定字段的数据 select id,name from emp where id > 3; [2]创建数据表 create databas ...