【HarmonyOS】自定义节点能力

文章目录

一、自定义能力

1、自定义能力分层

自定义能力是ArkUI开发框架提供的对UI界面进行开发和定制化的能力。ArkUI开发框架提供的不同控制层级的自定义能力用于实现不同场景的应用的开发。

当前开放的自定义能力由低到高包括以下几个层次:

  • 自定义组合:ArkUI框架提供的最基础的自定义方式,通过系统组件和自定义组件的基础能力,将这些能力进行组合,复用已有组件,进一步封装新的组件。具体包括:封装、布局、绘制和动画等基础能力。
  • 自定义扩展:ArkUI框架提供一系列基于Modifier的自定义扩展能力,通过与UI分离的方式,对已有UI组件的属性、手势、内容进行扩展修改。包括AttributeModifier、GestureModifier、DrawModifier等扩展能力。
  • 自定义节点:具备底层实体节点的部分基础能力的节点对象(FrameNode、RenderNode、BuilderNode),这些节点能够通过自定义占位节点与系统组件进行混合显示。自定义节点可以具备单个节点的测算布局、设置基础属性、设置事件监听、自定义绘制渲染内容的自定义能力。
  • 自定义渲染:通过XComponent的"surface"模式暴露出的NativeWindow,使用NDK接口,可以将EGL/OpenGLES生成的显示数据或其它方式解码生成的媒体流数据写入到NativeWindow中,可以实现渲染内容的自定义。

二、自定义节点

1、概念

自定义节点是ArkUI通过接口提供的底层实体节点,具备部分基础能力,能够与系统组件混合显示。**自定义节点的挂载与显示依赖于自定义占位节点。**当前,自定义节点涵盖三类对象:FrameNode、RenderNode和BuilderNode。FrameNode表示单个自定义组件节点,RenderNode表示为更轻量级的渲染节点,而BuilderNode对象赋予了创建、更新系统组件及其组件树的能力。

  • 系统组件:由ArkUI直接提供的称为系统组件。系统组件的创建、更新、销毁等生命周期由 ArkUI 框架自动管理,开发者无需手动操作。是框架为开发者提供的高级抽象,隐藏了底层复杂的渲染和布局细节。
  • 实体节点:系统内部维护了一棵组件树,用于处理组件的属性设置、生命周期等逻辑。该组件树上的节点即为实体节点 。是 ArkUI 框架在内部 维护的、构成 UI 渲染树的基本单元。它们是系统组件在渲染引擎中的具体表现。可以理解为:系统组件是 "设计图",实体节点是 "根据设计图建造出来的实际建筑"
  • 自定义节点:使用ArkUI提供的接口,以命令式创建的节点。包括自定义组件节点(FrameNode)、自定义渲染节点(RenderNode)、自定义声明式节点(BuilderNode)、ComponentContent等。

1.1 自定义占位节点

自定义占位节点作为系统组件可以在组件树上为自定义节点提供挂载的点,核心作用是在系统组件树上 "预留位置",让自定义节点能挂到这个位置上,实现 "系统组件和自定义节点混排"。

自定义占位节点本身不负责渲染,只做 "连接"。

1.2 自定义组件节点 (FrameNode)

FrameNode表示组件的实体节点,是功能最全的 "万能节点"。既可以自己 "从零构建" 组件,也可以 "代理" 系统组件,是连接自定义逻辑和系统能力的核心。

具体可以分为两大类能力:完全自定义节点的能力以及系统组件节点代理的能力。

类型 核心能力 创建方式 适用场景
完全自定义节点 自定义测量(计算尺寸)、布局(子节点排列)、绘制(画自定义图形);支持动态增删子节点、设置样式 / 事件。 1. new FrameNode(uiContext);2. typeNode.createNode('Text')/'Column' 不依赖系统组件,需要灵活控制 UI 逻辑(比如动态生成列表、自定义交互组件)。
系统组件代理节点 遍历系统组件树(查父子 / 兄弟节点)、访问系统组件信息(比如 Button 的文本)、给系统组件加额外事件监听。 通过getChild/getParent/getAttachedFrameNodeById等查询接口获取 无需修改系统组件源码,实现 "无感监听"(比如打点统计、广告 SDK 植入、性能监控)。

1.3 自定义渲染节点 (RenderNode)

