【HarmonyOS 鸿蒙实战开发】橡皮擦案例

218 篇文章 0 订阅
180 篇文章 0 订阅

往期知识点整理

介绍

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

效果图预览

使用说明

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

实现思路

  1. 使用NodeContainer构建绘制区域。
  • 定义NodeController的子类MyNodeController,实例化后可以通过将自绘制渲染节点RenderNode挂载到对应节点容器NodeContainer上实现自定义绘制。
   /**
    * NodeController的子类MyNodeController
    */
   export class MyNodeController extends NodeController {
     private rootNode: FrameNode | null = null; // 根节点
     rootRenderNode: RenderNode | null = null; // 从NodeController根节点获取的RenderNode,用于添加和删除新创建的MyRenderNode实例
   
     // 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 {
       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与图片内容区域重叠,控制绘制区域。
   @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)
  1. 使用MyImageRenderNode类创建一个节点作为绘图基本层,管理整个画布的绘制历史,记录每次绘制后的画布状态(pixelMap)。
  • 创建MyImageRenderNode类,定义属性pixelMapHistorycacheStack用于管理和记录画布上的图案变化,节点渲染时将pixelMapHistory栈顶的pixelMap绘制到画布上。
   /**
    * MyImageRenderNode类,绘制和记录画布图案的pixelMap
    */
   export class MyImageRenderNode extends RenderNode {
     pixelMapHistory: image.PixelMap[] = []; // 记录每次绘制后画布的pixelMap
     cacheStack: image.PixelMap[] = []; // 记录撤销时从pixelMapHistory中出栈的pixelMap,恢复时使用
   
     // RenderNode进行绘制时会调用draw方法
     draw(context: DrawContext): void {
       const canvas = context.canvas;
       if (this.pixelMapHistory.length !== 0) {
         // 使用drawImage绘制pixelMapHistory栈顶的pixelMap
         canvas.drawImage(this.pixelMapHistory[this.pixelMapHistory.length - 1], 0, 0);
       }
     }
   }
  • NodeContaineronAppear生命周期中初始化创建和挂载一个MyImageRenderNode节点currentImageNode,作为绘图的基础层。
   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);
       }
     })
  1. 创建MyRenderNode类来负责绘制路径,并定义其属性如路径对象、颜色混合模式和线宽以便动态修改。
   /**
    * 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);
       // 设置画笔的线宽,单位px
       this.pen.setStrokeWidth(this.lineStrokeWidth);
       // 将Pen画笔设置到canvas中
       canvas.attachPen(this.pen);
       // 绘制path
       canvas.drawPath(this.path);
     }
   }
  1. NodeContainer组件的onTouch回调函数中,处理手指按下、移动和抬起事件,以便在屏幕上绘制或擦除路径。
  • 手指按下时,如果是初次绘制,创建一个新的MyRenderNode节点currentNodeDraw并将其挂载到根节点上,否则在currentNodeDraw中重新添加路径,根据当前的选择状态(绘制或擦除)修改节点中画笔的blendMode,控制画笔涂鸦和擦除。
   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节点中的路径对象,并刷新节点。

   /**
    * 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重新渲染currentImageNode
           this.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();
           }
         }
       })
   }
  1. 通过操作currentImageNode节点的属性pixelMapHistorycacheStack中画布状态(pixelMap)的出入栈实现撤销和恢复功能。
  • 从历史记录栈pixelMapHistory中移除最近一次绘制的pixelMap,刷新currentImageNode节点实现撤销功能,移除的pixelMap放入缓存栈cacheStack中以备恢复时使用。
   /**
    * 撤销上一笔绘制
    */
   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节点恢复上次撤销之前的状态。
   /**
    * 恢复上一次撤销
    */
   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)全栈开发技术的学习路线,包含了鸿蒙开发必掌握的核心知识要点,内容有(OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、OpenHarmony驱动开发、系统定制移植……等)技术知识点。

《鸿蒙 (Harmony OS)开发学习手册》(共计892页):https://gitcode.com/HarmonyOS_MN/733GH/overview

如何快速入门?

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

开发基础知识:

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.……

在这里插入图片描述

鸿蒙开发面试真题(含参考答案):https://gitcode.com/HarmonyOS_MN/733GH/overview

在这里插入图片描述

OpenHarmony 开发环境搭建

图片

《OpenHarmony源码解析》:https://gitcode.com/HarmonyOS_MN/733GH/overview

  • 搭建开发环境
  • Windows 开发环境的搭建
  • Ubuntu 开发环境搭建
  • Linux 与 Windows 之间的文件共享
  • ……
  • 系统架构分析
  • 构建子系统
  • 启动流程
  • 子系统
  • 分布式任务调度子系统
  • 分布式通信子系统
  • 驱动子系统
  • ……

图片

OpenHarmony 设备开发学习手册:https://gitcode.com/HarmonyOS_MN/733GH/overview

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值