• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 自定义组件节点 (FrameNode)
2<!--Kit: ArkUI-->
3<!--Subsystem: ArkUI-->
4<!--Owner: @xiang-shouxing-->
5<!--Designer: @xiang-shouxing-->
6<!--Tester: @sally__-->
7<!--Adviser: @HelloCrease-->
8
9## 概述
10
11对于拥有自定义前端的第三方框架(如JSON、XML、DOM树等),需将特定的DSL转换为ArkUI的声明式描述。如下图描述了JSON定义的前端框架和ArkUI声明式描述的对应关系。
12
13![zh-cn_image_frame-node01](figures/frame-node01.png)
14
15上述转换过程需要依赖额外的数据驱动,绑定至[Builder](../ui/state-management/arkts-builder.md)中,较为复杂且性能欠佳。这类框架通常依赖于ArkUI的布局、事件处理、基础的节点操作和自定义能力。大部分组件通过自定义实现,但需结合使用部分系统组件以实现混合显示,如下图示例既使用了FrameNode的自定义方法进行绘制,又使用了系统组件Column及其子组件Text,通过BuilderNode的方式将其挂载到根节点的FrameNode上混合显示。
16
17![zh-cn_image_frame-node02](figures/frame-node02.png)
18
19[FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md)的设计初衷正是为了解决上述转换问题。FrameNode表示组件树中的实体节点,与自定义占位容器组件[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)相配合,实现在占位容器内构建一棵自定义的节点树。该节点树支持动态操作,如节点的增加、修改和删除。基础的FrameNode具备设置通用属性和事件回调的功能,同时提供完整的自定义能力,涵盖自定义测量、布局和绘制等方面。
20
21除此之外,ArkUI还提供了获取和遍历系统组件对应代理FrameNode对象的能力(下文简称代理节点)。代理节点能够用于遍历整个UI的树形结构,支持获取系统组件节点的详细信息,以及额外注册组件的事件监听回调。
22
23## 创建和删除节点
24
25FrameNode提供了节点创建和删除的能力。可以通过FrameNode的构造函数创建自定义FrameNode节点,通过构造函数创建的节点对应一个实体的节点。同时,可以通过FrameNode中的[dispose](../reference/apis-arkui/js-apis-arkui-frameNode.md#dispose12)接口来实现与实体节点的绑定关系的解除。
26
27> **说明:**
28>
29> - 在创建FrameNode对象的时候需要传入必选参数UIContext,若未传入UIContext对象或者传入不合法,则节点创建抛出异常。
30>
31> - 自定义占位组件将节点进行显示的时候需要保证UI上下文一致,否则会出现显示异常。
32>
33> - 若不持有FrameNode对象,则该对象会在GC的时候被回收。
34
35## 判断节点是否可修改
36
37[isModifiable](../reference/apis-arkui/js-apis-arkui-frameNode.md#ismodifiable12)用于查询当前节点类型是否为系统组件的代理节点。当FrameNode节点作为系统组件的代理节点的时候,该节点不可修改。即无法修改代理节点的自身属性以及其子节点的结构。
38
39## 获取对应的RenderNode节点
40
41FrameNode提供了[getRenderNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#getrendernode)接口,用于获取FrameNode中的RenderNode。可以通过对获取到的RenderNode对象进行操作,动态修改FrameNode上绘制相关的属性,具体可修改的属性参考[RenderNode](arkts-user-defined-arktsNode-renderNode.md)的接口。
42
43> **说明:**
44>
45> - 无法获取系统组件代理FrameNode的RenderNode对象。
46>
47> - BuilderNode中调用[getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode)获取得到的FrameNode节点对象中,可以通过getRenderNode获取对应的根节点的RenderNode对象。
48
49## 操作节点树
50
51FrameNode提供了节点的增、删、查、改的能力,能够修改非代理节点的子树结构。可以对所有FrameNode的节点的父子节点做出查询操作,并返回查询结果。
52
53> **说明:**
54>
55> 对节点进行增、删、改操作的时候,会对非法操作抛出异常信息。
56>
57> 通过查询获得的系统组件的代理节点,仅具备查询节点信息的作用,不具备修改节点属性的功能。代理节点不持有组件的实体节点,即不影响对应的节点的生命周期。
58>
59> 查询节点仅查询获得UI相关的节点,不返回语法节点。
60>
61> 使用自定义组件的场景下,可能查询获得自定义组件的新增节点,节点类型为“\_\_Common\_\_”。
62
63```ts
64import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
65import { BusinessError } from '@kit.BasicServicesKit';
66
67const TEST_TAG: string = "FrameNode"
68
69class Params {
70  text: string = "this is a text"
71}
72
73@Builder
74function buttonBuilder(params: Params) {
75  Column({ space: 10 }) {
76    Button(params.text)
77      .fontSize(12)
78      .borderRadius(8)
79      .borderWidth(2)
80      .backgroundColor(Color.Orange)
81
82    Button(params.text)
83      .fontSize(12)
84      .borderRadius(8)
85      .borderWidth(2)
86      .backgroundColor(Color.Pink)
87  }
88}
89
90class MyNodeController extends NodeController {
91  public buttonNode: BuilderNode<[Params]> | null = null;
92  public frameNode: FrameNode | null = null;
93  public childList: Array<FrameNode> = new Array<FrameNode>();
94  public rootNode: FrameNode | null = null;
95  private uiContext: UIContext | null = null;
96  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(buttonBuilder);
97
98  makeNode(uiContext: UIContext): FrameNode | null {
99    this.uiContext = uiContext;
100    if (this.rootNode == null) {
101      this.rootNode = new FrameNode(uiContext);
102      this.rootNode.commonAttribute
103        .width("50%")
104        .height(100)
105        .borderWidth(1)
106        .backgroundColor(Color.Gray)
107    }
108
109    if (this.frameNode == null) {
110      this.frameNode = new FrameNode(uiContext);
111      this.frameNode.commonAttribute
112        .width("100%")
113        .height(50)
114        .borderWidth(1)
115        .position({ x: 200, y: 0 })
116        .backgroundColor(Color.Pink);
117      this.rootNode.appendChild(this.frameNode);
118    }
119    if (this.buttonNode == null) {
120      this.buttonNode = new BuilderNode<[Params]>(uiContext);
121      this.buttonNode.build(this.wrapBuilder, { text: "This is a Button" })
122      this.rootNode.appendChild(this.buttonNode.getFrameNode())
123    }
124    return this.rootNode;
125  }
126
127  operationFrameNodeWithFrameNode(frameNode: FrameNode | undefined | null) {
128    if (frameNode) {
129      console.info(TEST_TAG + " get ArkTSNode success.")
130      console.info(TEST_TAG + " check rootNode whether is modifiable " + frameNode.isModifiable());
131    }
132    if (this.uiContext) {
133      let frameNode1 = new FrameNode(this.uiContext);
134      let frameNode2 = new FrameNode(this.uiContext);
135      frameNode1.commonAttribute.size({ width: 50, height: 50 })
136        .backgroundColor(Color.Black)
137        .position({ x: 50, y: 60 })
138      frameNode2.commonAttribute.size({ width: 50, height: 50 })
139        .backgroundColor(Color.Orange)
140        .position({ x: 120, y: 60 })
141      try {
142        frameNode?.appendChild(frameNode1);
143        console.info(TEST_TAG + " appendChild success ");
144      } catch (err) {
145        console.error(TEST_TAG + " appendChild fail :" + (err as BusinessError).code + " : " +
146        (err as BusinessError).message);
147      }
148      try {
149        frameNode?.insertChildAfter(frameNode2, null);
150        console.info(TEST_TAG + " insertChildAfter success ");
151      } catch (err) {
152        console.error(TEST_TAG + " insertChildAfter fail : " + (err as BusinessError).code + " : " +
153        (err as BusinessError).message);
154      }
155      setTimeout(() => {
156        try {
157          frameNode?.removeChild(frameNode?.getChild(0))
158          console.info(TEST_TAG + " removeChild success ");
159        } catch (err) {
160          console.error(TEST_TAG + " removeChild fail : " + (err as BusinessError).code + " : " +
161          (err as BusinessError).message);
162        }
163      }, 2000)
164      setTimeout(() => {
165        try {
166          frameNode?.clearChildren();
167          console.info(TEST_TAG + " clearChildren success ");
168        } catch (err) {
169          console.error(TEST_TAG + " clearChildren fail : " + (err as BusinessError).code + " : " +
170          (err as BusinessError).message);
171        }
172      }, 4000)
173    }
174  }
175
176  testInterfaceAboutSearch(frameNode: FrameNode | undefined | null): string {
177    let result: string = "";
178    if (frameNode) {
179      result = result + `current node is ${frameNode.getNodeType()} \n`;
180      result = result + `parent node is ${frameNode.getParent()?.getNodeType()} \n`;
181      result = result + `child count is ${frameNode.getChildrenCount()} \n`;
182      result = result + `first child node is ${frameNode.getFirstChild()?.getNodeType()} \n`;
183      result = result + `second child node is ${frameNode.getChild(1)?.getNodeType()} \n`;
184      result = result + `previousSibling node is ${frameNode.getPreviousSibling()?.getNodeType()} \n`;
185      result = result + `nextSibling node is ${frameNode.getNextSibling()?.getNodeType()} \n`;
186    }
187    return result;
188  }
189
190  checkAppendChild(parent: FrameNode | undefined | null, child: FrameNode | undefined | null) {
191    try {
192      if (parent && child) {
193        parent.appendChild(child);
194        console.info(TEST_TAG + " appendChild success ");
195      }
196    } catch (err) {
197      console.error(TEST_TAG + " appendChild fail : " + (err as BusinessError).code + " : " +
198      (err as BusinessError).message);
199    }
200  }
201}
202
203@Entry
204@Component
205struct Index {
206  @State index: number = 0;
207  @State result: string = ""
208  private myNodeController: MyNodeController = new MyNodeController();
209
210  build() {
211    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
212      List({ space: 20, initialIndex: 0 }) {
213        ListItem() {
214          Column({ space: 5 }) {
215            Text("验证FrameNode子节点的增、删、改功能")
216            Button("对自定义FrameNode进行操作")
217              .fontSize(16)
218              .width(400)
219              .onClick(() => {
220                // 对FrameNode节点进行增、删、改操作,正常实现。
221                this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.frameNode);
222              })
223            Button("对BuilderNode中的代理节点进行操作")
224              .fontSize(16)
225              .width(400)
226              .onClick(() => {
227                // 对BuilderNode代理节点进行增、删、改操作,捕获异常信息。
228                this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.buttonNode?.getFrameNode());
229              })
230            Button("对系统组件中的代理节点进行操作")
231              .fontSize(16)
232              .width(400)
233              .onClick(() => {
234                // 对代理节点进行增、删、改操作,捕获异常信息。
235                this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.rootNode?.getParent());
236              })
237          }
238        }
239
240        ListItem() {
241          Column({ space: 5 }) {
242            Text("验证FrameNode添加子节点的特殊场景")
243            Button("新增BuilderNode的代理节点")
244              .fontSize(16)
245              .width(400)
246              .onClick(() => {
247                let buttonNode = new BuilderNode<[Params]>(this.getUIContext());
248                buttonNode.build(wrapBuilder<[Params]>(buttonBuilder), { text: "BUTTON" })
249                this.myNodeController.checkAppendChild(this.myNodeController?.frameNode, buttonNode?.getFrameNode());
250              })
251            Button("新增系统组件代理节点")
252              .fontSize(16)
253              .width(400)
254              .onClick(() => {
255                this.myNodeController.checkAppendChild(this.myNodeController?.frameNode,
256                  this.myNodeController?.rootNode?.getParent());
257              })
258            Button("新增已有父节点的自定义节点")
259              .fontSize(16)
260              .width(400)
261              .onClick(() => {
262                this.myNodeController.checkAppendChild(this.myNodeController?.frameNode,
263                  this.myNodeController?.rootNode);
264              })
265          }
266        }
267
268        ListItem() {
269          Column({ space: 5 }) {
270            Text("验证FrameNode节点的查询功能")
271            Button("对自定义FrameNode进行操作")
272              .fontSize(16)
273              .width(400)
274              .onClick(() => {
275                // 对FrameNode节点进行进行查询。当前节点为NodeContainer的子节点。
276                this.result = this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.rootNode);
277                setTimeout(() => {
278                  // 对FrameNode节点进行进行查询。rootNode下的第一个子节点。
279                  this.result = this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.frameNode);
280                }, 2000)
281              })
282            Button("对BuilderNode中的代理节点进行操作")
283              .fontSize(16)
284              .width(400)
285              .onClick(() => {
286                // 对BuilderNode代理节点进行进行查询。当前节点为BuilderNode中的Column节点。
287                this.result =
288                  this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.buttonNode?.getFrameNode());
289              })
290            Button("对系统组件中的代理节点进行操作")
291              .fontSize(16)
292              .width(400)
293              .onClick(() => {
294                // 对代理节点进行查询。当前节点为NodeContainer。
295                this.result =
296                  this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.rootNode?.getParent());
297              })
298          }
299        }
300      }.height("50%")
301
302      Text(`Result:\n${this.result}`)
303        .fontSize(16)
304        .width(400)
305        .height(200)
306        .padding(30)
307        .borderWidth(1)
308      Column() {
309        Text("This is a NodeContainer.")
310          .textAlign(TextAlign.Center)
311          .borderRadius(10)
312          .backgroundColor(0xFFFFFF)
313          .width('100%')
314          .fontSize(16)
315        NodeContainer(this.myNodeController)
316          .borderWidth(1)
317          .width(400)
318          .height(150)
319      }
320    }
321    .padding({
322      left: 35,
323      right: 35,
324      top: 35,
325      bottom: 35
326    })
327    .width("100%")
328    .height("100%")
329  }
330}
331```
332
333## 使用moveTo移动命令式节点
334
335使用[moveTo](../reference/apis-arkui/js-apis-arkui-frameNode.md#moveto18)接口可以将FrameNode节点移动到新的父节点下,从而按需改变节点树结构。
336
337> **说明:**
338>
339> 当前FrameNode如果不可修改,抛出异常信息。
340>
341> 目标父节点为[typeNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#typenode12)时会校验子组件类型或个数,不满足抛出异常信息,限制情况请查看[typeNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#typenode12)描述。
342>
343> 当前不支持对无组件类型的命令式节点进行移动。
344>
345> 当前仅支持以下类型的[TypedFrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#typedframenode12)进行移动操作:[Stack](../reference/apis-arkui/js-apis-arkui-frameNode.md#stack12)、[XComponent](../reference/apis-arkui/js-apis-arkui-frameNode.md#xcomponent12)。对于其他类型的节点,移动操作不会生效。
346>
347> 当前仅支持根节点为以下类型组件的[BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#buildernode-1)进行移动操作:[Stack](../reference/apis-arkui/arkui-ts/ts-container-stack.md)、[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)、[EmbeddedComponent](../reference/apis-arkui/arkui-ts/ts-container-embedded-component.md)。对于其他类型的组件,移动操作不会生效。
348
349```ts
350import { FrameNode, NodeController, UIContext, typeNode } from '@kit.ArkUI';
351
352class MyNodeController extends NodeController {
353  uiContext: UIContext | null = null;
354  rootNode: FrameNode | null = null;
355  rowNode: FrameNode | null = null;
356  stackNode1: FrameNode | null = null;
357  stackNode2: FrameNode | null = null;
358  stackNode3: FrameNode | null = null;
359
360  makeNode(uiContext: UIContext): FrameNode | null {
361    this.uiContext = uiContext;
362    this.rootNode = new FrameNode(uiContext);
363
364    const row = typeNode.createNode(this.uiContext, 'Row');
365    row.initialize({ space: 10 });
366    this.rowNode = row;
367    this.rootNode.appendChild(this.rowNode);
368
369    const stack1 = typeNode.createNode(this.uiContext, 'Stack');
370    stack1.commonAttribute.width(50).height(50).backgroundColor(Color.Pink);
371    this.stackNode1 = stack1;
372    this.rowNode?.appendChild(this.stackNode1);
373    const stack2 = typeNode.createNode(this.uiContext, 'Stack');
374    stack2.commonAttribute.width(50).height(50).backgroundColor(Color.Yellow);
375    this.stackNode2 = stack2;
376    this.rowNode?.appendChild(this.stackNode2);
377    const stack3 = typeNode.createNode(this.uiContext, 'Stack');
378    stack3.commonAttribute.width(50).height(50).backgroundColor(Color.Green);
379    this.stackNode3 = stack3;
380    this.rowNode?.appendChild(this.stackNode3);
381
382    return this.rootNode;
383  }
384}
385
386@Entry
387@Component
388struct Index {
389  private myNodeController1: MyNodeController = new MyNodeController()
390  private myNodeController2: MyNodeController = new MyNodeController()
391
392  build() {
393    Column({ space: 20 }) {
394      NodeContainer(this.myNodeController1)
395      NodeContainer(this.myNodeController2)
396      Button("move")
397        .onClick(() => {
398          this.myNodeController1.stackNode1?.moveTo(this.myNodeController2.rowNode, 2);
399        })
400    }
401    .height('100%')
402    .width('100%')
403  }
404}
405```
406
407![moveToDemo](figures/moveToDemo.gif)
408
409## 设置节点通用属性和事件回调
410
411FrameNode提供了[commonAttribute](../reference/apis-arkui/js-apis-arkui-frameNode.md#commonattribute12)和[commonEvent](../reference/apis-arkui/js-apis-arkui-frameNode.md#commonevent12)两个对象用于设置节点的[通用属性](../reference/apis-arkui/arkui-ts/ts-component-general-attributes.md)和[设置事件回调](../reference/apis-arkui/arkui-ts/ts-uicommonevent.md)。
412
413> **说明:**
414>
415> - 由于代理节点的属性不可修改,因此通过代理节点的commonAttribute修改节点的基础属性不生效。
416>
417> - 设置的基础事件与系统组件定义的事件平行,参与事件竞争。设置的基础事件不覆盖系统组件事件。同时设置两个事件回调的时候,优先回调系统组件事件。
418
419```ts
420import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'
421
422class Params {
423  text: string = "this is a text"
424}
425
426@Builder
427function buttonBuilder(params: Params) {
428  Button(params.text)
429    .fontSize(12)
430    .borderRadius(8)
431    .borderWidth(2)
432    .backgroundColor(Color.Orange)
433    .onClick((event: ClickEvent) => {
434      console.info(`Button ${JSON.stringify(event)}`);
435    })
436}
437
438class MyNodeController extends NodeController {
439  public buttonNode: BuilderNode<[Params]> | null = null;
440  public frameNode: FrameNode | null = null;
441  public rootNode: FrameNode | null = null;
442  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(buttonBuilder);
443
444  makeNode(uiContext: UIContext): FrameNode | null {
445    if (this.rootNode == null) {
446      this.rootNode = new FrameNode(uiContext);
447      // 对rootNode进行属性修改,该节点为自定义的FrameNode节点,修改生效
448      this.rootNode.commonAttribute
449        .width("100%")
450        .height(100)
451        .borderWidth(1)
452        .backgroundColor(Color.Gray)
453    }
454
455    if (this.frameNode == null) {
456      this.frameNode = new FrameNode(uiContext);
457      // 对frameNode进行属性修改,该节点为自定义的FrameNode节点,修改生效
458      this.frameNode.commonAttribute
459        .width("50%")
460        .height(50)
461        .borderWidth(1)
462        .backgroundColor(Color.Pink);
463      this.rootNode.appendChild(this.frameNode);
464    }
465    if (this.buttonNode == null) {
466      this.buttonNode = new BuilderNode<[Params]>(uiContext);
467      this.buttonNode.build(this.wrapBuilder, { text: "This is a Button" })
468      // 对BuilderNode中获取的FrameNode进行属性修改,该节点非自定义的FrameNode节点,修改不生效
469      this.buttonNode?.getFrameNode()?.commonAttribute.position({ x: 100, y: 100 })
470      this.rootNode.appendChild(this.buttonNode.getFrameNode())
471    }
472    return this.rootNode;
473  }
474
475  modifyNode(frameNode: FrameNode | null | undefined, sizeValue: SizeOptions, positionValue: Position) {
476    if (frameNode) {
477      frameNode.commonAttribute.size(sizeValue).position(positionValue);
478    }
479  }
480
481  addClickEvent(frameNode: FrameNode | null | undefined) {
482    if (frameNode) {
483      frameNode.commonEvent.setOnClick((event: ClickEvent) => {
484        console.info(`FrameNode ${JSON.stringify(event)}`);
485      })
486    }
487  }
488}
489
490@Entry
491@Component
492struct Index {
493  private myNodeController: MyNodeController = new MyNodeController();
494
495  build() {
496    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
497      Column({ space: 10 }) {
498        Text("修改节点通用属性-宽高")
499        Button("modify ArkTS-FrameNode")
500          .onClick(() => {
501            // 获取到的是当前页面中的开发者创建的FrameNode对象,该节点可修改。即节点大小与位置。
502            console.info("Check the weather the node can be modified " + this.myNodeController?.frameNode
503            ?.isModifiable());
504            this.myNodeController.modifyNode(this.myNodeController?.frameNode, { width: 150, height: 100 }, {
505              x: 100,
506              y: 0
507            })
508          })
509        Button("modify FrameNode get by BuilderNode")
510          .onClick(() => {
511            // 获取到的是当前页面中的BuilderNode的根节点,该节点不可修改。即节点大小与位置未发生改变。
512            console.info("Check the weather the node can be modified " +
513            this.myNodeController?.buttonNode?.getFrameNode()
514            ?.isModifiable());
515            this.myNodeController.modifyNode(this.myNodeController?.buttonNode?.getFrameNode(), {
516              width: 100,
517              height: 100
518            }, { x: 50, y: 50 })
519          })
520        Button("modify proxyFrameNode get by search")
521          .onClick(() => {
522            // rootNode调用getParent()获取到的是当前页面中的NodeContainer节点,该节点不可修改。即节点大小与位置未发生改变。
523            console.info("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
524            ?.isModifiable());
525            this.myNodeController.modifyNode(this.myNodeController?.rootNode?.getParent(), {
526              width: 500,
527              height: 500
528            }, {
529              x: 0,
530              y: 0
531            })
532          })
533      }
534      .padding({
535        left: 35,
536        right: 35,
537        top: 35,
538        bottom: 35
539      })
540
541      Column({ space: 10 }) {
542        Text("修改节点点击事件")
543        Button("add click event to ArkTS-FrameNode")
544          .onClick(() => {
545            // 获取到的是当前页面中的开发者创建的FrameNode对象,该节点可增加点击事件。
546            // 增加的点击事件参与事件竞争,即点击事件会在该节点被消费且不不再向父组件冒泡。
547            console.info("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
548            ?.isModifiable());
549            this.myNodeController.addClickEvent(this.myNodeController?.frameNode)
550          })
551        Button("add click event to FrameNode get by BuilderNode")
552          .onClick(() => {
553            // 获取到的是当前页面中的BuilderNode的根节点,该类节点可增加点击事件。
554            // 点击的时候优先回调通过系统组件接口设置的click事件回调,然后回调通过commonEvent增加的click监听。
555            console.info("Check the weather the node can be modified " +
556            this.myNodeController?.buttonNode?.getFrameNode()
557            ?.isModifiable());
558            this.myNodeController.addClickEvent(this.myNodeController?.buttonNode?.getFrameNode())
559          })
560        Button("add click event to proxyFrameNode get by search")
561          .onClick(() => {
562            // rootNode调用getParent()获取到的是当前页面中的NodeContainer节点,该类节点可增加点击事件。
563            console.info("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
564            ?.isModifiable());
565            this.myNodeController.addClickEvent(this.myNodeController?.rootNode?.getParent());
566          })
567      }
568      .padding({
569        left: 35,
570        right: 35,
571        top: 35,
572        bottom: 35
573      })
574
575      NodeContainer(this.myNodeController)
576        .borderWidth(1)
577        .width("100%")
578        .height(100)
579        .onClick((event: ClickEvent) => {
580          console.info(`NodeContainer ${JSON.stringify(event)}`);
581        })
582    }
583    .padding({
584      left: 35,
585      right: 35,
586      top: 35,
587      bottom: 35
588    })
589    .width("100%")
590    .height("100%")
591  }
592}
593```
594
595## 自定义测量布局与绘制
596
597通过重写[onDraw](../reference/apis-arkui/js-apis-arkui-frameNode.md#ondraw12)方法,可以自定义FrameNode的绘制内容。[invalidate](../reference/apis-arkui/js-apis-arkui-frameNode.md#invalidate12)接口可以主动触发节点的重新绘制。
598
599通过重写[onMeasure](../reference/apis-arkui/js-apis-arkui-frameNode.md#onmeasure12)可以自定义FrameNode的测量方式,使用[measure](../reference/apis-arkui/js-apis-arkui-frameNode.md#measure12)可以主动传递布局约束触发重新测量。
600
601通过重写[onLayout](../reference/apis-arkui/js-apis-arkui-frameNode.md#onlayout12)方法可以自定义FrameNode的布局方式,使用[layout](../reference/apis-arkui/js-apis-arkui-frameNode.md#layout12)方法可以主动传递位置信息并触发重新布局。
602
603[setNeedsLayout](../reference/apis-arkui/js-apis-arkui-frameNode.md#setneedslayout12)可以将当前节点标记,在下一帧触发重新布局。
604
605> **说明:**
606>
607> - 对节点进行dispose解引用后,由于FrameNode对象不再对应一个实体节点,invalidate无法触发原有绑定节点的刷新。
608>
609> - 通过onDraw方法进行的自定义绘制,绘制内容大小无法超出组件大小。
610
611```ts
612import { DrawContext, FrameNode, NodeController, Position, Size, UIContext, LayoutConstraint } from '@kit.ArkUI';
613import { drawing } from '@kit.ArkGraphics2D';
614
615function GetChildLayoutConstraint(constraint: LayoutConstraint, child: FrameNode): LayoutConstraint {
616  const size = child.getUserConfigSize();
617  const width = Math.max(
618    Math.min(constraint.maxSize.width, size.width.value),
619    constraint.minSize.width
620  );
621  const height = Math.max(
622    Math.min(constraint.maxSize.height, size.height.value),
623    constraint.minSize.height
624  );
625  const finalSize: Size = { width, height };
626  const res: LayoutConstraint = {
627    maxSize: finalSize,
628    minSize: finalSize,
629    percentReference: finalSize
630  };
631
632  return res;
633}
634
635class MyFrameNode extends FrameNode {
636  public width: number = 100;
637  public offsetY: number = 0;
638  private space: number = 1;
639  uiContext: UIContext;
640
641  constructor(uiContext: UIContext) {
642    super(uiContext);
643    this.uiContext = uiContext;
644  }
645
646  onMeasure(constraint: LayoutConstraint): void {
647    let sizeRes: Size = { width: this.uiContext.vp2px(100), height: this.uiContext.vp2px(100) };
648    for (let i = 0; i < this.getChildrenCount(); i++) {
649      let child = this.getChild(i);
650      if (child) {
651        let childConstraint = GetChildLayoutConstraint(constraint, child);
652        child.measure(childConstraint);
653        let size = child.getMeasuredSize();
654        sizeRes.height += size.height + this.space;
655        sizeRes.width = Math.max(sizeRes.width, size.width);
656      }
657    }
658    this.setMeasuredSize(sizeRes);
659  }
660
661  onLayout(position: Position): void {
662    for (let i = 0; i < this.getChildrenCount(); i++) {
663      let child = this.getChild(i);
664      if (child) {
665        child.layout({
666          x: this.uiContext.vp2px(100),
667          y: this.uiContext.vp2px(this.offsetY)
668        });
669        let layoutPosition = child.getLayoutPosition();
670        console.info("child position:" + JSON.stringify(layoutPosition));
671      }
672    }
673    this.setLayoutPosition(position);
674  }
675
676  onDraw(context: DrawContext) {
677    const canvas = context.canvas;
678    const pen = new drawing.Pen();
679    pen.setStrokeWidth(15);
680    pen.setColor({
681      alpha: 255,
682      red: 255,
683      green: 0,
684      blue: 0
685    });
686    canvas.attachPen(pen);
687    canvas.drawRect({
688      left: 50,
689      right: this.width + 50,
690      top: 50,
691      bottom: this.width + 50,
692    });
693    canvas.detachPen();
694  }
695
696  addWidth() {
697    this.width = (this.width + 10) % 50 + 100;
698  }
699}
700
701class MyNodeController extends NodeController {
702  public rootNode: MyFrameNode | null = null;
703
704  makeNode(context: UIContext): FrameNode | null {
705    this.rootNode = new MyFrameNode(context);
706    this.rootNode?.commonAttribute?.size({ width: 100, height: 100 }).backgroundColor(Color.Green);
707    let frameNode: FrameNode = new FrameNode(context);
708    this.rootNode.appendChild(frameNode);
709    frameNode.commonAttribute.width(10).height(10).backgroundColor(Color.Pink);
710    return this.rootNode;
711  }
712}
713
714@Entry
715@Component
716struct Index {
717  private nodeController: MyNodeController = new MyNodeController();
718
719  build() {
720    Row() {
721      Column() {
722        NodeContainer(this.nodeController)
723          .width('100%')
724          .height(200)
725          .backgroundColor('#FFF0F0F0')
726        Button('Invalidate')
727          .margin(10)
728          .onClick(() => {
729            this.nodeController?.rootNode?.addWidth();
730            this.nodeController?.rootNode?.invalidate();
731          })
732        Button('UpdateLayout')
733          .onClick(() => {
734            let node = this.nodeController.rootNode;
735            node!.offsetY = (node!.offsetY + 10) % 110;
736            this.nodeController?.rootNode?.setNeedsLayout();
737          })
738      }
739      .width('100%')
740      .height('100%')
741    }
742    .height('100%')
743  }
744}
745```
746
747## 查找节点及获取基础信息
748
749FrameNode提供了查询接口用于返回实体节点的基础信息。具体返回的信息内容参考FrameNode中提供的接口。
750
751查找获得FrameNode的方式包括三种:
752
7531. 使用[getFrameNodeById](../reference/apis-arkui/arkts-apis-uicontext-uicontext.md#getframenodebyid12)获取。
754
7552. 使用[getFrameNodeByUniqueId](../reference/apis-arkui/arkts-apis-uicontext-uicontext.md#getframenodebyuniqueid12)获取。
756
7573. 通过[无感监听](../reference/apis-arkui/js-apis-arkui-observer.md)获取。
758
759> **说明:**
760>
761> 1、当前接口提供的可查询的信息包括:
762>
763> - 节点大小:[getMeasuredSize](../reference/apis-arkui/js-apis-arkui-frameNode.md#getmeasuredsize12),[getUserConfigSize](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuserconfigsize12)
764>
765> - 布局信息:[getPositionToWindow](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontowindow12),[getPositionToParent](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontoparent12),[getLayoutPosition](../reference/apis-arkui/js-apis-arkui-frameNode.md#getlayoutposition12),[getUserConfigBorderWidth](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuserconfigborderwidth12),[getUserConfigPadding](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuserconfigpadding12),[getUserConfigMargin](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuserconfigmargin12)
766>
767> - 节点信息:[getId](../reference/apis-arkui/js-apis-arkui-frameNode.md#getid12),[getUniqueId](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuniqueid12),[getNodeType](../reference/apis-arkui/js-apis-arkui-frameNode.md#getnodetype12),[getOpacity](../reference/apis-arkui/js-apis-arkui-frameNode.md#getopacity12),[isVisible](../reference/apis-arkui/js-apis-arkui-frameNode.md#isvisible12),[isClipToFrame](../reference/apis-arkui/js-apis-arkui-frameNode.md#iscliptoframe12),[isAttached](../reference/apis-arkui/js-apis-arkui-frameNode.md#isattached12),[getInspectorInfo](../reference/apis-arkui/js-apis-arkui-frameNode.md#getinspectorinfo12),[getCustomProperty](../reference/apis-arkui/js-apis-arkui-frameNode.md#getcustomproperty12)
768>
769> 2、无法获取UINode类型节点,例如:JsView节点、[Span](../../application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-span.md)、[ContainerSpan](../../application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-containerspan.md)、[ContentSlot](../../application-dev/reference/apis-arkui/arkui-ts/ts-components-contentSlot.md)、[ForEach](../../application-dev/reference/apis-arkui/arkui-ts/ts-rendering-control-foreach.md)、[LazyForEach](../../application-dev/reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)、if/else组件等。
770
771## 获取节点位置偏移信息
772
773FrameNode提供了查询节点相对窗口、父组件以及屏幕位置偏移的信息接口([getPositionToWindow](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontowindow12),[getPositionToParent](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontoparent12),[getPositionToScreen](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontoscreen12),[getGlobalPositionOnDisplay](../reference/apis-arkui/js-apis-arkui-frameNode.md#getglobalpositionondisplay20),[getPositionToWindowWithTransform](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontowindowwithtransform12),[getPositionToParentWithTransform](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontoparentwithtransform12),[getPositionToScreenWithTransform](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontoscreenwithtransform12),[getLayoutPosition](../reference/apis-arkui/js-apis-arkui-frameNode.md#getlayoutposition12),[getUserConfigBorderWidth](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuserconfigborderwidth12),[getUserConfigPadding](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuserconfigpadding12),[getUserConfigMargin](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuserconfigmargin12))。
774
775[getPositionToWindow](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontowindow12),[getPositionToParent](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontoparent12),[getPositionToScreen](../reference/apis-arkui/js-apis-arkui-frameNode.md#getpositiontoscreen12)三个接口获取到的位置信息关系如下图所示:
776
777![FrameNode-Position-Relation](./figures/frameNode-position-relation.png)
778
779```ts
780import { NodeController, FrameNode, UIContext } from '@kit.ArkUI';
781
782const TEST_TAG: string = "FrameNode"
783
784class MyNodeController extends NodeController {
785  public frameNode: FrameNode | null = null;
786  private rootNode: FrameNode | null = null;
787
788  makeNode(uiContext: UIContext): FrameNode | null {
789    this.rootNode = new FrameNode(uiContext);
790    this.frameNode = new FrameNode(uiContext);
791    this.rootNode.appendChild(this.frameNode);
792    return this.rootNode;
793  }
794
795  getPositionToWindow() {
796    let positionToWindow = this.rootNode?.getPositionToWindow(); // 获取FrameNode相对于窗口的位置偏移
797    console.info(`${TEST_TAG} ${JSON.stringify(positionToWindow)}`);
798  }
799
800  getPositionToParent() {
801    let positionToParent = this.rootNode?.getPositionToParent(); // 获取FrameNode相对于父组件的位置偏移
802    console.info(`${TEST_TAG} ${JSON.stringify(positionToParent)}`);
803  }
804
805  getPositionToScreen() {
806    let positionToScreen = this.rootNode?.getPositionToScreen(); // 获取FrameNode相对于屏幕的位置偏移
807    console.info(`${TEST_TAG} ${JSON.stringify(positionToScreen)}`);
808  }
809
810  getGlobalPositionOnDisplay() {
811    let positionOnGlobalDisplay = this.rootNode?.getGlobalPositionOnDisplay(); // 获取FrameNode相对于全局屏幕的位置偏移
812    console.info(`${TEST_TAG} ${JSON.stringify(positionOnGlobalDisplay)}`);
813  }
814
815  getPositionToWindowWithTransform() {
816    let positionToWindowWithTransform =
817      this.rootNode?.getPositionToWindowWithTransform(); // 获取FrameNode相对于窗口带有绘制属性的位置偏移
818    console.info(`${TEST_TAG} ${JSON.stringify(positionToWindowWithTransform)}`);
819  }
820
821  getPositionToParentWithTransform() {
822    let positionToParentWithTransform =
823      this.rootNode?.getPositionToParentWithTransform(); // 获取FrameNode相对于父组件带有绘制属性的位置偏移
824    console.info(`${TEST_TAG} ${JSON.stringify(positionToParentWithTransform)}`);
825  }
826
827  getPositionToScreenWithTransform() {
828    let positionToScreenWithTransform =
829      this.rootNode?.getPositionToScreenWithTransform(); // 获取FrameNode相对于屏幕带有绘制属性的位置偏移
830    console.info(`${TEST_TAG} ${JSON.stringify(positionToScreenWithTransform)}`);
831  }
832}
833
834@Entry
835@Component
836struct Index {
837  private myNodeController: MyNodeController = new MyNodeController();
838
839  build() {
840    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
841      Button("getPositionToWindow")
842        .width(300)
843        .onClick(() => {
844          this.myNodeController.getPositionToWindow();
845        })
846      Button("getPositionToParent")
847        .width(300)
848        .onClick(() => {
849          this.myNodeController.getPositionToParent();
850        })
851      Button("getPositionToScreen")
852        .width(300)
853        .onClick(() => {
854          this.myNodeController.getPositionToScreen();
855        })
856      Button("getGlobalPositionOnDisplay")
857        .width(300)
858        .onClick(() => {
859          this.myNodeController.getGlobalPositionOnDisplay();
860        })
861      Button("getPositionToParentWithTransform")
862        .width(300)
863        .onClick(() => {
864          this.myNodeController.getPositionToParentWithTransform();
865        })
866      Button("getPositionToWindowWithTransform")
867        .width(300)
868        .onClick(() => {
869          this.myNodeController.getPositionToWindowWithTransform();
870        })
871      Button("getPositionToScreenWithTransform")
872        .width(300)
873        .onClick(() => {
874          this.myNodeController.getPositionToScreenWithTransform();
875        })
876      Column() {
877        Text("This is a NodeContainer.")
878          .textAlign(TextAlign.Center)
879          .borderRadius(10)
880          .backgroundColor(0xFFFFFF)
881          .width('100%')
882          .fontSize(16)
883        NodeContainer(this.myNodeController)
884          .borderWidth(1)
885          .width(300)
886          .height(100)
887      }
888    }
889    .padding({
890      left: 35,
891      right: 35,
892      top: 35,
893      bottom: 35
894    })
895    .width("100%")
896    .height("100%")
897  }
898}
899```
900
901## 通过typeNode创建具体类型的FrameNode节点
902
903通过TypeNode创建具体类型的FrameNode节点,可以根据属性获取接口来检索用户设置的属性信息。
904
905```ts
906import { NodeController, FrameNode, UIContext, BuilderNode, typeNode } from '@kit.ArkUI';
907
908class Params {
909  text: string = "";
910
911  constructor(text: string) {
912    this.text = text;
913  }
914}
915
916@Builder
917function buildText(params: Params) {
918  Column() {
919    Text(params.text)
920      .id("buildText")
921      .border({ width: 1 })
922      .padding(1)
923      .fontSize(25)
924      .fontWeight(FontWeight.Bold)
925      .margin({ top: 10 })
926      .visibility(Visibility.Visible)
927      .opacity(0.7)
928      .customProperty("key1", "value1")
929      .width(300)
930  }
931}
932
933const TEST_TAG: string = "FrameNode"
934
935class MyNodeController extends NodeController {
936  public frameNode: typeNode.Column | null = null;
937  public uiContext: UIContext | undefined = undefined;
938  private rootNode: FrameNode | null = null;
939  private textNode: BuilderNode<[Params]> | null = null;
940  public textTypeNode: typeNode.Text | null = null;
941  private message: string = "DEFAULT";
942
943  makeNode(uiContext: UIContext): FrameNode | null {
944    this.rootNode = new FrameNode(uiContext);
945    this.uiContext = uiContext;
946    this.frameNode = typeNode.createNode(uiContext, "Column");
947    this.frameNode.attribute
948      .width("100%")
949      .height("100%")
950    this.rootNode.appendChild(this.frameNode);
951    this.textNode = new BuilderNode(uiContext);
952    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
953    this.frameNode.appendChild(this.textNode.getFrameNode());
954    this.textTypeNode = typeNode.createNode(uiContext, "Text");
955    this.textTypeNode.initialize("textTypeNode")
956      .fontSize(25)
957      .visibility(Visibility.Visible)
958      .id("textTypeNode")
959    this.frameNode.appendChild(this.textTypeNode);
960    return this.rootNode;
961  }
962
963  removeChild(frameNode: FrameNode) {
964    let parent = frameNode.getParent();
965    if (parent) {
966      parent.removeChild(frameNode);
967
968    }
969  }
970
971  getUserConfigBorderWidth(frameNode: FrameNode) {
972    let userConfigBorderWidth = frameNode?.getUserConfigBorderWidth(); // 获取用户设置的边框宽度
973    console.info(`${TEST_TAG} ${JSON.stringify(userConfigBorderWidth)}`);
974  }
975
976  getUserConfigPadding(frameNode: FrameNode) {
977    let userConfigPadding = frameNode?.getUserConfigPadding(); // 获取用户设置的内边距
978    console.info(`${TEST_TAG} ${JSON.stringify(userConfigPadding)}`);
979  }
980
981  getUserConfigMargin(frameNode: FrameNode) {
982    let userConfigMargin = frameNode?.getUserConfigMargin(); // 获取用户设置的外边距
983    console.info(`${TEST_TAG} ${JSON.stringify(userConfigMargin)}`);
984  }
985
986  getUserConfigSize(frameNode: FrameNode) {
987    let userConfigSize = frameNode?.getUserConfigSize(); // 获取用户设置的宽高
988    console.info(`${TEST_TAG} ${JSON.stringify(userConfigSize)}`);
989  }
990
991  getId(frameNode: FrameNode) {
992    let id = frameNode?.getId(); // 获取用户设置的节点ID
993    console.info(`${TEST_TAG} ${id}`);
994  }
995
996  getUniqueId(frameNode: FrameNode) {
997    let uniqueId = frameNode?.getUniqueId(); // 获取系统分配的唯一标识的节点UniqueID
998    console.info(`${TEST_TAG} ${uniqueId}`);
999  }
1000
1001  getNodeType(frameNode: FrameNode) {
1002    let nodeType = frameNode?.getNodeType(); // 获取节点的类型
1003    console.info(`${TEST_TAG} ${nodeType}`);
1004  }
1005
1006  getOpacity(frameNode: FrameNode) {
1007    let opacity = frameNode?.getOpacity(); // 获取节点的不透明度
1008    console.info(`${TEST_TAG} ${JSON.stringify(opacity)}`);
1009  }
1010
1011  isVisible(frameNode: FrameNode) {
1012    let visible = frameNode?.isVisible(); // 获取节点是否可见
1013    console.info(`${TEST_TAG} ${JSON.stringify(visible)}`);
1014  }
1015
1016  isClipToFrame(frameNode: FrameNode) {
1017    let clipToFrame = frameNode?.isClipToFrame(); // 获取节点是否是剪裁到组件区域
1018    console.info(`${TEST_TAG} ${JSON.stringify(clipToFrame)}`);
1019  }
1020
1021  isAttached(frameNode: FrameNode) {
1022    let attached = frameNode?.isAttached(); // 获取节点是否被挂载到主节点树上
1023    console.info(`${TEST_TAG} ${JSON.stringify(attached)}`);
1024  }
1025
1026  getInspectorInfo(frameNode: FrameNode) {
1027    let inspectorInfo = frameNode?.getInspectorInfo(); // 获取节点的结构信息
1028    console.info(`${TEST_TAG} ${JSON.stringify(inspectorInfo)}`);
1029  }
1030}
1031
1032@Entry
1033@Component
1034struct Index {
1035  private myNodeController: MyNodeController = new MyNodeController();
1036  @State index: number = 0;
1037
1038  build() {
1039    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
1040      Column() {
1041        Text("This is a NodeContainer.")
1042          .textAlign(TextAlign.Center)
1043          .borderRadius(10)
1044          .backgroundColor(0xFFFFFF)
1045          .width('100%')
1046          .fontSize(16)
1047        NodeContainer(this.myNodeController)
1048          .borderWidth(1)
1049          .width(300)
1050          .height(100)
1051      }
1052
1053      Button("getUserConfigBorderWidth")
1054        .width(300)
1055        .onClick(() => {
1056          const uiContext: UIContext = this.getUIContext();
1057          if (uiContext) {
1058            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1059            if (node) {
1060              this.myNodeController.getUserConfigBorderWidth(node);
1061            }
1062          }
1063        })
1064      Button("getUserConfigPadding")
1065        .width(300)
1066        .onClick(() => {
1067          const uiContext: UIContext = this.getUIContext();
1068          if (uiContext) {
1069            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1070            if (node) {
1071              this.myNodeController.getUserConfigPadding(node);
1072            }
1073          }
1074        })
1075      Button("getUserConfigMargin")
1076        .width(300)
1077        .onClick(() => {
1078          const uiContext: UIContext = this.getUIContext();
1079          if (uiContext) {
1080            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1081            if (node) {
1082              this.myNodeController.getUserConfigMargin(node);
1083            }
1084          }
1085        })
1086      Button("getUserConfigSize")
1087        .width(300)
1088        .onClick(() => {
1089          const uiContext: UIContext = this.getUIContext();
1090          if (uiContext) {
1091            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1092            if (node) {
1093              this.myNodeController.getUserConfigSize(node);
1094            }
1095          }
1096        })
1097      Button("getId")
1098        .width(300)
1099        .onClick(() => {
1100          const uiContext: UIContext = this.getUIContext();
1101          if (uiContext) {
1102            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1103            if (node) {
1104              this.myNodeController.getId(node);
1105            }
1106          }
1107        })
1108      Button("getUniqueId")
1109        .width(300)
1110        .onClick(() => {
1111          const uiContext: UIContext = this.getUIContext();
1112          if (uiContext) {
1113            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1114            if (node) {
1115              this.myNodeController.getUniqueId(node);
1116            }
1117          }
1118        })
1119      Button("getNodeType")
1120        .width(300)
1121        .onClick(() => {
1122          const uiContext: UIContext = this.getUIContext();
1123          if (uiContext) {
1124            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1125            if (node) {
1126              this.myNodeController.getNodeType(node);
1127            }
1128          }
1129        })
1130      Button("getOpacity")
1131        .width(300)
1132        .onClick(() => {
1133          const uiContext: UIContext = this.getUIContext();
1134          if (uiContext) {
1135            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1136            if (node) {
1137              this.myNodeController.getOpacity(node);
1138            }
1139          }
1140        })
1141      Button("isVisible")
1142        .width(300)
1143        .onClick(() => {
1144          const uiContext: UIContext = this.getUIContext();
1145          if (uiContext) {
1146            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1147            if (node) {
1148              this.myNodeController.isVisible(node);
1149            }
1150          }
1151        })
1152      Button("isClipToFrame")
1153        .width(300)
1154        .onClick(() => {
1155          const uiContext: UIContext = this.getUIContext();
1156          if (uiContext) {
1157            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1158            if (node) {
1159              this.myNodeController.isClipToFrame(node);
1160            }
1161          }
1162        })
1163      Button("isAttached")
1164        .width(300)
1165        .onClick(() => {
1166          const uiContext: UIContext = this.getUIContext();
1167          if (uiContext) {
1168            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1169            if (node) {
1170              this.myNodeController.isAttached(node);
1171            }
1172          }
1173        })
1174      Button("remove Text")
1175        .width(300)
1176        .onClick(() => {
1177          const uiContext: UIContext = this.getUIContext();
1178          if (uiContext) {
1179            const node: FrameNode | null = uiContext.getFrameNodeById("textTypeNode") || null;
1180            if (node) {
1181              this.myNodeController.removeChild(node);
1182              this.myNodeController.isAttached(node);
1183            }
1184          }
1185        })
1186      Button("getInspectorInfo")
1187        .width(300)
1188        .onClick(() => {
1189          const uiContext: UIContext = this.getUIContext();
1190          if (uiContext) {
1191            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1192            if (node) {
1193              this.myNodeController.getInspectorInfo(node);
1194            }
1195          }
1196        })
1197      Button("getCustomProperty")
1198        .width(300)
1199        .onClick(() => {
1200          const uiContext: UIContext = this.getUIContext();
1201          if (uiContext) {
1202            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1203            if (node) {
1204              const property = node.getCustomProperty("key1");
1205              console.info(TEST_TAG, JSON.stringify(property));
1206            }
1207          }
1208        })
1209    }
1210    .padding({
1211      left: 35,
1212      right: 35,
1213      top: 35,
1214      bottom: 35
1215    })
1216    .width("100%")
1217    .height("100%")
1218  }
1219}
1220```
1221
1222## 解除当前FrameNode对象对实体FrameNode节点的引用关系
1223
1224使用[dispose](../reference/apis-arkui/js-apis-arkui-frameNode.md#dispose12)接口可以立即解除当前FrameNode对象对实体FrameNode节点的引用关系。
1225
1226> **说明:**
1227>
1228> 在调用dispose方法后,FrameNode对象不再对应任何实际的FrameNode节点。此时,若尝试调用以下查询接口:getMeasuredSize、getLayoutPosition、getUserConfigBorderWidth、getUserConfigPadding、getUserConfigMargin、getUserConfigSize,将导致应用程序触发jscrash。
1229>
1230> 通过[getUniqueId](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuniqueid12)可以判断当前FrameNode是否对应一个实体FrameNode节点。当UniqueId大于0时表示该对象对应一个实体FrameNode节点。
1231
1232```ts
1233import { NodeController, FrameNode, BuilderNode } from '@kit.ArkUI';
1234
1235const TEST_TAG: string = "FrameNode";
1236
1237@Component
1238struct TestComponent {
1239  build() {
1240    Column() {
1241      Text('This is a BuilderNode.')
1242        .fontSize(16)
1243        .fontWeight(FontWeight.Bold)
1244    }
1245    .width('100%')
1246    .backgroundColor(Color.Gray)
1247  }
1248
1249  aboutToAppear() {
1250    console.error(TEST_TAG + ' aboutToAppear');
1251  }
1252
1253  aboutToDisappear() {
1254    console.error(TEST_TAG + ' aboutToDisappear');
1255  }
1256}
1257
1258@Builder
1259function buildComponent() {
1260  TestComponent()
1261}
1262
1263class MyNodeController extends NodeController {
1264  private rootNode: FrameNode | null = null;
1265  private builderNode: BuilderNode<[]> | null = null;
1266
1267  makeNode(uiContext: UIContext): FrameNode | null {
1268    this.rootNode = new FrameNode(uiContext);
1269    this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 200, height: 100 } });
1270    this.builderNode.build(new WrappedBuilder(buildComponent));
1271
1272    const rootRenderNode = this.rootNode.getRenderNode();
1273    if (rootRenderNode !== null) {
1274      rootRenderNode.size = { width: 200, height: 200 };
1275      rootRenderNode.backgroundColor = 0xff00ff00;
1276      rootRenderNode.appendChild(this.builderNode!.getFrameNode()!.getRenderNode());
1277    }
1278
1279    return this.rootNode;
1280  }
1281
1282  printUniqueId(): void {
1283    if (this.rootNode !== null && this.builderNode !== null) {
1284      console.info(`${TEST_TAG} rootNode's uniqueId: ${this.rootNode.getUniqueId()}`);
1285      const frameNode = this.builderNode.getFrameNode();
1286      if (frameNode) {
1287        console.info(`${TEST_TAG} the uniqueId of builderNode's framenode: ${frameNode.getUniqueId()}`);
1288      } else {
1289        console.info(`${TEST_TAG} builderNode's framenode is undefined`);
1290      }
1291    }
1292  }
1293
1294  disposeFrameNode(): void {
1295    if (this.rootNode !== null && this.builderNode !== null) {
1296      console.info(`${TEST_TAG} disposeFrameNode`);
1297      this.rootNode.removeChild(this.builderNode.getFrameNode());
1298      this.builderNode.dispose();
1299
1300      this.rootNode.dispose();
1301    }
1302  }
1303
1304  removeBuilderNode(): void {
1305    const rootRenderNode = this.rootNode!.getRenderNode();
1306    if (rootRenderNode !== null && this.builderNode !== null && this.builderNode.getFrameNode() !== null) {
1307      rootRenderNode.removeChild(this.builderNode!.getFrameNode()!.getRenderNode());
1308    }
1309  }
1310}
1311
1312@Entry
1313@Component
1314struct Index {
1315  private myNodeController: MyNodeController = new MyNodeController();
1316
1317  build() {
1318    Column({ space: 4 }) {
1319      NodeContainer(this.myNodeController)
1320      Button('FrameNode dispose')
1321        .onClick(() => {
1322          this.myNodeController.printUniqueId();
1323          this.myNodeController.disposeFrameNode();
1324          this.myNodeController.printUniqueId();
1325        })
1326        .width('100%')
1327    }
1328  }
1329}
1330```
1331
1332## 查询当前FrameNode是否解除引用
1333
1334前端节点均绑定有相应的后端实体节点,当节点调用dispose接口解除绑定后,再次调用接口可能会出现crash、返回默认值的情况。
1335
1336从API version 20开始,使用[isDisposed](../reference/apis-arkui/js-apis-arkui-frameNode.md#isdisposed20)接口查询当前FrameNode对象是否已解除与后端实体节点的引用关系,从而可以在操作节点前检查其有效性,避免潜在风险。
1337
1338```ts
1339import { NodeController, FrameNode } from '@kit.ArkUI';
1340
1341class MyNodeController extends NodeController {
1342  private rootNode: FrameNode | null = null;
1343
1344  makeNode(uiContext: UIContext): FrameNode | null {
1345    this.rootNode = new FrameNode(uiContext);
1346    this.rootNode.commonAttribute.width(100).height(100).backgroundColor(Color.Pink);
1347
1348    return this.rootNode;
1349  }
1350
1351  disposeFrameNode() {
1352    // 解除当前FrameNode对象对实体FrameNode节点的引用关系
1353    this.rootNode?.dispose();
1354  }
1355
1356  isDisposed() : string {
1357    if (this.rootNode !== null) {
1358      // 查询FrameNode是否解除引用
1359      if (this.rootNode.isDisposed()) {
1360        return 'frameNode isDisposed is true';
1361      }
1362      else {
1363        return 'frameNode isDisposed is false';
1364      }
1365    }
1366    return 'frameNode is null';
1367  }
1368}
1369
1370@Entry
1371@Component
1372struct Index {
1373  @State text: string = ''
1374  private myNodeController: MyNodeController = new MyNodeController();
1375
1376  build() {
1377    Column({ space: 4 }) {
1378      NodeContainer(this.myNodeController)
1379      Button('FrameNode dispose')
1380        .onClick(() => {
1381          this.myNodeController.disposeFrameNode();
1382          this.text = '';
1383        })
1384        .width(200)
1385        .height(50)
1386      Button('FrameNode isDisposed')
1387        .onClick(() => {
1388          this.text = this.myNodeController.isDisposed();
1389        })
1390        .width(200)
1391        .height(50)
1392      Text(this.text)
1393        .fontSize(25)
1394    }
1395    .width('100%')
1396    .height('100%')
1397  }
1398}
1399```
1400
1401## FrameNode的数据懒加载能力
1402
1403提供[NodeAdapter](../reference/apis-arkui/js-apis-arkui-frameNode.md#nodeadapter12)对象替代ArkTS侧的LazyForEach功能,提供自定义节点的数据懒加载功能,实现按需迭代数据。
1404
1405> **说明:**
1406>
1407> 入参不能为负数,入参为负数时不做处理。
1408
1409```ts
1410import { FrameNode, NodeController, NodeAdapter, typeNode } from '@kit.ArkUI';
1411
1412const TEST_TAG: string = "FrameNode";
1413
1414class MyNodeAdapter extends NodeAdapter {
1415  uiContext: UIContext
1416  cachePool: Array<FrameNode> = new Array();
1417  changed: boolean = false
1418  reloadTimes: number = 0;
1419  data: Array<string> = new Array();
1420  hostNode?: FrameNode
1421
1422  constructor(uiContext: UIContext, count: number) {
1423    super();
1424    this.uiContext = uiContext;
1425    this.totalNodeCount = count;
1426    this.loadData();
1427  }
1428
1429  reloadData(count: number): void {
1430    this.reloadTimes++;
1431    NodeAdapter.attachNodeAdapter(this, this.hostNode);
1432    this.totalNodeCount = count;
1433    this.loadData();
1434    this.reloadAllItems();
1435  }
1436
1437  refreshData(): void {
1438    let items = this.getAllAvailableItems()
1439    console.info(TEST_TAG + " get All items:" + items.length);
1440    this.totalNodeCount -= 1;
1441    this.reloadAllItems();
1442  }
1443
1444  detachData(): void {
1445    NodeAdapter.detachNodeAdapter(this.hostNode);
1446    this.reloadTimes = 0;
1447  }
1448
1449  loadData(): void {
1450    for (let i = 0; i < this.totalNodeCount; i++) {
1451      this.data[i] = "Adapter ListItem " + i + " r:" + this.reloadTimes;
1452    }
1453  }
1454
1455  changeData(from: number, count: number): void {
1456    this.changed = !this.changed;
1457    for (let i = 0; i < count; i++) {
1458      let index = i + from;
1459      this.data[index] = "Adapter ListItem " + (this.changed ? "changed:" : "") + index + " r:" + this.reloadTimes;
1460    }
1461    this.reloadItem(from, count);
1462  }
1463
1464  insertData(from: number, count: number): void {
1465    for (let i = 0; i < count; i++) {
1466      let index = i + from;
1467      this.data.splice(index, 0, "Adapter ListItem " + from + "-" + i);
1468    }
1469    this.insertItem(from, count);
1470    this.totalNodeCount += count;
1471    console.info(TEST_TAG + " after insert count:" + this.totalNodeCount);
1472  }
1473
1474  removeData(from: number, count: number): void {
1475    let arr = this.data.splice(from, count);
1476    this.removeItem(from, count);
1477    this.totalNodeCount -= arr.length;
1478    console.info(TEST_TAG + " after remove count:" + this.totalNodeCount);
1479  }
1480
1481  moveData(from: number, to: number): void {
1482    let tmp = this.data.splice(from, 1);
1483    this.data.splice(to, 0, tmp[0]);
1484    this.moveItem(from, to);
1485  }
1486
1487  onAttachToNode(target: FrameNode): void {
1488    console.info(TEST_TAG + " onAttachToNode id:" + target.getUniqueId());
1489    this.hostNode = target;
1490  }
1491
1492  onDetachFromNode(): void {
1493    console.info(TEST_TAG + " onDetachFromNode");
1494  }
1495
1496  onGetChildId(index: number): number {
1497    console.info(TEST_TAG + " onGetChildId:" + index);
1498    return index;
1499  }
1500
1501  onCreateChild(index: number): FrameNode {
1502    console.info(TEST_TAG + " onCreateChild:" + index);
1503    if (this.cachePool.length > 0) {
1504      let cacheNode = this.cachePool.pop();
1505      if (cacheNode !== undefined) {
1506        console.info(TEST_TAG + " onCreateChild reused id:" + cacheNode.getUniqueId());
1507        let text = cacheNode?.getFirstChild();
1508        let textNode = text as typeNode.Text;
1509        textNode?.initialize(this.data[index]).fontSize(20);
1510        return cacheNode;
1511      }
1512    }
1513    console.info(TEST_TAG + " onCreateChild createNew");
1514    let itemNode = typeNode.createNode(this.uiContext, "ListItem");
1515    let textNode = typeNode.createNode(this.uiContext, "Text");
1516    textNode.initialize(this.data[index]).fontSize(20);
1517    itemNode.appendChild(textNode);
1518    return itemNode;
1519  }
1520
1521  onDisposeChild(id: number, node: FrameNode): void {
1522    console.info(TEST_TAG + " onDisposeChild:" + id);
1523    if (this.cachePool.length < 10) {
1524      if (!this.cachePool.includes(node)) {
1525        console.info(TEST_TAG + " caching node id:" + node.getUniqueId());
1526        this.cachePool.push(node);
1527      }
1528    } else {
1529      node.dispose();
1530    }
1531  }
1532
1533  onUpdateChild(id: number, node: FrameNode): void {
1534    let index = id;
1535    let text = node.getFirstChild();
1536    let textNode = text as typeNode.Text;
1537    textNode?.initialize(this.data[index]).fontSize(20);
1538  }
1539}
1540
1541class MyNodeAdapterController extends NodeController {
1542  rootNode: FrameNode | null = null;
1543  nodeAdapter: MyNodeAdapter | null = null;
1544
1545  makeNode(uiContext: UIContext): FrameNode | null {
1546    this.rootNode = new FrameNode(uiContext);
1547    let listNode = typeNode.createNode(uiContext, "List");
1548    listNode.initialize({ space: 3 }).borderWidth(2).borderColor(Color.Black);
1549    this.rootNode.appendChild(listNode);
1550    this.nodeAdapter = new MyNodeAdapter(uiContext, 100);
1551    NodeAdapter.attachNodeAdapter(this.nodeAdapter, listNode);
1552    return this.rootNode;
1553  }
1554}
1555
1556@Entry
1557@Component
1558struct ListNodeTest {
1559  adapterController: MyNodeAdapterController = new MyNodeAdapterController();
1560
1561  build() {
1562    Column() {
1563      Text("ListNode Adapter");
1564      NodeContainer(this.adapterController)
1565        .width(300).height(300)
1566        .borderWidth(1).borderColor(Color.Black);
1567      Row() {
1568        Button("Reload")
1569          .onClick(() => {
1570            this.adapterController.nodeAdapter?.reloadData(50);
1571          })
1572        Button("Change")
1573          .onClick(() => {
1574            this.adapterController.nodeAdapter?.changeData(5, 10)
1575          })
1576        Button("Insert")
1577          .onClick(() => {
1578            this.adapterController.nodeAdapter?.insertData(10, 10);
1579          })
1580      }
1581
1582      Row() {
1583        Button("Remove")
1584          .onClick(() => {
1585            this.adapterController.nodeAdapter?.removeData(10, 10);
1586          })
1587        Button("Move")
1588          .onClick(() => {
1589            this.adapterController.nodeAdapter?.moveData(2, 5);
1590          })
1591        Button("Refresh")
1592          .onClick(() => {
1593            this.adapterController.nodeAdapter?.refreshData();
1594          })
1595        Button("Detach")
1596          .onClick(() => {
1597            this.adapterController.nodeAdapter?.detachData();
1598          })
1599      }
1600    }.borderWidth(1)
1601    .width("100%")
1602  }
1603}
1604```
1605
1606## 查询LazyForEach中的FrameNode节点信息
1607
1608如果FrameNode子节点中包含[LazyForEach](../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)节点,[getChild](../reference/apis-arkui/js-apis-arkui-frameNode.md#getchild15)接口支持指定子节点展开模式[ExpandMode](../reference/apis-arkui/js-apis-arkui-frameNode.md#expandmode15),以不同展开模式获取子节点。
1609
1610当前支持如下子节点展开模式:
1611
1612- ExpandMode.NOT_EXPAND:表示不展开当前FrameNode的子节点。如果FrameNode子节点中包含LazyForEach节点,获取在主节点树上的子节点时,不展开当前FrameNode的子节点。子节点序列号按在主节点树上的子节点计算。
1613- ExpandMode.EXPAND:表示展开当前FrameNode的子节点。如果FrameNode子节点中包含LazyForEach节点,获取任何子节点时,展开当前FrameNode的子节点。子节点序列号按所有子节点计算。
1614- ExpandMode.LAZY_EXPAND:表示按需展开当前FrameNode的子节点。如果FrameNode子节点中包含LazyForEach节点,获取在主节点树上的子节点时,不展开当前FrameNode的子节点;获取不在主节点树上的子节点时,展开当前FrameNode的子节点。子节点序列号按所有子节点计算。
1615
1616可以使用[getFirstChildIndexWithoutExpand](../reference/apis-arkui/js-apis-arkui-frameNode.md#getfirstchildindexwithoutexpand15)和[getLastChildIndexWithoutExpand](../reference/apis-arkui/js-apis-arkui-frameNode.md#getlastchildindexwithoutexpand15)获取当前节点第一个和最后一个在主节点树上的子节点的序列号,其中子节点序列号按所有子节点计算。
1617
1618```ts
1619import { NodeController, FrameNode, UIContext, BuilderNode, ExpandMode, LengthUnit } from '@kit.ArkUI';
1620
1621const TEST_TAG: string = "FrameNode ";
1622
1623// BasicDataSource实现了IDataSource接口,用于管理listener监听,以及通知LazyForEach数据更新
1624class BasicDataSource implements IDataSource {
1625  private listeners: DataChangeListener[] = [];
1626  private originDataArray: string[] = [];
1627
1628  public totalCount(): number {
1629    return 0;
1630  }
1631
1632  public getData(index: number): string {
1633    return this.originDataArray[index];
1634  }
1635
1636  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
1637  registerDataChangeListener(listener: DataChangeListener): void {
1638    if (this.listeners.indexOf(listener) < 0) {
1639      console.info('add listener');
1640      this.listeners.push(listener);
1641    }
1642  }
1643
1644  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
1645  unregisterDataChangeListener(listener: DataChangeListener): void {
1646    const pos = this.listeners.indexOf(listener);
1647    if (pos >= 0) {
1648      console.info('remove listener');
1649      this.listeners.splice(pos, 1);
1650    }
1651  }
1652
1653  // 通知LazyForEach组件需要重载所有子组件
1654  notifyDataReload(): void {
1655    this.listeners.forEach(listener => {
1656      listener.onDataReloaded();
1657    })
1658  }
1659
1660  // 通知LazyForEach组件需要在index对应索引处添加子组件
1661  notifyDataAdd(index: number): void {
1662    this.listeners.forEach(listener => {
1663      listener.onDataAdd(index);
1664      // 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
1665    })
1666  }
1667
1668  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
1669  notifyDataChange(index: number): void {
1670    this.listeners.forEach(listener => {
1671      listener.onDataChange(index);
1672      // 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]);
1673    })
1674  }
1675
1676  // 通知LazyForEach组件需要在index对应索引处删除该子组件
1677  notifyDataDelete(index: number): void {
1678    this.listeners.forEach(listener => {
1679      listener.onDataDelete(index);
1680      // 写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]);
1681    })
1682  }
1683
1684  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
1685  notifyDataMove(from: number, to: number): void {
1686    this.listeners.forEach(listener => {
1687      listener.onDataMove(from, to);
1688      // 写法2:listener.onDatasetChange(
1689      //         [{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}]);
1690    })
1691  }
1692
1693  notifyDatasetChange(operations: DataOperation[]): void {
1694    this.listeners.forEach(listener => {
1695      listener.onDatasetChange(operations);
1696    })
1697  }
1698}
1699
1700class MyDataSource extends BasicDataSource {
1701  private dataArray: string[] = []
1702
1703  public totalCount(): number {
1704    return this.dataArray.length;
1705  }
1706
1707  public getData(index: number): string {
1708    return this.dataArray[index];
1709  }
1710
1711  public addData(index: number, data: string): void {
1712    this.dataArray.splice(index, 0, data);
1713    this.notifyDataAdd(index);
1714  }
1715
1716  public pushData(data: string): void {
1717    this.dataArray.push(data);
1718    this.notifyDataAdd(this.dataArray.length - 1);
1719  }
1720}
1721
1722class Params {
1723  data: MyDataSource | null = null;
1724  scroller: Scroller | null = null;
1725  constructor(data: MyDataSource, scroller: Scroller) {
1726    this.data = data;
1727    this.scroller = scroller;
1728  }
1729}
1730
1731@Builder
1732function buildData(params: Params) {
1733  List({ scroller: params.scroller }) {
1734    LazyForEach(params.data, (item: string) => {
1735      ListItem() {
1736        Column() {
1737          Text(item)
1738            .fontSize(20)
1739            .onAppear(() => {
1740              console.info(TEST_TAG + " node appear: " + item)
1741            })
1742            .backgroundColor(Color.Pink)
1743            .margin({
1744              top: 30,
1745              bottom: 30,
1746              left: 10,
1747              right: 10
1748            })
1749        }
1750      }
1751      .id(item)
1752    }, (item: string) => item)
1753  }
1754  .cachedCount(5)
1755  .listDirection(Axis.Horizontal)
1756}
1757
1758class MyNodeController extends NodeController {
1759  private rootNode: FrameNode | null = null;
1760  private uiContext: UIContext | null = null;
1761  private data: MyDataSource = new MyDataSource();
1762  private scroller: Scroller = new Scroller();
1763
1764  makeNode(uiContext: UIContext): FrameNode | null {
1765    this.uiContext = uiContext;
1766    for (let i = 0; i <= 20; i++) {
1767      this.data.pushData(`N${i}`);
1768    }
1769    const params: Params = new Params(this.data, this.scroller);
1770    const dataNode: BuilderNode<[Params]> = new BuilderNode(uiContext);
1771    dataNode.build(wrapBuilder<[Params]>(buildData), params);
1772    this.rootNode = dataNode.getFrameNode();
1773    const scrollToIndexOptions: ScrollToIndexOptions = {
1774      extraOffset: {
1775        value: 20, unit: LengthUnit.VP
1776      }
1777    };
1778    this.scroller.scrollToIndex(6, true, ScrollAlign.START, scrollToIndexOptions);
1779    return this.rootNode;
1780  }
1781
1782  getFirstChildIndexWithoutExpand() {
1783    console.info(`${TEST_TAG} getFirstChildIndexWithoutExpand: ${this.rootNode!.getFirstChildIndexWithoutExpand()}`);
1784  }
1785
1786  getLastChildIndexWithoutExpand() {
1787    console.info(`${TEST_TAG} getLastChildIndexWithoutExpand: ${this.rootNode!.getLastChildIndexWithoutExpand()}`);
1788  }
1789
1790  getChildWithNotExpand() {
1791    const childNode = this.rootNode!.getChild(3, ExpandMode.NOT_EXPAND);
1792    console.info(TEST_TAG + " getChild(3, ExpandMode.NOT_EXPAND): " + childNode!.getId());
1793    if (childNode!.getId() === "N9") {
1794      console.info(TEST_TAG + " getChild(3, ExpandMode.NOT_EXPAND)  result: success.");
1795    } else {
1796      console.info(TEST_TAG + " getChild(3, ExpandMode.NOT_EXPAND)  result: fail.");
1797    }
1798  }
1799
1800  getChildWithExpand() {
1801    const childNode = this.rootNode!.getChild(3, ExpandMode.EXPAND);
1802    console.info(TEST_TAG + " getChild(3, ExpandMode.EXPAND): " + childNode!.getId());
1803    if (childNode!.getId() === "N3") {
1804      console.info(TEST_TAG + " getChild(3, ExpandMode.EXPAND)  result: success.");
1805    } else {
1806      console.info(TEST_TAG + " getChild(3, ExpandMode.EXPAND)  result: fail.");
1807    }
1808  }
1809
1810  getChildWithLazyExpand() {
1811    const childNode = this.rootNode!.getChild(3, ExpandMode.LAZY_EXPAND);
1812    console.info(TEST_TAG + " getChild(3, ExpandMode.LAZY_EXPAND): " + childNode!.getId());
1813    if (childNode!.getId() === "N3") {
1814      console.info(TEST_TAG + " getChild(3, ExpandMode.LAZY_EXPAND)  result: success.");
1815    } else {
1816      console.info(TEST_TAG + " getChild(3, ExpandMode.LAZY_EXPAND)  result: fail.");
1817    }
1818  }
1819}
1820
1821@Entry
1822@Component
1823struct Index {
1824  private myNodeController: MyNodeController = new MyNodeController();
1825  private scroller: Scroller = new Scroller();
1826
1827  build() {
1828    Scroll(this.scroller) {
1829      Column({ space: 8 }) {
1830        Column() {
1831          Text("This is a NodeContainer.")
1832            .textAlign(TextAlign.Center)
1833            .borderRadius(10)
1834            .backgroundColor(0xFFFFFF)
1835            .width('100%')
1836            .fontSize(16)
1837          NodeContainer(this.myNodeController)
1838            .borderWidth(1)
1839            .width(300)
1840            .height(100)
1841        }
1842
1843        Button("getFirstChildIndexWithoutExpand")
1844          .width(300)
1845          .onClick(() => {
1846            this.myNodeController.getFirstChildIndexWithoutExpand();
1847          })
1848        Button("getLastChildIndexWithoutExpand")
1849          .width(300)
1850          .onClick(() => {
1851            this.myNodeController.getLastChildIndexWithoutExpand();
1852          })
1853        Button("getChildWithNotExpand")
1854          .width(300)
1855          .onClick(() => {
1856            this.myNodeController.getChildWithNotExpand();
1857          })
1858        Button("getChildWithExpand")
1859          .width(300)
1860          .onClick(() => {
1861            this.myNodeController.getChildWithExpand();
1862          })
1863        Button("getChildWithLazyExpand")
1864          .width(300)
1865          .onClick(() => {
1866            this.myNodeController.getChildWithLazyExpand();
1867          })
1868      }
1869      .width("100%")
1870    }
1871    .scrollable(ScrollDirection.Vertical) // 滚动方向纵向
1872  }
1873}
1874```