1# Custom Component Node (FrameNode)
2
3## Overview
4
5For third-party frameworks with custom frontend definitions, such as those in JSON, XML, or a DOM tree, a conversion from the specific DSL into ArkUI's declarative descriptions is necessary. The following figure shows the mapping between a frontend framework defined by JSON and the ArkUI declarative description.
6
7![en-us_image_frame-node01](figures/frame-node01.png)
8
9The aforementioned conversion process, which relies on additional data-driven bindings to the [Builder](../quick-start/arkts-builder.md), is complex and can be performance-intensive. Such frameworks typically leverage ArkUI's layout and event handling, as well as basic node operations and customization capabilities. While most components are customized, some built-in components are needed for mixed display. This is where [FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md) comes into the picture. In the example shown below, while the custom method of **FrameNode** is used for drawing, a built-in component **Column** and its child component **Text** are mounted to the root FrameNode through **BuilderNode**, thereby achieving mixed display.
10
11![en-us_image_frame-node02](figures/frame-node02.png)
12
13FrameNode represents an entity node in the component tree. When used with custom placeholder containers such as [NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md), it allows for mounting a custom node tree in the containers. The node tree supports dynamic operations, including node addition, modification, and removal. Basic FrameNodes enable universal attribute setting, event callback setting, and full customization for measurement, layout, and rendering.
14
15Moreover, the ArkUI framework enables obtaining and traversing proxy FrameNode objects for built-in components, known as proxy nodes, which facilitate UI tree traversal and allow for obtaining specific information about built-in components or registering additional event listeners.
16
17## Creating and Removing Nodes
18
19You can create and remove nodes with **FrameNode**. You can create a custom instance of **FrameNode** using its constructor, and the instance thereby created corresponds to an entity node. You can use the [dispose](../reference/apis-arkui/js-apis-arkui-frameNode.md#dispose12) API in **FrameNode** to break the binding with the entity node.
20
21> **NOTE**
22>
23> - A valid **UIContext** object is required for creating a FrameNode. If no **UIContext** object is provided or if the provided one is invalid, an exception will be thrown during node creation.
24>
25> - Maintain UI context consistency for custom placeholder components to prevent display issues.
26>
27> - **FrameNode** objects are subject to garbage collection (GC) if not retained.
28
29## Checking Whether a Node is Modifiable
30
31Use [isModifiable](../reference/apis-arkui/js-apis-arkui-frameNode.md#ismodifiable12) to check whether the current node is a proxy for a built-in component. If a FrameNode serves as a proxy, it cannot be modified, which means you cannot change its properties or the structure of its child nodes.
32
33## Obtaining the Corresponding RenderNode
34
35Use the [getRenderNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#getrendernode) API to obtain the RenderNode associated with the FrameNode. You can then perform operations on the obtained RenderNode object to dynamically modify the drawing-related properties of the FrameNode. For details about the properties that can be modified, see [RenderNode](arkts-user-defined-arktsNode-renderNode.md).
36
37> **NOTE**
38>
39> - You cannot obtain the RenderNode for a built-in component's proxy FrameNode.
40>
41> - In **BuilderNode**, you can use [getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode) to get the FrameNode object, and then use **getRenderNode** to obtain the RenderNode object of the corresponding root node.
42
43## Operating the Node Tree
44
45With **FrameNode**, you can add, delete, query, and modify nodes, thereby changing the subtree structure of non-proxy nodes; you can also query the parent-child relationships to obtain the results.
46
47> **NOTE**
48>
49> Illegal operations for adding, deleting, or modifying nodes result in exceptions.
50>
51> Proxy nodes obtained through queries can only be used to obtain node information and cannot modify node properties; they do not hold the component entity nodes, meaning they do not affect the lifecycle of the corresponding nodes.
52>
53> Node queries only return UI-related nodes and do not include syntax nodes.
54>
55> In scenarios using custom components, you may query and obtain newly added nodes of the custom components, with the node type being **\_\_Common\_\_**.
56
57```ts
58import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
59import { BusinessError } from '@kit.BasicServicesKit';
60
61const TEST_TAG: string = "FrameNode"
62
63class Params {
64  text: string = "this is a text"
65}
66
67@Builder
68function buttonBuilder(params: Params) {
69  Column({ space: 10 }) {
70    Button(params.text)
71      .fontSize(12)
72      .borderRadius(8)
73      .borderWidth(2)
74      .backgroundColor(Color.Orange)
75
76    Button(params.text)
77      .fontSize(12)
78      .borderRadius(8)
79      .borderWidth(2)
80      .backgroundColor(Color.Pink)
81  }
82}
83
84class MyNodeController extends NodeController {
85  public buttonNode: BuilderNode<[Params]> | null = null;
86  public frameNode: FrameNode | null = null;
87  public childList: Array<FrameNode> = new Array<FrameNode>();
88  public rootNode: FrameNode | null = null;
89  private uiContext: UIContext | null = null;
90  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(buttonBuilder);
91
92  makeNode(uiContext: UIContext): FrameNode | null {
93    this.uiContext = uiContext;
94    if (this.rootNode == null) {
95      this.rootNode = new FrameNode(uiContext);
96      this.rootNode.commonAttribute
97        .width("50%")
98        .height(100)
99        .borderWidth(1)
100        .backgroundColor(Color.Gray)
101    }
102
103    if (this.frameNode == null) {
104      this.frameNode = new FrameNode(uiContext);
105      this.frameNode.commonAttribute
106        .width("100%")
107        .height(50)
108        .borderWidth(1)
109        .position({ x: 200, y: 0 })
110        .backgroundColor(Color.Pink);
111      this.rootNode.appendChild(this.frameNode);
112    }
113    if (this.buttonNode == null) {
114      this.buttonNode = new BuilderNode<[Params]>(uiContext);
115      this.buttonNode.build(this.wrapBuilder, { text: "This is a Button" })
116      this.rootNode.appendChild(this.buttonNode.getFrameNode())
117    }
118    return this.rootNode;
119  }
120
121  operationFrameNodeWithFrameNode(frameNode: FrameNode | undefined | null) {
122    if (frameNode) {
123      console.log(TEST_TAG + " get ArkTSNode success.")
124      console.log(TEST_TAG + " check rootNode whether is modifiable " + frameNode.isModifiable());
125    }
126    if (this.uiContext) {
127      let frameNode1 = new FrameNode(this.uiContext);
128      let frameNode2 = new FrameNode(this.uiContext);
129      frameNode1.commonAttribute.size({ width: 50, height: 50 })
130        .backgroundColor(Color.Black)
131        .position({ x: 50, y: 60 })
132      frameNode2.commonAttribute.size({ width: 50, height: 50 })
133        .backgroundColor(Color.Orange)
134        .position({ x: 120, y: 60 })
135      try {
136        frameNode?.appendChild(frameNode1);
137        console.log(TEST_TAG + " appendChild success ");
138      } catch (err) {
139        console.log(TEST_TAG + " appendChild fail :" + (err as BusinessError).code + " : " + (err as BusinessError).message);
140      }
141      try {
142        frameNode?.insertChildAfter(frameNode2, null);
143        console.log(TEST_TAG + " insertChildAfter success ");
144      } catch (err) {
145        console.log(TEST_TAG + " insertChildAfter fail : " + (err as BusinessError).code + " : " + (err as BusinessError).message);
146      }
147      setTimeout(() => {
148        try {
149          frameNode?.removeChild(frameNode?.getChild(0))
150          console.log(TEST_TAG + " removeChild success ");
151        } catch (err) {
152          console.log(TEST_TAG + " removeChild fail : " + (err as BusinessError).code + " : " + (err as BusinessError).message);
153        }
154      }, 2000)
155      setTimeout(() => {
156        try {
157          frameNode?.clearChildren();
158          console.log(TEST_TAG + " clearChildren success ");
159        } catch (err) {
160          console.log(TEST_TAG + " clearChildren fail : " + (err as BusinessError).code + " : " + (err as BusinessError).message);
161        }
162      }, 4000)
163    }
164  }
165
166  testInterfaceAboutSearch(frameNode: FrameNode | undefined | null): string {
167    let result: string = "";
168    if (frameNode) {
169      result = result + `current node is ${frameNode.getNodeType()} \n`;
170      result = result + `parent node is ${frameNode.getParent()?.getNodeType()} \n`;
171      result = result + `child count is ${frameNode.getChildrenCount()} \n`;
172      result = result + `first child node is ${frameNode.getFirstChild()?.getNodeType()} \n`;
173      result = result + `second child node is ${frameNode.getChild(1)?.getNodeType()} \n`;
174      result = result + `previousSibling node is ${frameNode.getPreviousSibling()?.getNodeType()} \n`;
175      result = result + `nextSibling node is ${frameNode.getNextSibling()?.getNodeType()} \n`;
176    }
177    return result;
178  }
179
180  checkAppendChild(parent: FrameNode | undefined | null, child: FrameNode | undefined | null) {
181    try {
182      if (parent && child) {
183        parent.appendChild(child);
184        console.log(TEST_TAG + " appendChild success ");
185      }
186    } catch (err) {
187      console.log(TEST_TAG + " appendChild fail : " + (err as BusinessError).code + " : " + (err as BusinessError).message);
188    }
189  }
190}
191
192@Entry
193@Component
194struct Index {
195  @State index: number = 0;
196  @State result: string = ""
197  private myNodeController: MyNodeController = new MyNodeController();
198
199  build() {
200    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
201      List({ space: 20, initialIndex: 0 }) {
202        ListItem() {
203          Column({ space: 5 }) {
204            Text("Verify the add, delete, and modify features of the FrameNode")
205            Button("Operate Custom FrameNode")
206              .fontSize(16)
207              .width(400)
208              .onClick(() => {
209                // Add, delete, and modify FrameNode child nodes, which is properly implemented.
210                this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.frameNode);
211              })
212            Button("Operate Proxy Node in BuilderNode")
213              .fontSize(16)
214              .width(400)
215              .onClick(() => {
216                // Add, delete, and modify the BuilderNode proxy node to generate an exception.
217                this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.buttonNode?.getFrameNode());
218              })
219            Button("Operate Proxy Node in Built-in Component")
220              .fontSize(16)
221              .width(400)
222              .onClick(() => {
223                // Add, delete, and modify the proxy node to generate an exception.
224                this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.rootNode?.getParent());
225              })
226          }
227        }
228
229        ListItem() {
230          Column({ space: 5 }) {
231            Text("Verify the feature to add subnodes to FrameNode")
232            Button("Add BuilderNode Proxy Node")
233              .fontSize(16)
234              .width(400)
235              .onClick(() => {
236                let buttonNode = new BuilderNode<[Params]>(this.getUIContext());
237                buttonNode.build(wrapBuilder<[Params]>(buttonBuilder), { text: "BUTTON" })
238                this.myNodeController.checkAppendChild(this.myNodeController?.frameNode, buttonNode?.getFrameNode());
239              })
240            Button("Add Built-in Component Proxy Node")
241              .fontSize(16)
242              .width(400)
243              .onClick(() => {
244                this.myNodeController.checkAppendChild(this.myNodeController?.frameNode, this.myNodeController?.rootNode?.getParent());
245              })
246            Button("Add Custom Node with Existing Parent Node")
247              .fontSize(16)
248              .width(400)
249              .onClick(() => {
250                this.myNodeController.checkAppendChild(this.myNodeController?.frameNode, this.myNodeController?.rootNode);
251              })
252          }
253        }
254
255        ListItem() {
256          Column({ space: 5 }) {
257            Text("Verify the query feature of the FrameNode")
258            Button("Operate Custom FrameNode")
259              .fontSize(16)
260              .width(400)
261              .onClick(() => {
262                // Query the FrameNode. The current node is a child of the NodeContainer.
263                this.result = this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.rootNode);
264                setTimeout(() => {
265                  // Query the FrameNode. The current node is the first child node under rootNode.
266                  this.result = this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.frameNode);
267                }, 2000)
268              })
269            Button("Operate Proxy Node in BuilderNode")
270              .fontSize(16)
271              .width(400)
272              .onClick(() => {
273                // Query the BuilderNode proxy nodes. The current node is the Column node within BuilderNode.
274                this.result = this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.buttonNode?.getFrameNode());
275              })
276            Button("Operate Proxy Node in Built-in Component")
277              .fontSize(16)
278              .width(400)
279              .onClick(() => {
280                // Query the proxy node. The current node is the NodeContainer.
281                this.result = this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.rootNode?.getParent());
282              })
283          }
284        }
285      }.height("50%")
286
287      Text(`Result: \n${this.result}`)
288        .fontSize(16)
289        .width(400)
290        .height(200)
291        .padding(30)
292        .borderWidth(1)
293      Column() {
294        Text("This is a NodeContainer.")
295          .textAlign(TextAlign.Center)
296          .borderRadius(10)
297          .backgroundColor(0xFFFFFF)
298          .width('100%')
299          .fontSize(16)
300        NodeContainer(this.myNodeController)
301          .borderWidth(1)
302          .width(400)
303          .height(150)
304      }
305    }
306    .padding({ left: 35, right: 35, top: 35, bottom: 35 })
307    .width("100%")
308    .height("100%")
309  }
310}
311```
312
313## Setting Universal Attributes and Event Callbacks
314
315Use the [commonAttribute](../reference/apis-arkui/js-apis-arkui-frameNode.md#commonattribute12) and [commonEvent](../reference/apis-arkui/js-apis-arkui-frameNode.md#commonevent12) objects to set the [universal attributes](../reference/apis-arkui/arkui-ts/ts-universal-attributes-size.md) and [event callbacks](../reference/apis-arkui/arkui-ts/ts-uicommonevent.md), respectively.
316
317> **NOTE**
318>
319> - Proxy node attributes are immutable. Therefore, **commonAttribute** is ineffective on proxy nodes.
320>
321> - The custom basic events that you define run in parallel with the events predefined in the built-in components, without overriding them. When two event callbacks are set, the built-in component event callback is prioritized.
322
323```ts
324import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'
325
326class Params {
327  text: string = "this is a text"
328}
329
330@Builder
331function buttonBuilder(params: Params) {
332  Button(params.text)
333    .fontSize(12)
334    .borderRadius(8)
335    .borderWidth(2)
336    .backgroundColor(Color.Orange)
337    .onClick((event: ClickEvent) => {
338      console.log(`Button ${JSON.stringify(event)}`);
339    })
340}
341
342class MyNodeController extends NodeController {
343  public buttonNode: BuilderNode<[Params]> | null = null;
344  public frameNode: FrameNode | null = null;
345  public rootNode: FrameNode | null = null;
346  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(buttonBuilder);
347
348  makeNode(uiContext: UIContext): FrameNode | null {
349    if (this.rootNode == null) {
350      this.rootNode = new FrameNode(uiContext);
351      // Modify the attributes of rootNode, which is a custom FrameNode, and the changes take effect.
352      this.rootNode.commonAttribute
353        .width("100%")
354        .height(100)
355        .borderWidth(1)
356        .backgroundColor(Color.Gray)
357    }
358
359    if (this.frameNode == null) {
360      this.frameNode = new FrameNode(uiContext);
361      // Modify the attributes of frameNode, which is a custom FrameNode, and the changes take effect.
362      this.frameNode.commonAttribute
363        .width("50%")
364        .height(50)
365        .borderWidth(1)
366        .backgroundColor(Color.Pink);
367      this.rootNode.appendChild(this.frameNode);
368    }
369    if (this.buttonNode == null) {
370      this.buttonNode = new BuilderNode<[Params]>(uiContext);
371      this.buttonNode.build(this.wrapBuilder, { text: "This is a Button" })
372      // Modify the attributes of the FrameNode obtained from BuilderNode, which is not a custom FrameNode, and the changes do not take effect.
373      this.buttonNode?.getFrameNode()?.commonAttribute.position({ x: 100, y: 100 })
374      this.rootNode.appendChild(this.buttonNode.getFrameNode())
375    }
376    return this.rootNode;
377  }
378
379  modifyNode(frameNode: FrameNode | null | undefined, sizeValue: SizeOptions, positionValue: Position) {
380    if (frameNode) {
381      frameNode.commonAttribute.size(sizeValue).position(positionValue);
382    }
383  }
384
385  addClickEvent(frameNode: FrameNode | null | undefined) {
386    if (frameNode) {
387      frameNode.commonEvent.setOnClick((event: ClickEvent) => {
388        console.log(`FrameNode ${JSON.stringify(event)}`);
389      })
390    }
391  }
392}
393
394@Entry
395@Component
396struct Index {
397  private myNodeController: MyNodeController = new MyNodeController();
398
399  build() {
400    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
401      Column({ space: 10 }) {
402        Text("Modify the universal node attributes: width and height.")
403        Button("modify ArkTS-FrameNode")
404          .onClick(() => {
405            // The object obtained is the FrameNode created on the current page, which can be modified. That is, the size and position of the node can be changed.
406            console.log("Check whether the node can be modified " + this.myNodeController?.frameNode
407            ?.isModifiable());
408            this.myNodeController.modifyNode(this.myNodeController?.frameNode, { width: 150, height: 100 }, {
409              x: 100,
410              y: 0
411            })
412          })
413        Button("modify FrameNode get by BuilderNode")
414          .onClick(() => {
415            // The object obtained is the root node of the BuilderNode on the current page, which cannot be modified. That is, the size and position of the node remain unchanged.
416            console.log("Check the weather the node can be modified " + this.myNodeController?.buttonNode?.getFrameNode()
417            ?.isModifiable());
418            this.myNodeController.modifyNode(this.myNodeController?.buttonNode?.getFrameNode(), {
419              width: 100,
420              height: 100
421            }, { x: 50, y: 50 })
422          })
423        Button("modify proxyFrameNode get by search")
424          .onClick(() => {
425            // The rootNode object calling getParent() obtains the NodeContainer node on the current page, which cannot be modified. That is, the size and position of the node remain unchanged.
426            console.log("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
427            ?.isModifiable());
428            this.myNodeController.modifyNode(this.myNodeController?.rootNode?.getParent(), {
429              width: 500,
430              height: 500
431            }, {
432              x: 0,
433              y: 0
434            })
435          })
436      }.padding({ left: 35, right: 35, top: 35, bottom: 35 })
437
438      Column({ space: 10 }) {
439        Text("Modify the node click event.")
440        Button("add click event to ArkTS-FrameNode")
441          .onClick(() => {
442            // The object obtained is the FrameNode created on the current page, to which click events can be added.
443            // The added click event participates in event competition, meaning the click event will be consumed by this node and will no longer bubble up to the parent component.
444            console.log("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
445            ?.isModifiable());
446            this.myNodeController.addClickEvent(this.myNodeController?.frameNode)
447          })
448        Button("add click event to FrameNode get by BuilderNode")
449          .onClick(() => {
450            // The object obtained is the root node of the BuilderNode on the current page, to which click events can be added.
451            // When the button is clicked, the click event callback set through the built-in component API is called first, followed by the click listener added through commonEvent.
452            console.log("Check the weather the node can be modified " + this.myNodeController?.buttonNode?.getFrameNode()
453            ?.isModifiable());
454            this.myNodeController.addClickEvent(this.myNodeController?.buttonNode?.getFrameNode())
455          })
456        Button("add click event to proxyFrameNode get by search")
457          .onClick(() => {
458            // The rootNode object calling getParent() obtains the NodeContainer node on the current page, to which click events can be added.
459            console.log("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
460            ?.isModifiable());
461            this.myNodeController.addClickEvent(this.myNodeController?.rootNode?.getParent());
462          })
463      }.padding({ left: 35, right: 35, top: 35, bottom: 35 })
464
465      NodeContainer(this.myNodeController)
466        .borderWidth(1)
467        .width("100%")
468        .height(100)
469        .onClick((event: ClickEvent) => {
470          console.log(`NodeContainer ${JSON.stringify(event)}`);
471        })
472    }
473    .padding({ left: 35, right: 35, top: 35, bottom: 35 })
474    .width("100%")
475    .height("100%")
476  }
477}
478```
479
480## Implementing Custom Measurement, Layout, and Drawing
481
482By overriding the [onDraw](../reference/apis-arkui/js-apis-arkui-frameNode.md#ondraw12) API, you can customize the drawing content of the FrameNode. Use the [invalidate](../reference/apis-arkui/js-apis-arkui-frameNode.md#invalidate12) API to manually trigger a redraw of the node.
483
484By overriding the [onMeasure](../reference/apis-arkui/js-apis-arkui-frameNode.md#onmeasure12) API, you can customize how the FrameNode measures its size. Use [measure](../reference/apis-arkui/js-apis-arkui-frameNode.md#measure12) to proactively pass layout constraints to initiate a remeasurement.
485
486By overriding the [onLayout](../reference/apis-arkui/js-apis-arkui-frameNode.md#onlayout12) API, you can customize the layout of the FrameNode. Use [layout](../reference/apis-arkui/js-apis-arkui-frameNode.md#layout12) to proactively pass position information and initiate a re-layout.
487
488Use [setNeedsLayout](../reference/apis-arkui/js-apis-arkui-frameNode.md#setneedslayout12) to mark the current node and trigger a re-layout in the next frame.
489
490> **NOTE**
491>
492> - After a node is disposed and unbound, the FrameNode no longer represents an entity node. In this case, the **invalidate** call cannot update the previously bound node.
493>
494> - Custom drawings made through the **onDraw** API cannot exceed the component's size.
495
496```ts
497import { DrawContext, FrameNode, NodeController, Position, Size, UIContext, LayoutConstraint } from '@kit.ArkUI';
498import { drawing } from '@kit.ArkGraphics2D';
499
500function GetChildLayoutConstraint(constraint: LayoutConstraint, child: FrameNode): LayoutConstraint {
501  const size = child.getUserConfigSize();
502  const width = Math.max(
503    Math.min(constraint.maxSize.width, size.width.value),
504    constraint.minSize.width
505  );
506  const height = Math.max(
507    Math.min(constraint.maxSize.height, size.height.value),
508    constraint.minSize.height
509  );
510  const finalSize: Size = { width, height };
511  const res: LayoutConstraint = {
512    maxSize: finalSize,
513    minSize: finalSize,
514    percentReference: finalSize
515  };
516
517  return res;
518}
519
520class MyFrameNode extends FrameNode {
521  public width: number = 100;
522  public offsetY: number = 0;
523  private space: number = 1;
524
525  onMeasure(constraint: LayoutConstraint): void {
526    let sizeRes: Size = { width: vp2px(100), height: vp2px(100) };
527    for (let i = 0;i < this.getChildrenCount(); i++) {
528      let child = this.getChild(i);
529      if (child) {
530        let childConstraint = GetChildLayoutConstraint(constraint, child);
531        child.measure(childConstraint);
532        let size = child.getMeasuredSize();
533        sizeRes.height += size.height + this.space;
534        sizeRes.width = Math.max(sizeRes.width, size.width);
535      }
536    }
537    this.setMeasuredSize(sizeRes);
538  }
539
540  onLayout(position: Position): void {
541    for (let i = 0;i < this.getChildrenCount(); i++) {
542      let child = this.getChild(i);
543      if (child) {
544        child.layout({
545          x: vp2px(100),
546          y: vp2px(this.offsetY)
547        });
548        let layoutPosition = child.getLayoutPosition();
549        console.log("child position:" + JSON.stringify(layoutPosition));
550      }
551    }
552    this.setLayoutPosition(position);
553  }
554
555  onDraw(context: DrawContext) {
556    const canvas = context.canvas;
557    const pen = new drawing.Pen();
558    pen.setStrokeWidth(15);
559    pen.setColor({ alpha: 255, red: 255, green: 0, blue: 0 });
560    canvas.attachPen(pen);
561    canvas.drawRect({
562      left: 50,
563      right: this.width + 50,
564      top: 50,
565      bottom: this.width + 50,
566    });
567    canvas.detachPen();
568  }
569
570  addWidth() {
571    this.width = (this.width + 10) % 50 + 100;
572  }
573}
574
575class MyNodeController extends NodeController {
576  public rootNode: MyFrameNode | null = null;
577
578  makeNode(context: UIContext): FrameNode | null {
579    this.rootNode = new MyFrameNode(context);
580    this.rootNode?.commonAttribute?.size({ width: 100, height: 100 }).backgroundColor(Color.Green);
581    let frameNode: FrameNode = new FrameNode(context);
582    this.rootNode.appendChild(frameNode);
583    frameNode.commonAttribute.width(10).height(10).backgroundColor(Color.Pink);
584    return this.rootNode;
585  }
586}
587
588@Entry
589@Component
590struct Index {
591  private nodeController: MyNodeController = new MyNodeController();
592
593  build() {
594    Row() {
595      Column() {
596        NodeContainer(this.nodeController)
597          .width('100%')
598          .height(200)
599          .backgroundColor('#FFF0F0F0')
600        Button('Invalidate')
601          .margin(10)
602          .onClick(() => {
603            this.nodeController?.rootNode?.addWidth();
604            this.nodeController?.rootNode?.invalidate();
605          })
606        Button('UpdateLayout')
607          .onClick(() => {
608            let node = this.nodeController.rootNode;
609            node!.offsetY = (node!.offsetY + 10) % 110;
610            this.nodeController?.rootNode?.setNeedsLayout();
611          })
612      }
613      .width('100%')
614      .height('100%')
615    }
616    .height('100%')
617  }
618}
619```
620
621## Searching for Nodes and Obtaining Basic Information
622
623**FrameNode** provides APIs for obtaining basic information about an entity node. For details about the returned information, see the FrameNode API documentation.
624
625To obtain a FrameNode, use any of the following methods:
626
6271. Use [getFrameNodeById](../reference/apis-arkui/js-apis-arkui-UIContext.md#getframenodebyid12).
628
6292. Use [getFrameNodeByUniqueId](../reference/apis-arkui/js-apis-arkui-UIContext.md#getframenodebyuniqueid12).
630
6313. Use an [observer](../reference/apis-arkui/js-apis-arkui-observer.md).
632
633> **NOTE**
634>
635> 1. Currently, the following information can be obtained:
636>
637> - Node size: [getMeasuredSize](../reference/apis-arkui/js-apis-arkui-frameNode.md#getmeasuredsize12), [getUserConfigSize](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuserconfigsize12)
638>
639> - Layout information: [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)
640>
641> - Node information: [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)
642>
643> 2. UINode-type nodes, such as JsView nodes, [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), and **if/else** components, cannot be obtained.
644
645## Obtaining Node Position Offset Information
646
647**FrameNode** provides APIs to obtain the position offsets of nodes relative to the window, parent component, and the screen: [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), [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).
648
649```ts
650import { NodeController, FrameNode, UIContext } from '@kit.ArkUI';
651
652const TEST_TAG : string = "FrameNode"
653class MyNodeController extends NodeController {
654  public frameNode: FrameNode | null = null;
655  private rootNode: FrameNode | null = null;
656  makeNode(uiContext: UIContext): FrameNode | null {
657    this.rootNode = new FrameNode(uiContext);
658    this.frameNode =  new FrameNode(uiContext);
659    this.rootNode.appendChild(this.frameNode);
660    return this.rootNode;
661  }
662  getPositionToWindow()
663  {
664    let positionToWindow = this.rootNode?.getPositionToWindow(); // Obtain the position offset of the FrameNode relative to the window.
665    console.log(TEST_TAG + JSON.stringify(positionToWindow));
666  }
667  getPositionToParent()
668  {
669    let positionToParent = this.rootNode?.getPositionToParent(); // Obtain the position offset of the FrameNode relative to the parent component.
670    console.log(TEST_TAG + JSON.stringify(positionToParent));
671  }
672  getPositionToScreen()
673  {
674    let positionToScreen = this.rootNode?.getPositionToScreen(); // Obtain the position offset of the FrameNode relative to the screen.
675    console.log(TEST_TAG + JSON.stringify(positionToScreen));
676  }
677  getPositionToWindowWithTransform()
678  {
679    let positionToWindowWithTransform = this.rootNode?.getPositionToWindowWithTransform(); // Obtain the position offset of the FrameNode relative to the window with drawing attributes.
680    console.log(TEST_TAG + JSON.stringify(positionToWindowWithTransform));
681  }
682  getPositionToParentWithTransform()
683  {
684    let positionToParentWithTransform = this.rootNode?.getPositionToParentWithTransform(); // Obtain the position offset of the FrameNode relative to the parent component with drawing attributes.
685    console.log(TEST_TAG + JSON.stringify(positionToParentWithTransform));
686  }
687  getPositionToScreenWithTransform()
688  {
689    let positionToScreenWithTransform = this.rootNode?.getPositionToScreenWithTransform(); // Obtain the position offset of the FrameNode relative to the screen with drawing attributes.
690    console.log(TEST_TAG + JSON.stringify(positionToScreenWithTransform));
691  }
692}
693
694@Entry
695@Component
696struct Index {
697  private myNodeController: MyNodeController = new MyNodeController();
698  build() {
699    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
700      Button("getPositionToWindow")
701        .width(300)
702        .onClick(()=>{
703          this.myNodeController.getPositionToWindow();
704        })
705      Button("getPositionToParent")
706        .width(300)
707        .onClick(()=>{
708          this.myNodeController.getPositionToParent();
709        })
710      Button("getPositionToScreen")
711        .width(300)
712        .onClick(()=>{
713          this.myNodeController.getPositionToScreen();
714        })
715      Button("getPositionToParentWithTransform")
716        .width(300)
717        .onClick(()=>{
718          this.myNodeController.getPositionToParentWithTransform();
719        })
720      Button("getPositionToWindowWithTransform")
721        .width(300)
722        .onClick(()=>{
723          this.myNodeController.getPositionToWindowWithTransform();
724        })
725      Button("getPositionToScreenWithTransform")
726        .width(300)
727        .onClick(()=>{
728          this.myNodeController.getPositionToScreenWithTransform();
729        })
730      Column(){
731        Text("This is a NodeContainer.")
732          .textAlign(TextAlign.Center).borderRadius(10).backgroundColor(0xFFFFFF)
733          .width('100%').fontSize(16)
734        NodeContainer(this.myNodeController)
735          .borderWidth(1)
736          .width(300)
737          .height(100)
738      }
739    }
740    .padding({ left: 35, right: 35, top: 35, bottom: 35 })
741    .width("100%")
742    .height("100%")
743  }
744}
745```
746
747## Creating a FrameNode of a Specific Type Using typeNode
748
749By creating a FrameNode of a specific type using **typeNode**, you can obtain user-set attribute information through attribute obtaining APIs.
750
751```ts
752import { NodeController, FrameNode, UIContext, BuilderNode, typeNode } from '@kit.ArkUI';
753import { BusinessError } from '@kit.BasicServicesKit';
754
755class Params {
756  text: string = "";
757
758  constructor(text: string) {
759    this.text = text;
760  }
761}
762
763@Builder
764function buildText(params: Params) {
765  Column() {
766    Text(params.text)
767      .id("buildText")
768      .border({width:1})
769      .padding(1)
770      .fontSize(25)
771      .fontWeight(FontWeight.Bold)
772      .margin({ top: 10 })
773      .visibility(Visibility.Visible)
774      .opacity(0.7)
775      .customProperty("key1", "value1")
776      .width(300)
777  }
778}
779
780const TEST_TAG : string = "FrameNode"
781class MyNodeController extends NodeController {
782  public frameNode: typeNode.Column | null = null;
783  public uiContext: UIContext | undefined = undefined;
784  private rootNode: FrameNode | null = null;
785  private textNode: BuilderNode<[Params]> | null = null;
786  public textTypeNode: typeNode.Text | null = null;
787  private message: string = "DEFAULT";
788
789  makeNode(uiContext: UIContext): FrameNode | null {
790    this.rootNode = new FrameNode(uiContext);
791    this.uiContext = uiContext;
792    this.frameNode = typeNode.createNode(uiContext, "Column");
793    this.frameNode.attribute
794      .width("100%")
795      .height("100%")
796    this.rootNode.appendChild(this.frameNode);
797    this.textNode = new BuilderNode(uiContext);
798    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
799    this.frameNode.appendChild(this.textNode.getFrameNode());
800    this.textTypeNode = typeNode.createNode(uiContext, "Text");
801    this.textTypeNode.initialize("textTypeNode")
802      .fontSize(25)
803      .visibility(Visibility.Visible)
804      .id("textTypeNode")
805    this.frameNode.appendChild(this.textTypeNode);
806    return this.rootNode;
807  }
808  removeChild(frameNode: FrameNode)
809  {
810    let parent = frameNode.getParent();
811    if (parent) {
812      parent.removeChild(frameNode);
813
814    }
815  }
816  getUserConfigBorderWidth(frameNode: FrameNode)
817  {
818    let userConfigBorderWidth = frameNode?.getUserConfigBorderWidth(); // Obtain the border width set by the user.
819    console.log(TEST_TAG + JSON.stringify(userConfigBorderWidth));
820  }
821  getUserConfigPadding(frameNode: FrameNode)
822  {
823    let userConfigPadding = frameNode?.getUserConfigPadding(); // Obtain the padding set by the user.
824    console.log(TEST_TAG + JSON.stringify(userConfigPadding));
825  }
826  getUserConfigMargin(frameNode: FrameNode)
827  {
828    let userConfigMargin = frameNode?.getUserConfigMargin(); // Obtain the margin set by the user.
829    console.log(TEST_TAG + JSON.stringify(userConfigMargin));
830  }
831  getUserConfigSize(frameNode: FrameNode)
832  {
833    let userConfigSize = frameNode?.getUserConfigSize(); // Obtain the width and height set by the user.
834    console.log(TEST_TAG + JSON.stringify(userConfigSize));
835  }
836  getId(frameNode: FrameNode)
837  {
838    let id = frameNode?.getId(); // Obtain the node ID set by the user.
839    console.log(TEST_TAG + id);
840  }
841  getUniqueId(frameNode: FrameNode)
842  {
843    let uniqueId = frameNode?.getUniqueId(); // Obtain the unique node ID allocated by the system.
844    console.log(TEST_TAG + uniqueId);
845  }
846  getNodeType(frameNode: FrameNode)
847  {
848    let nodeType = frameNode?.getNodeType(); // Obtain the node type.
849    console.log(TEST_TAG + nodeType);
850  }
851  getOpacity(frameNode: FrameNode)
852  {
853    let opacity = frameNode?.getOpacity(); // Obtain the node opacity.
854    console.log(TEST_TAG + JSON.stringify(opacity));
855  }
856  isVisible(frameNode: FrameNode)
857  {
858    let visible = frameNode?.isVisible(); // Obtain whether the node is visible.
859    console.log(TEST_TAG + JSON.stringify(visible));
860  }
861  isClipToFrame(frameNode: FrameNode)
862  {
863    let clipToFrame = frameNode?.isClipToFrame(); // Obtain whether the node is clipped to the component area.
864    console.log(TEST_TAG + JSON.stringify(clipToFrame));
865  }
866  isAttached(frameNode: FrameNode)
867  {
868    let attached = frameNode?.isAttached(); // Obtain whether a node is mounted to the main node tree.
869    console.log(TEST_TAG + JSON.stringify(attached));
870  }
871  getInspectorInfo(frameNode: FrameNode)
872  {
873    let inspectorInfo = frameNode?.getInspectorInfo(); // Obtain the structure information of the node.
874    console.log(TEST_TAG + JSON.stringify(inspectorInfo));
875  }
876}
877
878@Entry
879@Component
880struct Index {
881  private myNodeController: MyNodeController = new MyNodeController();
882  @State index : number = 0;
883  build() {
884    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
885      Column(){
886        Text("This is a NodeContainer.")
887          .textAlign(TextAlign.Center).borderRadius(10).backgroundColor(0xFFFFFF)
888          .width('100%').fontSize(16)
889        NodeContainer(this.myNodeController)
890          .borderWidth(1)
891          .width(300)
892          .height(100)
893      }
894      Button("getUserConfigBorderWidth")
895        .width(300)
896        .onClick(()=>{
897          const uiContext: UIContext = this.getUIContext();
898          if (uiContext) {
899            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
900            if (node) {
901              this.myNodeController.getUserConfigBorderWidth(node);
902            }
903          }
904        })
905      Button("getUserConfigPadding")
906        .width(300)
907        .onClick(()=>{
908          const uiContext: UIContext = this.getUIContext();
909          if (uiContext) {
910            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
911            if (node) {
912              this.myNodeController.getUserConfigPadding(node);
913            }
914          }
915        })
916      Button("getUserConfigMargin")
917        .width(300)
918        .onClick(()=>{
919          const uiContext: UIContext = this.getUIContext();
920          if (uiContext) {
921            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
922            if (node) {
923              this.myNodeController.getUserConfigMargin(node);
924            }
925          }
926        })
927      Button("getUserConfigSize")
928        .width(300)
929        .onClick(()=>{
930          const uiContext: UIContext = this.getUIContext();
931          if (uiContext) {
932            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
933            if (node) {
934              this.myNodeController.getUserConfigSize(node);
935            }
936          }
937        })
938      Button("getId")
939        .width(300)
940        .onClick(()=>{
941          const uiContext: UIContext = this.getUIContext();
942          if (uiContext) {
943            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
944            if (node) {
945              this.myNodeController.getId(node);
946            }
947          }
948        })
949      Button("getUniqueId")
950        .width(300)
951        .onClick(()=>{
952          const uiContext: UIContext = this.getUIContext();
953          if (uiContext) {
954            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
955            if (node) {
956              this.myNodeController.getUniqueId(node);
957            }
958          }
959        })
960      Button("getNodeType")
961        .width(300)
962        .onClick(()=>{
963          const uiContext: UIContext = this.getUIContext();
964          if (uiContext) {
965            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
966            if (node) {
967              this.myNodeController.getNodeType(node);
968            }
969          }
970        })
971      Button("getOpacity")
972        .width(300)
973        .onClick(()=>{
974          const uiContext: UIContext = this.getUIContext();
975          if (uiContext) {
976            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
977            if (node) {
978              this.myNodeController.getOpacity(node);
979            }
980          }
981        })
982      Button("isVisible")
983        .width(300)
984        .onClick(()=>{
985          const uiContext: UIContext = this.getUIContext();
986          if (uiContext) {
987            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
988            if (node) {
989              this.myNodeController.isVisible(node);
990            }
991          }
992        })
993      Button("isClipToFrame")
994        .width(300)
995        .onClick(()=>{
996          const uiContext: UIContext = this.getUIContext();
997          if (uiContext) {
998            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
999            if (node) {
1000              this.myNodeController.isClipToFrame(node);
1001            }
1002          }
1003        })
1004      Button("isAttached")
1005        .width(300)
1006        .onClick(()=>{
1007          const uiContext: UIContext = this.getUIContext();
1008          if (uiContext) {
1009            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1010            if (node) {
1011              this.myNodeController.isAttached(node);
1012            }
1013          }
1014        })
1015      Button("remove Text")
1016        .width(300)
1017        .onClick(()=>{
1018          const uiContext: UIContext = this.getUIContext();
1019          if (uiContext) {
1020            const node: FrameNode | null = uiContext.getFrameNodeById("textTypeNode") || null;
1021            if (node) {
1022              this.myNodeController.removeChild(node);
1023              this.myNodeController.isAttached(node);
1024            }
1025          }
1026        })
1027      Button("getInspectorInfo")
1028        .width(300)
1029        .onClick(()=>{
1030          const uiContext: UIContext = this.getUIContext();
1031          if (uiContext) {
1032            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1033            if (node) {
1034              this.myNodeController.getInspectorInfo(node);
1035            }
1036          }
1037        })
1038      Button("getCustomProperty")
1039        .width(300)
1040        .onClick(()=>{
1041          const uiContext: UIContext = this.getUIContext();
1042          if (uiContext) {
1043            const node: FrameNode | null = uiContext.getFrameNodeById("buildText") || null;
1044            if (node) {
1045              const property = node.getCustomProperty("key1");
1046              console.log(TEST_TAG, JSON.stringify(property));
1047            }
1048          }
1049        })
1050    }
1051    .padding({ left: 35, right: 35, top: 35, bottom: 35 })
1052    .width("100%")
1053    .height("100%")
1054  }
1055}
1056```
1057
1058## Disassociating the Current FrameNode Object from the Entity FrameNode
1059
1060To disassociate the current **FrameNode** object from the entity FrameNode, call the [dispose](../reference/apis-arkui/js-apis-arkui-frameNode.md#dispose12) API.
1061
1062> **NOTE**
1063>
1064> After the **dispose** API is called, the **FrameNode** object no longer corresponds to any actual FrameNode. In this case, any attempt to call the following APIs will result in a JS crash in the application: **getMeasuredSize**, **getLayoutPosition**, **getUserConfigBorderWidth**, **getUserConfigPadding**, **getUserConfigMargin**, **getUserConfigSize**.
1065>
1066> To check whether the current **FrameNode** object corresponds to an entity FrameNode, you can use [getUniqueId](../reference/apis-arkui/js-apis-arkui-frameNode.md#getuniqueid12) API. A **UniqueId** value greater than 0 indicates that the object is associated with an entity FrameNode.
1067
1068```ts
1069import { NodeController, FrameNode, BuilderNode } from '@kit.ArkUI';
1070
1071@Component
1072struct TestComponent {
1073  build() {
1074    Column() {
1075      Text('This is a BuilderNode.')
1076        .fontSize(16)
1077        .fontWeight(FontWeight.Bold)
1078    }
1079    .width('100%')
1080    .backgroundColor(Color.Gray)
1081  }
1082
1083  aboutToAppear() {
1084    console.error('aboutToAppear');
1085  }
1086
1087  aboutToDisappear() {
1088    console.error('aboutToDisappear');
1089  }
1090}
1091
1092@Builder
1093function buildComponent() {
1094  TestComponent()
1095}
1096
1097class MyNodeController extends NodeController {
1098  private rootNode: FrameNode | null = null;
1099  private builderNode: BuilderNode<[]> | null = null;
1100
1101  makeNode(uiContext: UIContext): FrameNode | null {
1102    this.rootNode = new FrameNode(uiContext);
1103    this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 200, height: 100 } });
1104    this.builderNode.build(new WrappedBuilder(buildComponent));
1105
1106    const rootRenderNode = this.rootNode.getRenderNode();
1107    if (rootRenderNode !== null) {
1108      rootRenderNode.size = { width: 200, height: 200 };
1109      rootRenderNode.backgroundColor = 0xff00ff00;
1110      rootRenderNode.appendChild(this.builderNode!.getFrameNode()!.getRenderNode());
1111    }
1112
1113    return this.rootNode;
1114  }
1115
1116  disposeFrameNode() {
1117    if (this.rootNode !== null && this.builderNode !== null) {
1118      this.rootNode.removeChild(this.builderNode.getFrameNode());
1119      this.builderNode.dispose();
1120
1121      this.rootNode.dispose();
1122    }
1123  }
1124
1125  removeBuilderNode() {
1126    const rootRenderNode = this.rootNode!.getRenderNode();
1127    if (rootRenderNode !== null && this.builderNode !== null && this.builderNode.getFrameNode() !== null) {
1128      rootRenderNode.removeChild(this.builderNode!.getFrameNode()!.getRenderNode());
1129    }
1130  }
1131}
1132
1133@Entry
1134@Component
1135struct Index {
1136  private myNodeController: MyNodeController = new MyNodeController();
1137
1138  build() {
1139    Column({ space: 4 }) {
1140      NodeContainer(this.myNodeController)
1141      Button('FrameNode dispose')
1142        .onClick(() => {
1143          this.myNodeController.disposeFrameNode();
1144        })
1145        .width('100%')
1146    }
1147  }
1148}
1149```
1150
1151## Using the Lazy Loading Capability of FrameNode
1152
1153To implement lazy loading for custom nodes, you can use the [NodeAdapter](../reference/apis-arkui/js-apis-arkui-frameNode.md#nodeadapter12) object, the counterpart of **LazyForEach** on ArkTS.
1154
1155> **NOTE**
1156>
1157> Make sure the input parameter is not a negative number. If a negative value is provided, no action will be taken.
1158
1159```ts
1160import { FrameNode, NodeController, NodeAdapter, typeNode } from '@kit.ArkUI';
1161
1162class MyNodeAdapter extends NodeAdapter {
1163  uiContext: UIContext
1164  cachePool: Array<FrameNode> = new Array();
1165  changed: boolean = false
1166  reloadTimes: number = 0;
1167  data: Array<string> = new Array();
1168  hostNode?: FrameNode
1169
1170  constructor(uiContext: UIContext, count: number) {
1171    super();
1172    this.uiContext = uiContext;
1173    this.totalNodeCount = count;
1174    this.loadData();
1175  }
1176
1177  reloadData(count: number): void {
1178    this.reloadTimes++;
1179    NodeAdapter.attachNodeAdapter(this, this.hostNode);
1180    this.totalNodeCount = count;
1181    this.loadData();
1182    this.reloadAllItems();
1183  }
1184
1185  refreshData(): void {
1186    let items = this.getAllAvailableItems()
1187    console.log("UINodeAdapter get All items:" + items.length);
1188    this.reloadAllItems();
1189  }
1190
1191  detachData(): void {
1192    NodeAdapter.detachNodeAdapter(this.hostNode);
1193    this.reloadTimes = 0;
1194  }
1195
1196  loadData(): void {
1197    for (let i = 0; i < this.totalNodeCount; i++) {
1198      this.data[i] = "Adapter ListItem " + i + " r:" + this.reloadTimes;
1199    }
1200  }
1201
1202  changeData(from: number, count: number): void {
1203    this.changed = !this.changed;
1204    for (let i = 0; i < count; i++) {
1205      let index = i + from;
1206      this.data[index] = "Adapter ListItem " + (this.changed ? "changed:" : "") + index + " r:" + this.reloadTimes;
1207    }
1208    this.reloadItem(from, count);
1209  }
1210
1211  insertData(from: number, count: number): void {
1212    for (let i = 0; i < count; i++) {
1213      let index = i + from;
1214      this.data.splice(index, 0, "Adapter ListItem " + from + "-" + i);
1215    }
1216    this.insertItem(from, count);
1217    this.totalNodeCount += count;
1218    console.log("UINodeAdapter after insert count:" + this.totalNodeCount);
1219  }
1220
1221  removeData(from: number, count: number): void {
1222    let arr = this.data.splice(from, count);
1223    this.removeItem(from, count);
1224    this.totalNodeCount -= arr.length;
1225    console.log("UINodeAdapter after remove count:" + this.totalNodeCount);
1226  }
1227
1228  moveData(from: number, to: number): void {
1229    let tmp = this.data.splice(from, 1);
1230    this.data.splice(to, 0, tmp[0]);
1231    this.moveItem(from, to);
1232  }
1233
1234  onAttachToNode(target: FrameNode): void {
1235    console.log("UINodeAdapter onAttachToNode id:" + target.getUniqueId());
1236    this.hostNode = target;
1237  }
1238
1239  onDetachFromNode(): void {
1240    console.log("UINodeAdapter onDetachFromNode");
1241  }
1242
1243  onGetChildId(index: number): number {
1244    console.log("UINodeAdapter onGetChildId:" + index);
1245    return index;
1246  }
1247
1248  onCreateChild(index: number): FrameNode {
1249    console.log("UINodeAdapter onCreateChild:" + index);
1250    if (this.cachePool.length > 0) {
1251      let cacheNode = this.cachePool.pop();
1252      if (cacheNode !== undefined) {
1253        console.log("UINodeAdapter onCreateChild reused id:" + cacheNode.getUniqueId());
1254        let text = cacheNode?.getFirstChild();
1255        let textNode = text as typeNode.Text;
1256        textNode?.initialize(this.data[index]).fontSize(20);
1257        return cacheNode;
1258      }
1259    }
1260    console.log("UINodeAdapter onCreateChild createNew");
1261    let itemNode = typeNode.createNode(this.uiContext, "ListItem");
1262    let textNode = typeNode.createNode(this.uiContext, "Text");
1263    textNode.initialize(this.data[index]).fontSize(20);
1264    itemNode.appendChild(textNode);
1265    return itemNode;
1266  }
1267
1268  onDisposeChild(id: number, node: FrameNode): void {
1269    console.log("UINodeAdapter onDisposeChild:" + id);
1270    if (this.cachePool.length < 10) {
1271      if (!this.cachePool.includes(node)) {
1272        console.log("UINodeAdapter caching node id:" + node.getUniqueId());
1273        this.cachePool.push(node);
1274      }
1275    } else {
1276      node.dispose();
1277    }
1278  }
1279
1280  onUpdateChild(id: number, node: FrameNode): void {
1281    let index = id;
1282    let text = node.getFirstChild();
1283    let textNode = text as typeNode.Text;
1284    textNode?.initialize(this.data[index]).fontSize(20);
1285  }
1286}
1287
1288class MyNodeAdapterController extends NodeController {
1289  rootNode: FrameNode | null = null;
1290  nodeAdapter: MyNodeAdapter | null = null;
1291
1292  makeNode(uiContext: UIContext): FrameNode | null {
1293    this.rootNode = new FrameNode(uiContext);
1294    let listNode = typeNode.createNode(uiContext, "List");
1295    listNode.initialize({ space: 3 }).borderWidth(2).borderColor(Color.Black);
1296    this.rootNode.appendChild(listNode);
1297    this.nodeAdapter = new MyNodeAdapter(uiContext, 100);
1298    NodeAdapter.attachNodeAdapter(this.nodeAdapter, listNode);
1299    return this.rootNode;
1300  }
1301}
1302
1303@Entry
1304@Component
1305struct ListNodeTest {
1306  adapterController: MyNodeAdapterController = new MyNodeAdapterController();
1307
1308  build() {
1309    Column() {
1310      Text("ListNode Adapter");
1311      NodeContainer(this.adapterController)
1312        .width(300).height(300)
1313        .borderWidth(1).borderColor(Color.Black);
1314      Row() {
1315        Button("Reload")
1316          .onClick(() => {
1317            this.adapterController.nodeAdapter?.reloadData(50);
1318          })
1319        Button("Change")
1320          .onClick(() => {
1321            this.adapterController.nodeAdapter?.changeData(5, 10)
1322          })
1323        Button("Insert")
1324          .onClick(() => {
1325            this.adapterController.nodeAdapter?.insertData(10, 10);
1326          })
1327      }
1328
1329      Row() {
1330        Button("Remove")
1331          .onClick(() => {
1332            this.adapterController.nodeAdapter?.removeData(10, 10);
1333          })
1334        Button("Move")
1335          .onClick(() => {
1336            this.adapterController.nodeAdapter?.moveData(2, 5);
1337          })
1338        Button("Refresh")
1339          .onClick(() => {
1340            this.adapterController.nodeAdapter?.refreshData();
1341          })
1342        Button("Detach")
1343          .onClick(() => {
1344            this.adapterController.nodeAdapter?.detachData();
1345          })
1346      }
1347    }.borderWidth(1)
1348    .width("100%")
1349  }
1350}
1351```
1352