1# 自定义占位节点
2
3ArkUI提供了系统组件[NodeContainer](../../application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)和[ContentSlot](../../application-dev/reference/apis-arkui/arkui-ts/ts-components-contentSlot.md)作为自定义节点的占位节点。主要用于自定义节点以及自定义节点树的显示。
4
5[NodeContainer](../../application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)作为容器节点存在,具备通用属性,是UI节点。[ContentSlot](../quick-start/arkts-rendering-control-contentslot.md)只是一个语法节点,无通用属性,不参与布局和渲染。支持混合模式开发,当容器是ArkTS组件,子组件在Native侧创建时,推荐使用ContentSlot占位组件。具体使用参考[ContentSlot](../../application-dev/reference/apis-arkui/arkui-ts/ts-components-contentSlot.md)的接口文档说明。
6
7[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)是用来占位的系统组件,主要用于自定义节点以及自定义节点树的显示,支持组件的通用属性,对通用属性的处理请参考默认左上角对齐的[Stack](../reference/apis-arkui/arkui-ts/ts-container-stack.md)组件。
8
9[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)提供了一系列生命周期回调,通过[makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode)回调返回一个 [FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#framenode) 节点树的根节点。将[FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md)节点树挂载到对应的[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)下。同时提供了[aboutToAppear](../reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoappear)、[aboutToDisappear](../reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttodisappear)、[aboutToResize](../reference/apis-arkui/js-apis-arkui-nodeController.md#abouttoresize)、[onTouchEvent](../reference/apis-arkui/js-apis-arkui-nodeController.md#ontouchevent)、[rebuild](../reference/apis-arkui/js-apis-arkui-nodeController.md#rebuild)五个回调方法用于监听对应的[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)的状态。
10
11每个生命周期的回调的具体含义参考[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)的接口文档说明。
12
13> **说明:**
14>
15> - NodeContainer下仅支持挂载自定义的FrameNode节点以及BuilderNode创建的组件树的根节点。
16>
17> - 从API Version 12开始支持的接口,可以通过FrameNode的查询接口返回原生组件的代理节点,代理节点可以作为makeNode的返回值进行返回,但代理节点无法成功挂载在组件树上,最终的显示结果为代理节点挂载失败。
18>
19> - 需要保证一个节点只能作为一个父节点的子节点去使用,否则可能存在显示异常或者功能异常,尤其是页面路由场景或者动效场景。例如,如果通过NodeController将同一个节点挂载在多个NodeContainer上,仅一个占位容器下会显示节点,且多个NodeContainer的可见性、透明度等影响子组件状态的属性更新均会影响被挂载的子节点。
20
21## 使用NodeContainer挂载自定义节点
22
23通过NodeController在NodeContainer下挂载自定义节点。
24
25```ts
26// common.ets
27import { BuilderNode, UIContext } from '@kit.ArkUI'
28
29class Params {
30  text: string = "this is a text"
31}
32
33let buttonNode: BuilderNode<[Params]> | null = null;
34
35@Builder
36function buttonBuilder(params: Params) {
37  Column() {
38    Button(params.text)
39      .fontSize(12)
40      .borderRadius(8)
41      .borderWidth(2)
42      .backgroundColor(Color.Orange)
43  }
44}
45
46export function createNode(uiContext: UIContext) {
47  buttonNode = new BuilderNode<[Params]>(uiContext);
48  buttonNode.build(wrapBuilder(buttonBuilder), { text: "This is a Button" });
49  return buttonNode;
50}
51
52export function getOrCreateNode(uiContext: UIContext): BuilderNode<[Params]> | null {
53  if (buttonNode?.getFrameNode() && buttonNode?.getFrameNode()?.getUniqueId() != -1) {
54    return buttonNode;
55  } else {
56    return createNode(uiContext);
57  }
58}
59```
60```ts
61// Index.ets
62import { FrameNode, NodeController, Size, UIContext } from '@kit.ArkUI'
63import { getOrCreateNode } from "./common"
64
65class MyNodeController extends NodeController {
66  private isShow: boolean = false;
67
68  constructor(isShow: boolean) {
69    super();
70    this.isShow = isShow;
71  }
72
73  makeNode(uiContext: UIContext): FrameNode | null {
74    if (!this.isShow) {
75      return null;
76    }
77    let frameNode = getOrCreateNode(uiContext)?.getFrameNode();
78    return frameNode ? frameNode : null;
79  }
80
81  aboutToResize(size: Size) {
82    console.log("aboutToResize width : " + size.width + " height : " + size.height)
83  }
84
85  aboutToAppear() {
86    console.log("aboutToAppear")
87  }
88
89  aboutToDisappear() {
90    console.log("aboutToDisappear");
91  }
92
93  onTouchEvent(event: TouchEvent) {
94    console.log("onTouchEvent");
95  }
96
97  toShow() {
98    this.isShow = true;
99    this.rebuild();
100  }
101
102  toHide() {
103    this.isShow = false;
104    this.rebuild();
105  }
106}
107
108@Entry
109@Component
110struct Index {
111  private myNodeController1: MyNodeController = new MyNodeController(true);
112  private myNodeController2: MyNodeController = new MyNodeController(false);
113
114  build() {
115    Column() {
116      NodeContainer(this.myNodeController1)
117        .width("100%")
118        .height("40%")
119        .backgroundColor(Color.Brown)
120      NodeContainer(this.myNodeController2)
121        .width("100%")
122        .height("40%")
123        .backgroundColor(Color.Gray)
124      Button("Change the place of button")
125        .onClick(() => {
126          // 先在原始占位节点中下树
127          // 后在新的占位节点中上树
128          // 保证自定义节点仅作为一个节点的子节点存在
129          this.myNodeController1.toHide();
130          this.myNodeController2.toShow();
131        })
132    }
133    .padding({ left: 35, right: 35, top: 35 })
134    .width("100%")
135    .height("100%")
136  }
137}
138```
139
140## NodeContainer和ContentSlot添加子节点布局差异
141
142[NodeContainer](../../application-dev/reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)是一个容器节点,布局参考默认左上角对齐的[Stack](../reference/apis-arkui/arkui-ts/ts-container-stack.md)组件,不会按照父容器的布局规则进行布局。[ContentSlot](../../application-dev/reference/apis-arkui/arkui-ts/ts-components-contentSlot.md)只是一个语法节点,不参与布局,添加的子节点会按照父容器的布局规则进行布局。
143
144```ts
145import { FrameNode, NodeContent, NodeController, typeNode, UIContext } from '@kit.ArkUI';
146
147class NodeContentCtrl {
148  content: NodeContent
149  textNode: Array<typeNode.Text> = new Array();
150  uiContext: UIContext
151  width: number
152
153  constructor(uiContext: UIContext) {
154    this.content = new NodeContent()
155    this.uiContext = uiContext
156    this.width = Infinity
157  }
158
159  AddNode() {
160    let node = typeNode.createNode(this.uiContext, "Text")
161    node.initialize("ContentText:" + this.textNode.length).fontSize(20)
162    this.textNode.push(node)
163    this.content.addFrameNode(node)
164  }
165
166  RemoveNode() {
167    let node = this.textNode.pop()
168    this.content.removeFrameNode(node)
169  }
170
171  RemoveFront() {
172    let node = this.textNode.shift()
173    this.content.removeFrameNode(node)
174  }
175
176  GetContent(): NodeContent {
177    return this.content
178  }
179}
180
181class MyNodeController extends NodeController {
182  public rootNode: FrameNode | null = null;
183  textNode: Array<typeNode.Text> = new Array();
184  makeNode(uiContext: UIContext): FrameNode {
185    this.rootNode = new FrameNode(uiContext);
186    return this.rootNode;
187  }
188
189  AddNode(frameNode: FrameNode | null, uiContext: UIContext) {
190    let node = typeNode.createNode(uiContext, "Text")
191    node.initialize("ControllerText:" + this.textNode.length).fontSize(20)
192    this.textNode.push(node)
193    frameNode?.appendChild(node)
194  }
195
196  RemoveNode(frameNode: FrameNode | null) {
197    let node = this.textNode.pop()
198    frameNode?.removeChild(node)
199  }
200
201  RemoveFront(frameNode: FrameNode | null) {
202    let node = this.textNode.shift()
203    frameNode?.removeChild(node)
204  }
205}
206
207@Entry
208@Component
209struct Index {
210  @State message: string = 'Hello World';
211  controller = new NodeContentCtrl(this.getUIContext());
212  myNodeController = new MyNodeController();
213  build() {
214    Row() {
215      Column() {
216        ContentSlot(this.controller.GetContent())
217        Button("AddToSlot")
218          .onClick(() => {
219            this.controller.AddNode()
220          })
221        Button("RemoveBack")
222          .onClick(() => {
223            this.controller.RemoveNode()
224          })
225        Button("RemoveFront")
226          .onClick(() => {
227            this.controller.RemoveFront()
228          })
229      }
230      .width('50%')
231      Column() {
232        NodeContainer(this.myNodeController)
233        Button("AddToNodeContainer")
234          .onClick(() => {
235            this.myNodeController.AddNode(this.myNodeController.rootNode, this.getUIContext())
236          })
237        Button("RemoveBack")
238          .onClick(() => {
239            this.myNodeController.RemoveNode(this.myNodeController.rootNode)
240          })
241        Button("RemoveFront")
242          .onClick(() => {
243            this.myNodeController.RemoveFront(this.myNodeController.rootNode)
244          })
245      }
246      .width('50%')
247    }
248    .height('100%')
249  }
250}
251```
252
253![zh-cn_image_user-defined-node-01](figures/user-defined-node-01.gif)
254