纯血鸿蒙APP实战开发——手写绘制及保存图片
介绍
本示例使用drawing库的Pen和Path结合NodeContainer组件实现手写绘制功能。手写板上完成绘制后,通过调用image库的packToFile和packing接口将手写板的绘制内容保存为图片,并将图片文件保存在应用沙箱路径中。
效果图预览

使用说明
- 在虚线区域手写绘制,点击撤销按钮撤销前一笔绘制,点击重置按钮清空绘制。
- 点击packToFile保存图片按钮和packing保存图片按钮可以将绘制内容保存为图片写入文件,显示图片的沙箱路径。
实现思路
- 创建NodeController的子类MyNodeController,用于获取根节点的RenderNode和绑定的NodeContainer组件宽高。
export class MyNodeController extends NodeController {private rootNode: FrameNode | null = null; // 根节点rootRenderNode: RenderNode | null = null; // 从NodeController根节点获取的RenderNode,用于添加和删除新创建的MyRenderNode实例width: number = 0; // 实例绑定的NodeContainer组件的宽,单位pxheight: number = 0; // 实例绑定的NodeContainer组件的宽,单位px// MyNodeController实例绑定的NodeContainer创建时触发,创建根节点rootNode并将其挂载至NodeContainermakeNode(uiContext: UIContext): FrameNode {this.rootNode = new FrameNode(uiContext);if (this.rootNode !== null) {this.rootRenderNode = this.rootNode.getRenderNode();}return this.rootNode;}// 绑定的NodeContainer布局时触发,获取NodeContainer的宽高aboutToResize(size: Size): void {this.width = size.width;this.height = size.height;// 设置画布底色为白色if (this.rootRenderNode !== null) {// NodeContainer布局完成后设置rootRenderNode的背景色为白色this.rootRenderNode.backgroundColor = 0XFFFFFFFF;// rootRenderNode的位置从组件NodeContainer的左上角(0,0)坐标开始,大小为NodeContainer的宽高this.rootRenderNode.frame = { x: 0, y: 0, width: this.width, height: this.height };}}
}
- 创建RenderNode的子类MyRenderNode,初始化画笔和绘制path路径。
export class MyRenderNode extends RenderNode {path: drawing.Path = new drawing.Path(); // 新建路径对象,用于绘制手指移动轨迹// RenderNode进行绘制时会调用draw方法,初始化画笔和绘制路径draw(context: DrawContext): void {const canvas = context.canvas;// 创建一个画笔Pen对象,Pen对象用于形状的边框线绘制const pen = new drawing.Pen();// 设置画笔开启反走样,可以使得图形的边缘在显示时更平滑pen.setAntiAlias(true);// 设置画笔颜色为黑色const pen_color: common2D.Color = { alpha: 0xFF, red: 0x00, green: 0x00, blue: 0x00 };pen.setColor(pen_color);// 开启画笔的抖动绘制效果。抖动绘制可以使得绘制出的颜色更加真实。pen.setDither(true);// 设置画笔的线宽为5pxpen.setStrokeWidth(5);// 将Pen画笔设置到canvas中canvas.attachPen(pen);// 绘制pathcanvas.drawPath(this.path);}
}
- 创建变量currentNode用于存储当前正在绘制的节点,变量nodeCount用来记录已挂载的节点数量。
private currentNode: MyRenderNode | null = null; // 当前正在绘制的节点private nodeCount: number = 0; // 已挂载到根节点的子节点数量
- 创建自定义节点容器组件NodeContainer,接收MyNodeController的实例,将自定义的渲染节点挂载到组件上,实现自定义绘制。
NodeContainer(this.myNodeController).width('100%').height($r('app.integer.hand_writing_canvas_height')).onTouch((event: TouchEvent) => {this.onTouchEvent(event);}).id(NODE_CONTAINER_ID)
- 在NodeContainer组件的onTouch回调函数中,手指按下创建新的节点并挂载到rootRenderNode,nodeCount加一,手指移动更新节点中的path对象,绘制移动轨迹,并将节点重新渲染。
onTouchEvent(event: TouchEvent): void {// TODO:知识点:在手指按下时创建新的MyRenderNode对象,挂载到rootRenderNode上,手指移动时根据触摸点坐标绘制线条,并重新渲染节点// 获取手指触摸位置的坐标点const positionX: number = vp2px(event.touches[0].x);const positionY: number = vp2px(event.touches[0].y);logger.info(TAG, `Touch positionX: ${positionX}, Touch positionY: ${positionY}`);switch (event.type) {case TouchType.Down: {// 每次手指按下,创建一个MyRenderNode对象,用于记录和绘制手指移动的轨迹const newNode = new MyRenderNode();// 定义newNode的大小和位置,位置从组件NodeContainer的左上角(0,0)坐标开始,大小为NodeContainer的宽高newNode.frame = { x: 0, y: 0, width: this.myNodeController.width, height: this.myNodeController.height };this.currentNode = newNode;// 移动新节点中的路径path到手指按下的坐标点this.currentNode.path.moveTo(positionX, positionY);if (this.myNodeController.rootRenderNode !== null) {// appendChild在renderNode最后一个子节点后添加新的子节点this.myNodeController.rootRenderNode.appendChild(this.currentNode);// 已挂载的节点数量加一this.nodeCount++;}break;}case TouchType.Move: {if (this.currentNode !== null) {// 手指移动,绘制移动轨迹this.currentNode.path.lineTo(positionX, positionY);// 节点的path更新后需要调用invalidate()方法触发重新渲染this.currentNode.invalidate();}break;}case TouchType.Up: {// 手指抬起,释放this.currentNodethis.currentNode = null;}default: {break;}}}
- rootRenderNode调用getChild方法获取最后一个挂载的子节点,再使用removeChild方法移除,实现撤销上一笔的效果。
goBack() {if (this.myNodeController.rootRenderNode !== null && this.nodeCount > 0) {// getChild获取最后挂载的子节点const node = this.myNodeController.rootRenderNode.getChild(this.nodeCount - 1);// removeChild移除指定子节点this.myNodeController.rootRenderNode.removeChild(node);this.nodeCount--;}}
- 使用clearChildren清除当前rootRenderNode的所有子节点,实现画布重置,nodeCount清零。
resetCanvas() {if (this.myNodeController.rootRenderNode !== null && this.nodeCount > 0) {// 清除当前rootRenderNode的所有子节点this.myNodeController.rootRenderNode.clearChildren();this.nodeCount = 0;}}
- 使用componentSnapshot.get获取组件NodeContainer的PixelMap对象,用于保存图片
componentSnapshot.get(NODE_CONTAINER_ID, async (error: Error, pixelMap: image.PixelMap) => {if (pixelMap !== null) {// 图片写入文件this.filePath = await this.saveFile(getContext(), pixelMap);logger.info(TAG, `Images saved using the packing method are located in : ${this.filePath}`);}})
- 使用image库的packToFile()和packing()将获取的PixelMap对象保存为图片,并将图片文件保存在应用沙箱路径中。
- ImagePacker.packToFile()可直接将PixelMap对象写入为图片。
async packToFile(context: Context, pixelMap: PixelMap): Promise<string> {// 创建图像编码ImagePacker对象const imagePackerApi = image.createImagePacker();// 设置编码输出流和编码参数。format为图像的编码格式;quality为图像质量,范围从0-100,100为最佳质量const options: image.PackingOption = { format: "image/jpeg", quality: 100 };// 图片写入的沙箱路径const filePath: string = `${context.filesDir}/${getTimeStr()}.jpg`;const file: fs.File = await fs.open(filePath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);// 使用packToFile直接将pixelMap写入文件await imagePackerApi.packToFile(pixelMap, file.fd, options);fs.closeSync(file);return filePath;}
- ImagePacker.packing()可获取图片的ArrayBuffer数据,再使用fs将数据写入为图片。
async saveFile(context: Context, pixelMap: PixelMap): Promise<string> {// 创建图像编码ImagePacker对象const imagePackerApi = image.createImagePacker();// 设置编码输出流和编码参数。format为图像的编码格式;quality为图像质量,范围从0-100,100为最佳质量const options: image.PackingOption = { format: "image/jpeg", quality: 100 };// 图片写入的沙箱路径const filePath: string = `${context.filesDir}/${getTimeStr()}.jpg`;const file: fs.File = await fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);// 使用packing打包获取图片的ArrayBufferconst data: ArrayBuffer = await imagePackerApi.packing(pixelMap, options);// 将图片的ArrayBuffer数据写入文件fs.writeSync(file.fd, data);fs.closeSync(file);return filePath;}
高性能知识点
不涉及
工程结构&模块类型
handwritingtoimage // har类型
|---/src/main/ets/model
| |---RenderNodeModel.ets // 模型层-节点数据模型
|---/src/main/ets/view
| |---HandWritingToImage.ets // 视图层-手写板场景页面
模块依赖
- 本实例依赖common模块中的日志工具类logger。
参考资料
@ohos.graphics.drawing (绘制模块)
NodeController
自渲染节点RenderNode
@ohos.multimedia.image(图片处理)
@ohos.file.fs(文件管理)
鸿蒙全栈开发全新学习指南
也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线【包含了大厂APP实战项目开发】。
本路线共分为四个阶段:
第一阶段:鸿蒙初中级开发必备技能

第二阶段:鸿蒙南北双向高工技能基础:gitee.com/MNxiaona/733GH

第三阶段:应用开发中高级就业技术

第四阶段:全网首发-工业级南向设备开发就业技术:https://gitee.com/MNxiaona/733GH

《鸿蒙 (Harmony OS)开发学习手册》(共计892页)
如何快速入门?
1.基本概念
2.构建第一个ArkTS应用
3.……

开发基础知识:gitee.com/MNxiaona/733GH
1.应用基础知识
2.配置文件
3.应用数据管理
4.应用安全管理
5.应用隐私保护
6.三方应用调用管控机制
7.资源分类与访问
8.学习ArkTS语言
9.……

基于ArkTS 开发
1.Ability开发
2.UI开发
3.公共事件与通知
4.窗口管理
5.媒体
6.安全
7.网络与链接
8.电话服务
9.数据管理
10.后台任务(Background Task)管理
11.设备管理
12.设备使用信息统计
13.DFX
14.国际化开发
15.折叠屏系列
16.……

鸿蒙开发面试真题(含参考答案):gitee.com/MNxiaona/733GH

鸿蒙入门教学视频:

美团APP实战开发教学:gitee.com/MNxiaona/733GH

写在最后
- 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
- 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
- 想要获取更多完整鸿蒙最新学习资源,请移步前往小编:
gitee.com/MNxiaona/733GH

相关文章:
纯血鸿蒙APP实战开发——手写绘制及保存图片
介绍 本示例使用drawing库的Pen和Path结合NodeContainer组件实现手写绘制功能。手写板上完成绘制后,通过调用image库的packToFile和packing接口将手写板的绘制内容保存为图片,并将图片文件保存在应用沙箱路径中。 效果图预览 使用说明 在虚线区域手写…...
在什么情况下表单会被重复提交?如何避免?
表单被重复提交是Web应用中常见的问题,通常在用户提交表单后点击按钮多次,或在表单提交后刷新页面时发生。这可能导致数据的重复处理,比如重复记录或订单。 何时会发生表单重复提交? 用户多次点击提交按钮:在网络延迟…...
JavaScript 中的 Class 类
🔥 个人主页:空白诗 文章目录 🔥 引言🎯 基础知识🏗️ 构造函数 (Constructor)🔐 私有字段 (Private Fields)🔐 私有方法 (Private Methods)🧬 继承 (Inheritance)📦 静态…...
python实验三 实现UDP协议、TCP协议进行服务器端与客户端的交互
实验三 实验题目 1、请利用生成器构造一下求阶乘的函数Factorial(),定义一个函数m(),在m()中调用生成器Factorial()生成小于100的阶乘序列存入集合s中,输出s。 【代码】 def factorial():n1f1while 1: f * n yield (f) n1…...
ServiceNow 研究:通过RAG减少结构化输出中的幻觉
论文地址:https://arxiv.org/pdf/2404.08189 原文地址:rag-hallucination-structure-research-by-servicenow 在灾难性遗忘和模型漂移中,幻觉仍然是一个挑战。 2024 年 4 月 18 日 灾难性遗忘: 这是在序列学习或连续学习环境中出现…...
ADS基础教程10-多态性(动态模型选择)
目录 一、多态性定义二、操作步骤1.模型建立2.模型选择3.执行仿真 一、多态性定义 ADS中支持一个Symbol中,可以同时存在多个子图。在仿真时可以动态选择不同的子图继续宁仿真。 二、操作步骤 1.模型建立 在上一章A…...
代码随想录第四十六天|单词拆分
题目链接:. - 力扣(LeetCode)...
RabbitMQ的介绍和使用
1.同步通讯和异步通讯 举个例子,同步通讯就像是在打电话,因此它时效性较强,可以立即得到结果,但如果你正在和一个MM打电话,其他MM找你的话,你们之间是不能进行消息的传递和响应的 异步通讯就像是微信&#…...
前端get请求日期类型参数向后端传参失败
1、背景 get请求,通过url上传参,因此日期类型是string类型数据 2、异常信息 nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.time.LocalDate] for…...
【docker 】 push 镜像提示:denied: requested access to the resource is denied
往 Docker Registry (私服)push 镜像提示:denied: requested access to the resource is denied 镜像push 语法:docker push <registry-host>:<registry-port>/<repository>:<tag> docker push 192.16…...
浏览器各类好用插件使用及常见问题(技巧)总结
目录 Vimium C快捷键问题为什么Vimium C - 全键盘操作浏览器插件在百度页面中, x ,o,f等快捷键不起作用如何使用viminum c插件进行自定义快捷键?vimucm 为什么在浏览器首页时快捷键不起作用? 网页截图问题firefox 网页截图使用 idm问题浏览器点击idm 不下载? 待续、更新中 V…...
Python批量计算多张遥感影像的NDVI
本文介绍基于Python中的gdal模块,批量基于大量多波段遥感影像文件,计算其每1景图像各自的NDVI数值,并将多景结果依次保存为栅格文件的方法。 如下图所示,现在有大量.tif格式的遥感影像文件,其中均含有红光波段与近红外…...
6.k8s中的secrets资源
一、Secret secrets资源,类似于configmap资源,只是secrets资源是用来传递重要的信息的; secret资源就是将value的值使用base64编译后传输,当pod引用secret后,k8s会自动将其base64的编码,反编译回正常的字符…...
git 更换远程仓库地址三种方法总结
git 更换远程仓库地址三种方法总结 一、前言 由于私服的 gitlab 的地址变更,导致部分项目代码提交不上去,需要修改远端仓地址。 其它需要修改远程仓地址的情况如:切换git clone 协议由ssh变为https。 二、环境 windows 10git version 2.3…...
快速找出存(不存在)在某个(或多个)文件的文件夹
首先,需要用到的这个工具: 度娘网盘 提取码:qwu2 蓝奏云 提取码:2r1z 想要找出有下面这个文件存在的文件夹 切换到批量文件复制版块,快捷键Ctrl5 右侧,搜索添加 选定范围,勾选搜索文件夹、包…...
Linux USB转串口设备路径的查找方法
1、USB转串口设备 USB转串口设备是在嵌入式软件开发过程中经常要使用的,常常用于对接各种各样的串口设备。如果一台linux主机上使用多个usb转串口设备时,应用程序中就需要知道自己操作的是哪个串口设备。串口设备在系统上电时,由于驱动加载的…...
【初阶数据结构】单链表之环形链表
目录标题 前言环形链表的约瑟夫问题环形链表环形链表|| 前言 前面我们已经学习了关于单链表的一些基本东西,今天我们来学习单链表的一个拓展——环形链表,我们将用力扣和牛客网上的三道题目来分析讲解环形链表问题。 环形链表的约瑟夫问题 我们首先来看…...
【积分,微分,导数,偏导数公式推导】
1. 积分 积分是微积分的一个分支,用于计算曲边梯形的面积或者变速直线运动的总距离等。积分分为不定积分和定积分。 不定积分:给出一个函数,求出其所有可能的原函数。定积分:计算一个函数在特定区间上的积分。 2. 微分 微分是…...
java:递归实现的案例
//求第20个月兔子的对数 //每个月兔子对数:1,1,2,3,5,8 public class Test {//求第20个月兔子的对数//每个月兔子对数:1,1,2,3,5,8pu…...
Arxml文件解析03- 自动驾驶Radar服务radar_svc.arxml
<AR-PACKAGES><AR-PACKAGE><SHORT-NAME>bosch</SHORT-NAME><AR-PACKAGES>...</AR-PACKAGES>...
【Axure高保真原型】引导弹窗
今天和大家中分享引导弹窗的原型模板,载入页面后,会显示引导弹窗,适用于引导用户使用页面,点击完成后,会显示下一个引导弹窗,直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
Linux链表操作全解析
Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表?1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…...
【Linux】C语言执行shell指令
在C语言中执行Shell指令 在C语言中,有几种方法可以执行Shell指令: 1. 使用system()函数 这是最简单的方法,包含在stdlib.h头文件中: #include <stdlib.h>int main() {system("ls -l"); // 执行ls -l命令retu…...
工程地质软件市场:发展现状、趋势与策略建议
一、引言 在工程建设领域,准确把握地质条件是确保项目顺利推进和安全运营的关键。工程地质软件作为处理、分析、模拟和展示工程地质数据的重要工具,正发挥着日益重要的作用。它凭借强大的数据处理能力、三维建模功能、空间分析工具和可视化展示手段&…...
从零实现STL哈希容器:unordered_map/unordered_set封装详解
本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说,直接开始吧! 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
Swagger和OpenApi的前世今生
Swagger与OpenAPI的关系演进是API标准化进程中的重要篇章,二者共同塑造了现代RESTful API的开发范式。 本期就扒一扒其技术演进的关键节点与核心逻辑: 🔄 一、起源与初创期:Swagger的诞生(2010-2014) 核心…...
Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
uni-app 中 Web-view 与 Vue 页面的通讯机制详解 一、Web-view 简介 Web-view 是 uni-app 提供的一个重要组件,用于在原生应用中加载 HTML 页面: 支持加载本地 HTML 文件支持加载远程 HTML 页面实现 Web 与原生的双向通讯可用于嵌入第三方网页或 H5 应…...