RenderNode作为轻量级的渲染节点,仅提供了设置渲染相关属性、自定义绘制内容以及节点操作的能力,不包含复杂的布局、组件代理能力,主打 "轻量化、高性能绘制"。适用于仅依赖系统渲染与动画能力的自定义场景。

  • 核心能力:设置渲染属性(比如背景色、透明度)、自定义绘制(通过 Canvas 画图形 / 文字)、基础节点操作(增删子节点)。
  • 能力边界:没有 "测量 / 布局" 的自定义能力,布局逻辑依赖父节点或系统默认规则;不能代理系统组件。
  • 适用场景:只需要 "画点东西",不需要复杂交互或布局的场景(比如自定义图标、动态绘制图表、简单动画效果)。
  • 优势:比 FrameNode 更轻量,渲染性能更高,适合高频刷新的场景(比如实时折线图)。

1.4 自定义声明式节点 (BuilderNode)

定位 :用声明式语法(@Builder)定义 UI 片段,再封装成命令式可操作的节点,兼具 "声明式的简洁" 和 "命令式的灵活"。

特点 & 价值

  1. 预创建优势:可以手动控制节点的创建时机(比如在页面初始化时预创建,需要时直接显示),避免频繁创建销毁带来的性能损耗。
  2. 节点复用:持有实体节点对象,同一个 BuilderNode 可以被多个占位节点挂载(或在不同位置切换),实现 "一处定义、多处使用"。(迁移复用:BuilderNode 先挂载到 A 占位节点,需要时 "下树",再挂载到 B 占位节点,全程复用同一个实体节点,避免重复创建销毁的性能损耗,同时避免两个占位节点的样式(如透明度、位置)冲突,会导致显示异常)。
  3. 混合显示:内部是系统组件(比如ColumnButton),外部可以和 FrameNode、RenderNode 等自定义节点混合显示。
  4. 创建方式:先通过@Builder定义 UI 片段,再用new BuilderNode(uiContext)创建节点,最后通过build()方法绑定 UI 片段和参数。

适用场景:需要复用固定 UI 结构(比如通用弹窗、列表项模板),且需要灵活控制显示位置 / 时机的场景。

2、自定义占位节点

ArkUI提供了系统组件NodeContainer和ContentSlot作为自定义节点的占位节点。主要用于自定义节点以及自定义节点树的显示。

维度 NodeContainer(容器组件) ContentSlot(语法节点)
本质 具备 UI 属性的系统组件(属于 UI 节点) 无 UI 属性的语法标记(不属于 UI 节点)
核心能力 1. 支持通用属性(如宽高、背景色、对齐方式);2. 参与布局和渲染(默认左上角对齐,类似 Stack);3. 通过 NodeController 动态控制节点上树 / 下树 1. 无通用属性(不能设宽高、背景);2. 不参与布局和渲染(仅提供挂载点);3. 直接接收 NodeContent 等自定义内容容器
依赖关系 必须与 NodeController 组合使用(通过 NodeController 获取要挂载的节点树) 无需控制器,直接对接 NodeContent 等内容载体
适用场景 需要控制自定义节点的布局、样式,或监听其生命周期(如尺寸变化、触摸事件)的场景 仅需简单挂载自定义节点集合,无需额外样式 / 生命周期控制的场景

NodeContainer是用来占位的系统组件,主要用于自定义节点以及自定义节点树的显示,支持组件的通用属性,对通用属性的处理请参考默认左上角对齐的Stack组件。

NodeController提供了一系列生命周期回调,通过makeNode回调返回一个FrameNode节点树的根节点。将FrameNode节点树挂载到对应的NodeContainer下。同时提供了aboutToAppear、aboutToDisappear、aboutToResize、onTouchEvent、rebuild五个回调方法用于监听对应的NodeContainer的状态。

  • NodeContainer下仅支持挂载自定义的FrameNode节点以及BuilderNode创建的组件树的根节点。
  • 需要保证一个节点只能作为一个父节点的子节点去使用,否则可能会出现 "节点仅在一个容器显示""多个容器属性更新冲突""路由 / 动效场景崩溃" 等问题。例如,如果通过NodeController将同一个节点挂载在多个NodeContainer上,仅一个占位容器下会显示节点,且多个NodeContainer的可见性、透明度等影响子组件状态的属性更新均会影响被挂载的子节点。所以切换节点挂载容器时,先通过toHide()(或移除节点)让节点从原父容器 "下树",再通过toShow()(或添加节点)让节点在新父容器 "上树"。
  • 自定义节点:使用ArkUI提供的接口,以命令式创建的节点。包括自定义组件节点(FrameNode)、自定义渲染节点(RenderNode)、自定义声明式节点(BuilderNode)、ComponentContent等。
  • 自定义节点树:根节点为自定义节点的节点树。可以作为一个 "整体" 被嵌入到声明式节点树中(如通过NodeContainerContentSlot组件)。
  • 声明式节点树:根节点为声明式节点的节点树。通过声明式语法描述 UI 结构(如Column() { Button() }),框架自动解析并生成节点树。UI 更新依赖状态变量(如@State),状态变化时框架自动刷新对应的节点。

