HarmonyOS开发实战( Beta5.0)橡皮擦案例实践详解

devtools/2024/11/15 1:58:50/

鸿蒙HarmonyOS开发往期必看:

HarmonyOS NEXT应用开发性能实践总结

最新版!“非常详细的” 鸿蒙HarmonyOS Next应用开发学习路线!(从零基础入门到精通)


介绍

本示例通过@ohos.graphics.drawing库和blendMode颜色混合实现了橡皮擦功能,能够根据手指移动轨迹擦除之前绘制的内容,并且可以进行图案的撤销和恢复。

效果图预览

使用说明

  1. 页面底部左侧展示涂鸦和橡皮擦按钮,点击可以切换选中状态和当前的绘制模式,右侧为线宽列表,点击可以修改绘制时的轨迹宽度。
  2. 在图片上触摸并拖动手指,可以绘制路径,涂鸦模式时绘制橙色线条,橡皮擦模式时擦除线条。
  3. 页面顶部按钮默认不可用,进行绘制操作后左侧撤销按钮高亮,点击可以撤销上一步绘制,撤销后未进行绘制时右侧恢复按钮高亮,点击可以恢复上一次撤销。

实现思路

  1. 使用NodeContainer构建绘制区域。

    • 定义NodeController的子类MyNodeController,实例化后可以通过将自绘制渲染节点RenderNode挂载到对应节点容器NodeContainer上实现自定义绘制。源码参考RenderNodeModel.ets
    /*** NodeController的子类MyNodeController*/
    export class MyNodeController extends NodeController {private rootNode: FrameNode | null = null; // 根节点rootRenderNode: RenderNode | null = null; // 从NodeController根节点获取的RenderNode,用于添加和删除新创建的MyRenderNode实例// 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 {if (this.rootRenderNode !== null) {// NodeContainer布局完成后设置rootRenderNode的背景透明this.rootRenderNode.backgroundColor = 0X00000000;// rootRenderNode的位置从组件NodeContainer的左上角(0,0)坐标开始,大小为NodeContainer的宽高this.rootRenderNode.frame = {x: 0,y: 0,width: size.width,height: size.height};}}// 添加节点addNode(node: RenderNode): void {if (this.rootNode === null) {return;}if (this.rootRenderNode !== null) {this.rootRenderNode.appendChild(node);}}// 清空节点clearNodes(): void {if (this.rootNode === null) {return;}if (this.rootRenderNode !== null) {this.rootRenderNode.clearChildren();}}
    }
    
    • 创建自定义节点容器组件NodeContainer,接收MyNodeController的实例,组件的宽高为图片加载完成后实际内容区域的宽高,并通过相对容器布局的alignRules使NodeContainer与图片内容区域重叠,控制绘制区域。源码参考EraserMainPage.ets
    @Builder
    drawingArea() {Image($r('app.media.palette_picture')).width($r('app.string.palette_full_size')).objectFit(ImageFit.Contain).alignRules({top: { anchor: Constants.TOP_BUTTON_LINE_ID, align: VerticalAlign.Bottom },middle: { anchor: Constants.CONTAINER_ID, align: HorizontalAlign.Center },bottom: { anchor: Constants.BOTTOM_PEN_SHAPE_ID, align: VerticalAlign.Top }}).onComplete((event) => {if (event !== undefined) {// NodeContainer的宽高设置为图片成功加载后实际绘制的尺寸this.nodeContainerWidth = px2vp(event.contentWidth);this.nodeContainerHeight = px2vp(event.contentHeight);}})NodeContainer(this.myNodeController).width(this.nodeContainerWidth).height(this.nodeContainerHeight).alignRules({top: { anchor: Constants.TOP_BUTTON_LINE_ID, align: VerticalAlign.Bottom },middle: { anchor: Constants.CONTAINER_ID, align: HorizontalAlign.Center },bottom: { anchor: Constants.BOTTOM_PEN_SHAPE_ID, align: VerticalAlign.Top }}).id(Constants.NODE_CONTAINER_ID)// ...
    }
    
    • NodeContainer设置属性blendMode创建一个离屏画布,NodeContainer的子节点进行颜色混合时将基于该画布进行混合。源码参考EraserMainPage.ets
    .blendMode(BlendMode.SRC_OVER, BlendApplyType.OFFSCREEN)
    
  2. 使用MyImageRenderNode类创建一个节点作为绘图基本层,管理整个画布的绘制历史,记录每次绘制后的画布状态(pixelMap)。

    • 创建MyImageRenderNode类,定义属性pixelMapHistorycacheStack用于管理和记录画布上的图案变化,节点渲染时将pixelMapHistory栈顶的pixelMap绘制到画布上。源码参考RenderNodeModel.ets
    /*** MyImageRenderNode类,绘制和记录画布图案的pixelMap*/
    export class MyImageRenderNode extends RenderNode {pixelMapHistory: image.PixelMap[] = []; // 记录每次绘制后画布的pixelMapcacheStack: image.PixelMap[] = []; // 记录撤销时从pixelMapHistory中出栈的pixelMap,恢复时使用// RenderNode进行绘制时会调用draw方法draw(context: DrawContext): void {const canvas = context.canvas;if (this.pixelMapHistory.length !== 0) {// 使用drawImage绘制pixelMapHistory栈顶的pixelMapcanvas.drawImage(this.pixelMapHistory[this.pixelMapHistory.length - 1], 0, 0);}}
    }
    
    • NodeContaineronAppear生命周期中初始化创建和挂载一个MyImageRenderNode节点currentImageNode,作为绘图的基础层。源码参考EraserMainPage.ets
    NodeContainer(this.myNodeController)// ....onAppear(() => {// NodeContainer组件挂载完成后初始化一个MyImageRenderNode节点添加到根节点上if (this.currentImageNode === null) {// 创建一个MyImageRenderNode对象const newNode = new MyImageRenderNode();// 定义newNode的大小和位置,位置从组件NodeContainer的左上角(0,0)坐标开始,大小为NodeContainer的宽高newNode.frame = {x: 0,y: 0,width: this.nodeContainerWidth,height: this.nodeContainerHeight};this.currentImageNode = newNode;this.myNodeController.addNode(this.currentImageNode);}})
    
  3. 创建MyRenderNode类来负责绘制路径,并定义其属性如路径对象、颜色混合模式和线宽以便动态修改。源码参考RenderNodeModel.ets

    /*** MyRenderNode类,初始化画笔和绘制路径*/
    export class MyRenderNode extends RenderNode {path: drawing.Path = new drawing.Path(); // 新建路径对象,用于绘制手指移动轨迹pen: drawing.Pen = new drawing.Pen(); // 创建一个画笔Pen对象,Pen对象用于形状的边框线绘制blendMode: drawing.BlendMode = drawing.BlendMode.SRC_OVER; // 画笔的颜色混合模式lineStrokeWidth: number = 0; // 画笔线宽constructor() {super();// 设置画笔颜色const pen_color: common2D.Color = {alpha: 0xFF,red: 0xFA,green: 0x64,blue: 0x00};this.pen.setColor(pen_color);// 设置画笔开启反走样,可以使得图形的边缘在显示时更平滑this.pen.setAntiAlias(true);// 开启画笔的抖动绘制效果。抖动绘制可以使得绘制出的颜色更加真实。this.pen.setDither(true);// 设置画笔绘制转角的样式为圆头this.pen.setJoinStyle(drawing.JoinStyle.ROUND_JOIN);// 设置画笔线帽的样式,即画笔在绘制线段时在线段头尾端点的样式为半圆弧this.pen.setCapStyle(drawing.CapStyle.ROUND_CAP);}// RenderNode进行绘制时会调用draw方法draw(context: DrawContext): void {const canvas = context.canvas;// 设置画笔的颜色混合模式,根据不同的混合模式实现涂鸦和擦除效果this.pen.setBlendMode(this.blendMode);// 设置画笔的线宽,单位pxthis.pen.setStrokeWidth(this.lineStrokeWidth);// 将Pen画笔设置到canvas中canvas.attachPen(this.pen);// 绘制pathcanvas.drawPath(this.path);}
    }
    
  4. NodeContainer组件的onTouch回调函数中,处理手指按下、移动和抬起事件,以便在屏幕上绘制或擦除路径。

    • 手指按下时,如果是初次绘制,创建一个新的MyRenderNode节点currentNodeDraw并将其挂载到根节点上,否则在currentNodeDraw中重新添加路径,根据当前的选择状态(绘制或擦除)修改节点中画笔的blendMode,控制画笔涂鸦和擦除。源码参考EraserMainPage.ets
    case TouchType.Down: {// 初次绘制时创建一个新的MyRenderNode对象,用于记录和绘制手指移动的路径,后续绘制时在已创建的currentNodeDraw中重新添加路径let newNode: MyRenderNode;if (this.currentNodeDraw !== null) {this.currentNodeDraw.path.moveTo(positionX, positionY);} else {const newNode = new MyRenderNode();newNode.frame = {x: 0,y: 0,width: this.nodeContainerWidth,height: this.nodeContainerHeight};this.currentNodeDraw = newNode;this.currentNodeDraw.path.moveTo(positionX, positionY);this.myNodeController.addNode(this.currentNodeDraw);}// TODO:知识点:给画笔设置不同的颜色混合模式,实现涂鸦和擦除效果if (!this.isClear) {// SRC_OVER类型,将源像素(新绘制内容)按照透明度与目标像素(下层图像)进行混合,覆盖在目标像素(下层图像)上this.currentNodeDraw.blendMode = drawing.BlendMode.SRC_OVER;} else {// CLEAR类型,将源像素(新绘制内容)覆盖的目标像素(下层图像)清除为完全透明this.currentNodeDraw.blendMode = drawing.BlendMode.CLEAR;}// 修改画笔线宽this.currentNodeDraw.lineStrokeWidth = this.currentLineStrokeWidth;break;
    }
    
    • 手指移动时,更新currentNodeDraw中的路径对象,并触发节点的重新渲染,绘制或擦除对应的移动轨迹。源码参考EraserMainPage.ets
    case TouchType.Move: {if (this.currentNodeDraw !== null) {// 手指移动,绘制移动轨迹this.currentNodeDraw.path.lineTo(positionX, positionY);// 节点的path更新后需要调用invalidate()方法触发重新渲染this.currentNodeDraw.invalidate();}break;
    }
    
    • 手指抬起时,通过组件截图功能获取当前NodeContainer上绘制结果的pixelMap,将其存入currentImageNode节点的历史记录栈pixelMapHistory中,并重新渲染currentImageNode节点。然后重置currentNodeDraw节点中的路径对象,并刷新节点。源码参考EraserMainPage.ets
    /*** touch事件触发后绘制手指移动轨迹*/
    onTouchEvent(event: TouchEvent): void {// 获取手指触摸位置的坐标点const positionX: number = vp2px(event.touches[0].x);const positionY: number = vp2px(event.touches[0].y);switch (event.type) {// ...case TouchType.Up: {// 之前没有绘制过,即pixelMapHistory长度为0时,擦除操作不会更新绘制结果if (this.isClear && this.currentImageNode?.pixelMapHistory.length === 0 && this.currentNodeDraw !== null) {// 重置绘制节点的路径,this.currentNodeDraw.path.reset();this.currentNodeDraw.invalidate();return;}// 手指离开时更新绘制结果this.updateDrawResult();}default: {break;}}
    }/*** 更新绘制结果*/
    updateDrawResult() {// TODO:知识点:通过组件截图componentSnapshot获取NodeContainer上当前绘制结果的pixelMap,需要设置waitUntilRenderFinished为true尽可能获取最新的渲染结果componentSnapshot.get(Constants.NODE_CONTAINER_ID, { waitUntilRenderFinished: true }).then(async (pixelMap: image.PixelMap) => {if (this.currentImageNode !== null) {// 获取到的pixelMap推入pixelMapHistory栈中,并且调用invalidate重新渲染currentImageNodethis.currentImageNode.pixelMapHistory.push(pixelMap);this.currentImageNode.invalidate();// 更新绘制结果后将用于恢复的栈清空this.currentImageNode.cacheStack = [];// 更新撤销和恢复按钮状态this.redoEnabled = false;this.undoEnabled = true;if (this.currentNodeDraw !== null) {// 重置绘制节点的路径,this.currentNodeDraw.path.reset();this.currentNodeDraw.invalidate();}}})
    }
    
  5. 通过操作currentImageNode节点的属性pixelMapHistorycacheStack中画布状态(pixelMap)的出入栈实现撤销和恢复功能。

    • 从历史记录栈pixelMapHistory中移除最近一次绘制的pixelMap,刷新currentImageNode节点实现撤销功能,移除的pixelMap放入缓存栈cacheStack中以备恢复时使用。源码参考EraserMainPage.ets
    /*** 撤销上一笔绘制*/
    undo() {if (this.currentImageNode !== null) {// 绘制历史记录pixelMapHistory顶部的pixelMap出栈,推入cacheStack栈中const pixelMap = this.currentImageNode.pixelMapHistory.pop();if (pixelMap) {this.currentImageNode.cacheStack.push(pixelMap);}// 节点重新渲染,将此时pixelMapHistory栈顶的pixelMap绘制到画布上this.currentImageNode.invalidate();// 更新撤销和恢复按钮状态this.redoEnabled = this.currentImageNode.cacheStack.length !== 0 ? true : false;this.undoEnabled = this.currentImageNode.pixelMapHistory.length !== 0 ? true : false;}
    }
    
    • 从缓存栈cacheStack中取出栈顶的pixelMap,重新放入历史记录栈pixelMapHistory中,刷新currentImageNode节点恢复上次撤销之前的状态。源码参考EraserMainPage.ets
    /*** 恢复上一次撤销*/
    redo() {if (this.currentImageNode !== null) {// cacheStack顶部的pixelMap出栈,推入绘制历史记录pixelMapHistory栈中const pixelMap = this.currentImageNode.cacheStack.pop();if (pixelMap) {this.currentImageNode.pixelMapHistory.push(pixelMap);}// 节点重新渲染,将此时pixelMapHistory栈顶的pixelMap绘制到画布上this.currentImageNode.invalidate();// 更新撤销和恢复按钮状态this.redoEnabled = this.currentImageNode.cacheStack.length !== 0 ? true : false;this.undoEnabled = this.currentImageNode.pixelMapHistory.length !== 0 ? true : false;}
    }
    

高性能知识点

  1. onTouch是系统高频回调函数,避免在函数中进行冗余或耗时操作,例如应该减少或避免在函数打印日志,会有较大的性能损耗。

工程结构&模块类型

eraser                                        // har类型
|---model                        
|   |---RenderNodeModel.ets                   // 数据模型层-节点数据模型
|---pages                        
|   |---EraserMainPage.ets                    // 视图层-主页面
|---constants                        
|   |---Constants.ets                         // 常量数据

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为体系杂乱无章,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)路线、视频、文档用来跟着学习是非常有必要的。

如果你是一名有经验的资深Android移动开发、Java开发、前端开发、对鸿蒙感兴趣以及转行人员

鸿蒙 NEXT 全栈开发学习笔记 希望这一份鸿蒙学习文档能够给大家带来帮助~


 鸿蒙(HarmonyOS NEXT)最新学习路线

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频教程+学习PDF文档

鸿蒙语法ArkTS、TypeScript、ArkUI教程……)

 纯血版鸿蒙全套学习文档(面试、文档、全套视频等)

                   

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线


http://www.ppmy.cn/devtools/114952.html

相关文章

小阿轩yx-通过state模块定义主机状态

小阿轩yx-通过state模块定义主机状态 前言 前面学习了远程执行模块,这些模块的执行类似语段 shell 脚本,每次执行都会触发一次相同的功能,在大量的 minion 上运行远程命令当然是重要的,但是对于 minion 的环境控制,使…

【Linux庖丁解牛】—Linux基本指令(上)!

🌈个人主页:秋风起,再归来~🔥系列专栏: Linux庖丁解牛 🔖克心守己,律己则安 目录 1、 pwd命令 2、ls 指令 3、cd 指令 4、Linux下的根目录 5、touch指令 6、 stat指令 7、mkdi…

Linux系统中的进程调度队列

目录 一、进程调度队列结构 二、活动队列与过期队列 1.queue[140] 2.bitmap[5] 一、进程调度队列结构 Linux系统中,每一个CPU都有一个进程调度队列runqueue,如图所示 二、活动队列与过期队列 运行队列runqueue中有两个指针*active、*expired。*acti…

堆的向下调整算法和TOPK问题

目录 1.什么是堆? 1.1 向下调整建堆的时间复杂度计算 1.2 堆的结构体设计 2.堆的功能实现: 2.1 堆的插入: 2.2 堆的删除: 2.3 堆排序: 2.4 向下调整建堆: 2.5 TOPK问题: 2.6 向上调整算…

浅析OceanBase数据库的向量化执行引擎

本篇博客是偏数据库系统概念性的内容,不会深入到 OceanBase 中各个算子和表达式的在向量化中的详细设计和实现。 背景 为了提升OceanBase社区版用户解决问题的效率,OceanBase官方不久前推出了《OceanBase 从入门到实践》系列课程。在第七期直播课程后&a…

visual studio2015安装番茄助手

VS2015安装使用番茄助手Visual Assist_vs2015番茄助手-CSDN博客 【VS和番茄助手的安装步骤】https://www.bilibili.com/video/BV1K24y1n7Xk?vd_sourcefad0750b8c666dbeaf016e547f99a602 【番茄助手VS2019安装】https://www.bilibili.com/video/BV13p4y1v7bG?vd_sourcefad0…

在Flask中实现日志记录

在Flask中实现日志记录是一个关键的功能,它有助于监控应用的运行情况、调试问题以及记录重要的运行信息。以下是在Flask中实现日志记录的详细步骤和最佳实践: 一、使用Python内置的logging模块 Flask应用通常会使用Python的logging模块来进行日志记录。…

NCNN 学习(2)-Mat

Mat 是 NCNN 中最重要数据结构之一,NCNN 的很多计算都会涉及到 Mat。 1 数据成员 Mat 的定义在 https://github.com/Tencent/ncnn/blob/master/src/mat.h。从代码中可以看到,Mat 有这样的几个主要数据成员: class NCNN_EXPORT Mat { publi…