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](../ui/state-management/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## 基本概念 22 23- 自定义节点:使用ArkUI提供的接口,以命令式创建的节点。包括自定义组件节点(FrameNode)、自定义渲染节点(RenderNode)、自定义声明式节点(BuilderNode)、[ComponentContent](../reference/apis-arkui/js-apis-arkui-ComponentContent.md)等。 24 25- 自定义节点树:根节点为自定义节点的节点树。 26 27- 声明式节点树:根节点为声明式节点的节点树。 28 29- 节点树:一种常见的数据结构,用于表示节点的层级关系。 30 31- 占位节点:用于在声明式节点树上为自定义节点树预留位置的节点,主要包括NodeContainer和ContentSlot。鉴于页面的主树采用声明式节点树,因此,唯有借助占位节点,才能将命令式构建的自定义节点成功挂载至声明式节点树上。 32 33## 使用NodeContainer挂载自定义节点 34 35通过NodeController在NodeContainer下挂载自定义节点。 36 37```ts 38// common.ets 39import { BuilderNode, UIContext } from '@kit.ArkUI'; 40 41class Params { 42 text: string = "this is a text"; 43} 44 45let buttonNode: BuilderNode<[Params]> | null = null; 46 47@Builder 48function buttonBuilder(params: Params) { 49 Column() { 50 Button(params.text) 51 .fontSize(12) 52 .borderRadius(8) 53 .borderWidth(2) 54 .backgroundColor(Color.Orange) 55 } 56} 57 58export function createNode(uiContext: UIContext) { 59 buttonNode = new BuilderNode<[Params]>(uiContext); 60 buttonNode.build(wrapBuilder(buttonBuilder), { text: "This is a Button" }); 61 return buttonNode; 62} 63 64export function getOrCreateNode(uiContext: UIContext): BuilderNode<[Params]> | null { 65 if (buttonNode?.getFrameNode() && buttonNode?.getFrameNode()?.getUniqueId() != -1) { 66 return buttonNode; 67 } else { 68 return createNode(uiContext); 69 } 70} 71``` 72```ts 73// Index.ets 74import { FrameNode, NodeController, Size, UIContext } from '@kit.ArkUI'; 75import { getOrCreateNode } from "./common"; 76 77const TEST_TAG: string = "NodeContainer"; 78 79class MyNodeController extends NodeController { 80 private isShow: boolean = false; 81 82 constructor(isShow: boolean) { 83 super(); 84 this.isShow = isShow; 85 } 86 87 makeNode(uiContext: UIContext): FrameNode | null { 88 if (!this.isShow) { 89 return null; 90 } 91 let frameNode = getOrCreateNode(uiContext)?.getFrameNode(); 92 return frameNode ? frameNode : null; 93 } 94 95 aboutToResize(size: Size) { 96 console.log(TEST_TAG + " aboutToResize width : " + size.width + " height : " + size.height); 97 } 98 99 aboutToAppear() { 100 console.log(TEST_TAG + " aboutToAppear"); 101 } 102 103 aboutToDisappear() { 104 console.log(TEST_TAG + " aboutToDisappear"); 105 } 106 107 onTouchEvent(event: TouchEvent) { 108 console.log(TEST_TAG + " onTouchEvent"); 109 } 110 111 toShow() { 112 this.isShow = true; 113 this.rebuild(); 114 } 115 116 toHide() { 117 this.isShow = false; 118 this.rebuild(); 119 } 120} 121 122@Entry 123@Component 124struct Index { 125 private myNodeController1: MyNodeController = new MyNodeController(true); 126 private myNodeController2: MyNodeController = new MyNodeController(false); 127 128 build() { 129 Column() { 130 NodeContainer(this.myNodeController1) 131 .width("100%") 132 .height("40%") 133 .backgroundColor(Color.Brown) 134 NodeContainer(this.myNodeController2) 135 .width("100%") 136 .height("40%") 137 .backgroundColor(Color.Gray) 138 Button("Change the place of button") 139 .onClick(() => { 140 // 先在原始占位节点中下树 141 // 后在新的占位节点中上树 142 // 保证自定义节点仅作为一个节点的子节点存在 143 this.myNodeController1.toHide(); 144 this.myNodeController2.toShow(); 145 }) 146 } 147 .padding({ left: 35, right: 35, top: 35 }) 148 .width("100%") 149 .height("100%") 150 } 151} 152``` 153 154## NodeContainer和ContentSlot添加子节点布局差异 155 156[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)只是一个语法节点,不参与布局,添加的子节点会按照父容器的布局规则进行布局。 157 158```ts 159import { FrameNode, NodeContent, NodeController, typeNode, UIContext } from '@kit.ArkUI'; 160 161class NodeContentCtrl { 162 content: NodeContent 163 textNode: Array<typeNode.Text> = new Array(); 164 uiContext: UIContext 165 width: number 166 167 constructor(uiContext: UIContext) { 168 this.content = new NodeContent(); 169 this.uiContext = uiContext; 170 this.width = Infinity; 171 } 172 173 AddNode() { 174 let node = typeNode.createNode(this.uiContext, "Text"); 175 node.initialize("ContentText:" + this.textNode.length).fontSize(20); 176 this.textNode.push(node); 177 this.content.addFrameNode(node); 178 } 179 180 RemoveNode() { 181 let node = this.textNode.pop(); 182 this.content.removeFrameNode(node); 183 } 184 185 RemoveFront() { 186 let node = this.textNode.shift(); 187 this.content.removeFrameNode(node); 188 } 189 190 GetContent(): NodeContent { 191 return this.content; 192 } 193} 194 195class MyNodeController extends NodeController { 196 public rootNode: FrameNode | null = null; 197 textNode: Array<typeNode.Text> = new Array(); 198 199 makeNode(uiContext: UIContext): FrameNode { 200 this.rootNode = new FrameNode(uiContext); 201 return this.rootNode; 202 } 203 204 AddNode(frameNode: FrameNode | null, uiContext: UIContext) { 205 let node = typeNode.createNode(uiContext, "Text"); 206 node.initialize("ControllerText:" + this.textNode.length).fontSize(20); 207 this.textNode.push(node); 208 frameNode?.appendChild(node); 209 } 210 211 RemoveNode(frameNode: FrameNode | null) { 212 let node = this.textNode.pop(); 213 frameNode?.removeChild(node); 214 } 215 216 RemoveFront(frameNode: FrameNode | null) { 217 let node = this.textNode.shift(); 218 frameNode?.removeChild(node); 219 } 220} 221 222@Entry 223@Component 224struct Index { 225 @State message: string = 'Hello World'; 226 controller = new NodeContentCtrl(this.getUIContext()); 227 myNodeController = new MyNodeController(); 228 229 build() { 230 Row() { 231 Column() { 232 ContentSlot(this.controller.GetContent()) 233 Button("AddToSlot") 234 .onClick(() => { 235 this.controller.AddNode(); 236 }) 237 .margin(10) 238 Button("RemoveBack") 239 .onClick(() => { 240 this.controller.RemoveNode(); 241 }) 242 .margin(10) 243 Button("RemoveFront") 244 .onClick(() => { 245 this.controller.RemoveFront(); 246 }) 247 .margin(10) 248 } 249 .width('50%') 250 251 Column() { 252 NodeContainer(this.myNodeController) 253 Button("AddToNodeContainer") 254 .onClick(() => { 255 this.myNodeController.AddNode(this.myNodeController.rootNode, this.getUIContext()); 256 }) 257 .margin(10) 258 Button("RemoveBack") 259 .onClick(() => { 260 this.myNodeController.RemoveNode(this.myNodeController.rootNode); 261 }) 262 .margin(10) 263 Button("RemoveFront") 264 .onClick(() => { 265 this.myNodeController.RemoveFront(this.myNodeController.rootNode); 266 }) 267 .margin(10) 268 } 269 .width('50%') 270 } 271 .height('100%') 272 } 273} 274``` 275 276 277