自定义节点是通过命令式代码手动创建 的节点,而非声明式 UI(如ColumnButton)那样通过布局描述自动生成。必须调用 ArkUI 提供的接口(如new FrameNode()typeNode.createNode()new BuilderNode())手动实例化。支持命令式操作,比如appendChild(添加子节点)、removeChild(删除子节点)、setBackgroundColor(修改样式)等。

  • 节点树:一种常见的数据结构,用于表示节点的层级关系。
  • 占位节点:用于在声明式节点树上为自定义节点树预留位置的节点,主要包括NodeContainer和ContentSlot。鉴于页面的主树采用声明式节点树,因此,唯有借助占位节点,才能将命令式构建的自定义节点成功挂载至声明式节点树上。
typescript 复制代码
// method one    (common.ets 略)
let frameNode = getOrCreateNode(uiContext)?.getFrameNode();

// method two
let frameNode: FrameNode | null = new FrameNode(uiContext);
let node = typeNode.createNode(uiContext, "Text");
node.initialize("ControllerText:" + this.textNode.length).fontSize(20);
this.textNode.push(node);
frameNode?.appendChild(node);

// 示例代码中这两步都是创建自定义节点

3、自定义组件节点 (FrameNode)

对于拥有自定义前端的第三方框架(如JSON、XML、DOM树等),需将特定的DSL转换为ArkUI的声明式描述。如下图描述了JSON定义的前端框架和ArkUI声明式描述的对应关系。

上述转换过程需要依赖额外的数据驱动,绑定至Builder中,较为复杂且性能欠佳。这类框架通常依赖于ArkUI的布局、事件处理、基础的节点操作和自定义能力。大部分组件通过自定义实现,但需结合使用部分系统组件以实现混合显示,如下图示例既使用了FrameNode的自定义方法进行绘制,又使用了系统组件Column及其子组件Text,通过BuilderNode的方式将其挂载到根节点的FrameNode上混合显示。

  • 蓝色圆圈 :系统组件(如ColumnText,由 ArkUI 直接提供)
  • 橙色圆圈 :自定义节点(如Custom FrameNode,开发者通过命令式 API 创建)
  • 橙色矩形 :自定义节点对象(如FrameNodeRenderNode等实例,持有实体节点引用)
  • 实线箭头 :对象间的强引用关系(被引用对象不会被垃圾回收)
  • 虚线箭头 :对象间的弱引用关系(被引用对象可能被回收,不影响生命周期)
  1. @Builder buildText:声明式 UI 片段,后续被BuilderNode封装为可复用的自定义节点。
  2. MyFrame:自定义渲染节点(继承FrameNode),重写onDraw方法实现自定义绘制
  3. TextNodeController:节点控制器(管理自定义节点),创建根容器FrameNode,添加自定义绘制节点MyFrame和声明式节点BuilderNode
  • 引用关系
    • NodeContainer ←(弱引用)→ FrameNode :当自定义节点被销毁时,NodeContainer不会阻止其回收。
    • Custom FrameNode ↔ 自定义节点对象 :管理组件布局、子节点等完整组件能力,负责自定义绘制逻辑(对应onDraw方法)。
    • BuilderNode的引用链BuilderNode封装了声明式 UI 片段(buildText),关联关系:BuilderNodeFrameNodeRenderNode,最终映射到系统组件Text(通过实体节点关联)。

FrameNode表示组件树中的实体节点,与自定义占位容器组件NodeContainer相配合,实现在占位容器内构建一棵自定义的节点树。该节点树支持动态操作,如节点的增加、修改和删除。基础的FrameNode具备设置通用属性和事件回调的功能,同时提供完整的自定义能力,涵盖自定义测量、布局和绘制等方面。

3.1 创建和删除节点

FrameNode提供了节点创建和删除的能力。可以通过FrameNode的构造函数创建自定义FrameNode节点,通过构造函数创建的节点对应一个实体的节点。同时,可以通过FrameNode中的dispose接口来实现与实体节点的绑定关系的解除。

  • 在创建FrameNode对象的时候需要传入必选参数UIContext,若未传入UIContext对象或者传入不合法,则节点创建抛出异常。
  • 自定义占位组件将节点进行显示的时候需要保证UI上下文一致,否则会出现显示异常。
  • 若不持有FrameNode对象,则该对象会在GC的时候被回收。
