简介

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组件实现涂鸦功能?的更多相关文章

  1. vue组件:canvas实现图片涂鸦功能

    方案背景 需求 需要对图片进行标注,导出图片. 需要标注N多图片最后同时保存. 需要根据多边形区域数据(区域.颜色.名称)标注. 对应方案 用canvas实现涂鸦.圆形.矩形的绘制,最终生成图片bas ...

  2. OpenHarmony 3.1 Beta 版本关键特性解析——ArkUI canvas组件

    (以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点) 江英杰 华为技术有限公司 canvas 是 ArkUI 开发框架里的画布组件,常用于自定义绘制图形.因为其轻量.灵活. ...

  3. Canvas组件:画布,可以实现动画操作。

    Module  10 Canvas组件:画布,可以实现动画操作. TextArea:文本域. 在单行文本域中回车会激发ActionEvent. 用CheckBoxGroup实现单选框功能. Java中 ...

  4. Canvas组件:画布,可以实现动画操作

    Canvas组件:画布,可以实现动画操作. TextArea:文本域. 在单行文本域中回车会激发ActionEvent. 用CheckBoxGroup实现单选框功能. Java中,单选框和复选框都是使 ...

  5. GUI的最终选择 Tkinter(六):Canvas组件

    Canvas组件,是一个可以让你任性的组件,一个可以让你随心所欲地绘制界面的组件.Canvas是一个通用的组件,它通常用于显示和编辑图形,可以用它来绘制直线,圆形,多边形,甚至是绘制其他组件. 在Ca ...

  6. 关于微信小程序前端Canvas组件教程

    关于微信小程序前端Canvas组件教程 微信小程序Canvas接口函数 ​ 上述为微信小程序Canvas的内部接口,通过熟练使用Canvas,即可画出较为美观的前端页面.下面是使用微信小程序画图的一些 ...

  7. 如何利用阿里视频云开源组件,快速自定义你的H5播放器?

    摘要: Aliplayer希望提供一种方便.简单.灵活的机制,让客户能够扩展播放器的功能,并且Aliplayer提供一些组件的基本实现,用户可以基于这些开源的组件实现个性化功能,比如自定义UI和自己A ...

  8. canvas模仿微信抢红包功能

    1.原理:先创建一张img图片,用filter滤镜制作毛玻璃效果. 2.利用绝对定位,使canvas刚好盖在img上面. 3.利用canvas原生clip函数剪辑一个圆形. 地址:http://san ...

  9. 利用Bootstrap Paginator插件和KnockoutJS完成分页功能

    在最近一个项目中,需要结合一堆条件查询并对查询的结果数据完成一个简单分页功能,可是做着做着,自己的思路越来越模糊,做到心态崩溃!!! 哈哈,特此花点时间重新总结,并从最简单的分页,然后向多条件查询分页 ...

  10. 042——VUE中组件之子组件使用$on与$emit事件触发父组件实现购物车功能

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

随机推荐

  1. Unity3D常用方法

    1.StartCoroutine(Thread1()) 启动协程运行Thread1()方法. 注意是协程,不是线程,详情见:https://www.jianshu.com/p/6d923cb0c900 ...

  2. 【Azure 应用服务】Azure Function Python函数部署到Azure后遇见 Value cannot be null. (Parameter 'receiverConnectionString') 错误

    问题描述 使用VS Code创建Python Function,处理Event Hub中的数据.当部署到Azure Function App后,函数无法执行,查看 Function 日志出现 Valu ...

  3. 【Azure 环境】自动化账号生成的时候怎么生成连接与证书

    问题描述 自动化账号生成的时候怎么生成连接与证书? 什么是自动化? Azure 自动化提供基于云的自动化和配置服务,用于支持 Azure 环境和非 Azure 环境之间的一致管理. Azure 自动化 ...

  4. TensorFlow 回归模型

    TensorFlow 回归模型 首先,导入所需的库和模块.代码中使用了numpy进行数值计算,matplotlib进行数据可视化,tensorflow进行机器学习模型的构建和训练,sklearn进行多 ...

  5. Nebula Graph 特性讲解——RocksDB 统计信息的收集和展示

    由于 Nebula Graph 的底层存储使用了 RocksDB,出于运维管理需要,我们的社区用户 @chenxu14 在 pr#2243 为 Nebula Graph 贡献了 RocksDB 统计信 ...

  6. 【Openxml】如何为OpenXml元素创建超链接

    已知在OpenXml有以下几种超链接 功能 说明 跳转页面 跳转某一页:ppaction://hlinksldjump跳转第一页:ppaction://hlinkshowjump?jump=first ...

  7. python中json.dumps() 与json.dump(),json.load()与json.loads()区别?

    json.dumps() 将 Python 对象转换为 JSON 字符串,并返回该字符串.而 json.dump() 将 Python 对象转换为 JSON 字符串,并将该字符串写入文件. json. ...

  8. 8、mysql的内存管理及优化

    内存优化原则 1) 将尽量多的内存分配给MySQL做缓存,但要给操作系统和其他程序预留足够内存. 2) MyISAM 存储引擎的数据文件读取依赖于操作系统自身的IO缓存,因此,如果有MyISAM表,就 ...

  9. k8s实战之MySQL单实例部署

    前面我们学习了k8s入门系列文章,了解了k8s的一些基础概念以及怎么使用.本篇文章将进行一个小小的实战,使用k8s来部署单机版的mysql数据库,基本涵盖到前面讲到的Namespace.Pod.Dep ...

  10. MySQL之过滤条件

    [一]筛选过滤条件 [1]查询语句 -- 查询当前表中的指定字段的数据 select id,name from emp where id > 3; [2]创建数据表 create databas ...