HarmonyOS 开发-手写绘制及保存图片

news2024/11/25 12:23:10

介绍

本示例使用drawing库的Pen和Path结合NodeContainer组件实现手写绘制功能,并通过image库的packToFile和packing接口将手写板的绘制内容保存为图片。

效果图预览

使用说明

  1. 在虚线区域手写绘制,点击撤销按钮撤销前一笔绘制,点击重置按钮清空绘制。
  2. 点击packToFile保存图片按钮和packing保存图片按钮可以将绘制内容保存为图片写入文件,显示图片保存路径。

实现思路

  1. 创建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组件的宽,单位px
  height: number = 0; // 实例绑定的NodeContainer组件的宽,单位px

  // MyNodeController实例绑定的NodeContainer创建时触发,创建根节点rootNode并将其挂载至NodeContainer
  makeNode(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 };
    }
  }
}
  1. 创建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);
    // 设置画笔的线宽为5px
    pen.setStrokeWidth(5);
    // 将Pen画笔设置到canvas中
    canvas.attachPen(pen);
    // 绘制path
    canvas.drawPath(this.path);
  }
}
  1. 创建变量currentNode用于存储当前正在绘制的节点,变量nodeCount用来记录已挂载的节点数量。
  private currentNode: MyRenderNode | null = null; // 当前正在绘制的节点
  private nodeCount: number = 0; // 已挂载到根节点的子节点数量
  1. 创建自定义节点容器组件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)
  1. 在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.currentNode
        this.currentNode = null;
      }
      default: {
        break;
      }
    }
  }
  1. 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--;
    }
  }
  1. 使用clearChildren清除当前rootRenderNode的所有子节点,实现画布重置,nodeCount清零。
  resetCanvas() {
    if (this.myNodeController.rootRenderNode !== null && this.nodeCount > 0) {
      // 清除当前rootRenderNode的所有子节点
      this.myNodeController.rootRenderNode.clearChildren();
      this.nodeCount = 0;
    }
  }
  1. 使用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}`);
    }
  })
  1. 使用image库的packToFile()和packing()将获取的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打包获取图片的ArrayBuffer
    const 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                // 视图层-手写板场景页面

为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05

《鸿蒙开发学习手册》:

如何快速入门:https://qr21.cn/FV7h05

  1. 基本概念
  2. 构建第一个ArkTS应用
  3. ……

开发基础知识:https://qr21.cn/FV7h05

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

基于ArkTS 开发:https://qr21.cn/FV7h05

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

鸿蒙开发面试真题(含参考答案):https://qr18.cn/F781PH

鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH

1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1580455.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

动态路由-基于vue-admin-template

基于 vue-admin-template的动态路由 1. 拆分静态路由与动态路由 静态路由----所有人都可以访问—首页/登录/404 动态路由–有权限的人才可以访问—组织/角色/员工/权限 2. 根据用户权限添加动态路由 获取对应的权限标识(vuex中actions中把用户资料通过return 进行返回&…

代码随想录算法训练营DAY17|C++二叉树Part.4|110.平衡二叉树、257.二叉树的所有路径、404.左叶子之和

文章目录 110.平衡二叉树思路伪代码CPP代码 257.二叉树的所有路径思路伪代码实现CPP代码 404.左叶子之和思路伪代码CPP代码 110.平衡二叉树 力扣题目链接 文章讲解&#xff1a;110.平衡二叉树 视频讲解&#xff1a;后序遍历求高度&#xff0c;高度判断是否平衡 | LeetCode&…

CSS导读 (复合选择器)

&#xff08;大家好&#xff0c;今天我们将继续来学习CSS的相关知识&#xff0c;大家可以在评论区进行互动答疑哦~加油&#xff01;&#x1f495;&#xff09; 目录 二、CSS的复合选择器 2.1 什么是复合选择器 2.2 后代选择器(重要) 2.3 子选择器(重要) Questions 小提…

七年老测试整理的RF框架大全,一看就会,一学就懂

1.RF框架 全称robot framework,一个基于python开发的&#xff0c;自动化测试框架&#xff0c;这个框架可以做&#xff1a;web自动化&#xff0c;接口自动化&#xff0c;APP自动化。 github官网 1&#xff09;.安装python 检查python环境 python -V或 pip -V 2&#xff09;.…

【linux基础】bash脚本的学习:定义变量及引用变量、统计目标目录下所有文件行数、列数

假设目的&#xff1a;统计并输出指定文件夹下所有文件行数 单个文件可以用 wc -l &#xff1b;多个文件&#xff0c;可以用通配符 / 借助bash脚本 1.定义变量名&#xff0c;使用引号 a"bestqc.com.map" b"Anno.variant_function" c"enrichment/GOe…

UE4_导入内容_Alembic文件导入器

Alembic文件导入器 Alembic文件格式(.abc)是一个开放的计算机图形交换框架&#xff0c;它将复杂的动画化场景浓缩成一组非过程式的、与应用程序无关的烘焙几何结果。虚幻引擎4(UE4)允许你通过 Alembic导入器 导入你的Alembic文件&#xff0c;这让你可以在外部自由地创建复杂的…

android支付宝接入流程

接入前准备 接入APP支付能力前&#xff0c;开发者需要完成以下前置步骤。 本文档展示了如何从零开始&#xff0c;使用支付宝开放平台服务端 SDK 快速接入App支付产品&#xff0c;完成与支付宝对接的部分。 第一步&#xff1a;创建应用并获取APPID 要在您的应用中接入支付宝…

Hot100【十一】:编辑距离

// 定义dp[i][j]: 表示word1前i个字符转换到word2前j个字符最小操作数 // 初始化dp[m1][n1] class Solution {public int minDistance(String word1, String word2) {int m word1.length();int n word2.length();// 1. dp数组int[][] dp new int[m 1][n 1];// 2. dp数组初…

代码算法训练营day14 | 理论基础、递归遍历

day14&#xff1a; 理论基础二叉树的分类&#xff1a;二叉树的种类&#xff1a;满二叉树完全二叉树二叉搜索树平衡二叉搜索树 二叉树的存储方式&#xff1a;链式存储顺序存储 二叉树的遍历方式&#xff1a;深度优先和广度优先遍历实现方式 二叉树的定义&#xff1a; 递归遍历递…

【攻防世界】web2(逆向解密)

进入题目环境&#xff0c;查看页面信息&#xff1a; <?php $miwen"a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";function encode($str){$_ostrrev($str);// echo $_o;for($_00;$_0<strlen($_o);$_0){$_csubstr($_o,$_0,1);$__ord($_c)1;…

磁盘管理与文件管理

文章目录 一、磁盘结构二、MBR与磁盘分区分区的优势与缺点分区的方式文件系统分区工具挂载与解挂载 一、磁盘结构 1.硬盘结构 硬盘分类&#xff1a; 1.机械硬盘&#xff1a;靠磁头转动找数据 慢 便宜 2.固态硬盘&#xff1a;靠芯片去找数据 快 贵 硬盘的数据结构&#xff1a;…

重温OKHTTP源码

本文基于OkHttp4.12.0源码分析 官方地址 概括 本篇主要是对okhttp开源库的一个详细解析&#xff0c;包含详细的请求流程分析、各大拦截器的解读等。 使用方法 同步请求&#xff1a;创建一个OKHttpClient对象&#xff0c;一个Request对象&#xff0c;然后利用它们创建一个Ca…

动态代理

动态代理 动态代理和静态代理角色一致。 代理类是动态生成的&#xff0c;不是我们直接写好的。 动态代理分为俩大类&#xff1a;基于接口的动态代理、基于类的动态代理 基于接口&#xff1a;JDK动态代理&#xff08;以下示例就是这个&#xff09; 基于类&#xff1a;cglib jav…

微机原理——绪论

本篇文章是我在观看网课时记录的笔记。如有错误欢迎批评指正。 微机原理————绪论 我们在使用计算机时&#xff0c;最重要最核心的就是计算机的CPU(中央处理器)&#xff0c;决定了计算机的计算速度&#xff0c;但是CPU无法直接读取外界的温度、湿度、压力之类的物理量&…

MSTP/RSTP的保护功能

目录 原理概述 实验目的 实验内容 实验拓扑 1.配置RSTP/MSTP 2.配置BPDU保护 3.配置根保护 4.配置环路保护 5.配置TC-BPDU保护 原理概述 在RSTP或MSTP交换网络中&#xff0c;为了防止恶意攻击或临时环路的产生&#xff0c;可配置保护功能来增强网络的健壮性和安全性。…

VSCode配置AI自动补全插件Tabnine

面向软件开发人员的 AI 助手 使用 AI 代码完成更快地编写代码 什么是Tabnine Tabnine 是一款 AI 代码助手&#xff0c;可让您成为更好的开发人员。Tabnine 将通过所有最流行的编码语言和 IDE 的实时代码完成、聊天和代码生成来提高您的开发速度。 无论您将其称为 IntelliSens…

【网络捉鬼记】微信可以部分网页可以,其它网页打不开提示无法找到NDS地址

蹭网蹭得好好的&#xff0c;为啥突然这样呢&#xff1f; 发现微信可以&#xff0c;百度搜索网页可以打开但图片出不来&#xff0c;再点一个新闻进去又是上图的样子。 问AI&#xff01;却发现连质谱清言也打不开&#xff01;用自己热点问&#xff1a; 至于win10怎么更换DNS&…

免费幻兽帕鲁游戏云服务器领取及搭建教程

幻兽帕鲁是一款多人在线游戏&#xff0c;为了获得更好的游戏体验&#xff0c;许多玩家会选择自行搭建游戏联机服务器&#xff0c;但是游戏云服务器一般配置较高&#xff0c;价格自然也比较高&#xff0c;本文将为大家分享免费幻兽帕鲁游戏云服务器领取及搭建教程。 雨云是一家国…

16.事件标志组

一、简介 事件标志组与信号量一样属于任务间同步的机制&#xff0c;但是信号量一般用于任务间的单事件同 步&#xff0c;对于任务间的多事件同步&#xff0c;仅使用信号量就显得力不从心了。FreeRTOS 提供的事件标志组 可以很好的处理多事件情况下的任务同步。 1. 事件标志 …

C语言文件操作2

1.二进制读写函数 在上一章我们介绍了字符读写函数、文本读写函数和格式化输入输出函数&#xff0c;这张我们继续为大家介绍剩下的一组读写函数——二进制读写函数&#xff1a;fread函数和fwrite函数。 ⚀fread函数 &#x1f7e1;函数作用 以二进制的方式从指定流中读取数据 …