typescript 复制代码
  disposeFrameNode() {
    if (this.rootNode !== null && this.builderNode !== null) {
      // 解除rootNode对实体FrameNode节点的引用关系前,移除rootNode的所有子节点
      this.rootNode.removeChild(this.builderNode.getFrameNode());
      // 解除builderNode对实体FrameNode节点的引用关系
      this.builderNode.dispose();//
      // 解除rootNode对实体FrameNode节点的引用关系
      this.rootNode.dispose();
    }
  }

3.2 获取对应的RenderNode节点

FrameNode提供了getRenderNode接口,用于获取FrameNode中的RenderNode。可以通过对获取到的RenderNode对象进行操作,动态修改FrameNode上绘制相关的属性,具体可修改的属性参考RenderNode的接口。

3.3 操作节点树

FrameNode提供了节点的增、删、查、改的能力,能够修改非代理节点的子树结构。可以对所有FrameNode的节点的父子节点做出查询操作,并返回查询结果。

  1. 节点可修改性
    • 自定义FrameNodethis.frameNode):isModifiable()返回true,支持增删改
      • 如果节点是开发者通过命令式 API 手动创建的,开发者拥有完整所有权,可自由修改
      • 如果节点是框架自动创建的(如系统组件、BuilderNode 内部节点),框架保留所有权,不允许开发者修改
    • BuilderNode的代理节点:由声明式 UI 生成,isModifiable()返回false,操作会抛异常
    • 系统组件节点(如NodeContainer):不可修改,操作会抛异常
  2. 节点父子关系规则
    • 一个节点只能有一个父节点,添加已有父节点的节点会失败
    • 代理节点(BuilderNode内部节点、系统组件节点)通常不允许作为子节点添加
  3. 节点查询 API
    • getNodeType():返回节点类型(如FrameNodeBuilderNode对应的类型)
    • getParent()/getChildrenCount()/getFirstChild()等:查询节点树结构关系

3.4 自定义测量布局与绘制

通过重写onDraw方法,可以自定义FrameNode的绘制内容。invalidate接口可以主动触发节点的重新绘制。

通过重写onMeasure可以自定义FrameNode的测量方式,使用measure可以主动传递布局约束触发重新测量。

通过重写onLayout方法可以自定义FrameNode的布局方式,使用layout方法可以主动传递位置信息并触发重新布局。

setNeedsLayout可以将当前节点标记,在下一帧触发重新布局。

4、自定义渲染节点 (RenderNode)

对于不具备自己的渲染环境的三方框架,尽管已实现前端解析、布局及事件处理等功能,但仍需依赖系统的基础渲染和动画能力。FrameNode上的通用属性与通用事件对这类框架而言是冗余的,会导致多次不必要的操作,涵盖布局、事件处理等逻辑。

自定义渲染节点 (RenderNode)是更加轻量的渲染节点,仅具备与渲染相关的功能。它提供了设置基础渲染属性的能力,以及节点的动态添加、删除和自定义绘制的能力。RenderNode能够为第三方框架提供基础的渲染和动画支持。

4.1 创建和删除节点

RenderNode提供了节点创建和删除的能力。可以通过RenderNode的构造函数创建自定义的RenderNode节点。通过构造函数创建的节点对应一个实体的节点。同时,可以通过RenderNode中的dispose接口来实现与实体节点的绑定关系的解除。

4.2 操作节点树

RenderNode提供了节点的增、删、查、改的能力,能够修改节点的子树结构;可以对所有RenderNode的节点的父子节点做出查询操作,并返回查询结果。

5、自定义声明式节点 (BuilderNode)

自定义声明式节点 (BuilderNode)提供能够挂载系统组件的能力,支持采用无状态的UI方式,通过全局自定义构建函数@Builder定制组件树。组件树的根FrameNode节点可通过getFrameNode获取,该节点既可直接由NodeController返回并挂载于NodeContainer节点下,亦可在FrameNode树与RenderNode树中嵌入声明式组件,实现混合显示。同时,BuilderNode具备纹理导出功能,导出的纹理可在XComponent中实现同层渲染。

不建议对BuilderNode中的RenderNode进行修改操作。BuilderNode中持有的FrameNode仅用于将该BuilderNode作为子节点挂载到其他FrameNode上,对该FrameNode或对应的RenderNode进行属性设置与子节点操作可能会产生未定义行为,包括但不限于显示异常、事件异常、稳定性问题等。

5.1 基本概念

  • 系统组件:组件是UI的必要元素,形成了在界面中的样子,由ArkUI直接提供的称为系统组件。
  • 实体节点:由后端创建的Native节点。

BuilderNode仅可作为叶子节点进行使用。如有更新需要,建议通过BuilderNode中的update方式触发更新,不建议通过BuilderNode中获取的RenderNode对节点进行修改操作。

  • BuilderNode只支持一个由wrapBuilder包装的全局自定义构建函数@Builder。
  • 一个新建的BuilderNode在build之后才能通过getFrameNode获取到一个指向根节点的FrameNode对象,否则返回null。
  • 如果传入的Builder的根节点为语法节点(if/else/foreach/...),需要额外生成一个FrameNode,在节点树中的显示为"BuilderProxyNode"。
  • 如果BuilderNode通过getFrameNode将节点挂载在另一个FrameNode上,或者将其作为子节点挂载在NodeContainer节点上。则节点中使用父组件的布局约束进行布局。
  • 如果BuilderNode的FrameNode通过getRenderNode形式将自己的节点挂载在RenderNode节点上,由于其FrameNode未上树,其大小默认为0,需要通过构造函数中的selfIdeaSize显式指定布局约束大小,才能正常显示。
  • BuilderNode的预加载并不会减少组件的创建时间。Web组件创建的时候需要在内核中加载资源,预创建不能减少Web组件的创建的时间,但是可以让内核进行预加载,减少正式使用时候内核的加载耗时。

5.2 创建BuilderNode对象

BuilderNode对象为一个模板类,需要在创建的时候指定类型。该类型需要与后续build方法中传入的WrappedBuilder的类型保持一致,否则会存在编译告警导致编译失败。

5.3 创建组件树

通过BuilderNode的build可以实现组件树的创建。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。

无状态的UI方法全局@Builder最多拥有一个根节点。

build方法中对应的@Builder支持一个参数作为入参

build中对于@Builder嵌套@Builder进行使用的场景,需要保证嵌套的参数与build的中提供的入参一致。

对于@Builder嵌套@Builder进行使用的场景,如果入参类型不一致,则要求增加BuilderOptions字段作为build的入参。

需要操作BuilderNode中的对象时,需要保证其引用不被回收。当BuilderNode对象被虚拟机回收之后,它的FrameNode、RenderNode对象也会与后端节点解引用。即从BuilderNode中获取的FrameNode对象不对应任何一个节点。

创建离线节点以及组件树,结合FrameNode进行使用。

BuilderNode的根节点直接作为NodeController的makeNode返回值。

5.4 更新组件树

通过BuilderNode对象的build创建组件树。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。

自定义组件的更新遵循状态管理的更新机制。WrappedBuilder中直接使用的自定义组件其父组件为BuilderNode对象。因此,更新子组件即WrappedBuilder中定义的自定义组件,需要遵循状态管理的定义将相关的状态变量定义为@Prop或者@ObjectLink。装饰器的选择请参照状态管理的装饰器规格结合应用开发需求进行选择。

使用update更新BuilderNode中的节点。

使用updateConfiguration触发BuilderNode中节点的全量更新。

更新BuilderNode中的节点。

相关推荐
6***x5452 小时前
TypeScript在全栈开发中的使用
前端·javascript·typescript
SuperHeroWu72 小时前
【HarmonyOS 6】为什么getContext 废弃,使用getHostContext说明
华为·harmonyos·context·上下文·getcontext·gethostcontext
lqj_本人3 小时前
鸿蒙Qt权限避坑:动态申请与Crash修复
qt·华为·harmonyos
在下历飞雨3 小时前
Kuikly基础之音频播放与资源管理:青蛙叫声实现
android·ios·harmonyos
1***Q7845 小时前
TypeScript类型兼容
前端·javascript·typescript
不爱吃糖的程序媛6 小时前
彻底解决 Flutter 开发 HarmonyOS 应用:No Hmos SDK found 报错
flutter·华为·harmonyos
liuxf12346 小时前
fvm管理鸿蒙flutter
flutter·华为·harmonyos
Y***K4347 小时前
TypeScript模块解析
前端·javascript·typescript
lqj_本人7 小时前
鸿蒙Qt数据库实战:SQLite死锁与沙箱路径陷阱
数据库·qt·harmonyos