1# Custom Render Node (RenderNode) 2 3## Overview 4 5For third-party frameworks that lack an inherent rendering environment, leveraging the system's basic rendering and animation capabilities is crucial. Such frameworks often have their own mechanisms for front-end parsing, layout management, and event handling. As a result, the universal attributes and events associated with [FrameNode](./arkts-user-defined-arktsNode-frameNode.md) may be redundant for these frameworks, potentially causing duplicate work in managing layout and event logic. 6 7This is where [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md) becomes beneficial. **RenderNode** is a streamlined render node designed to offer rendering-specific features. It allows for setting of basic rendering properties and provides the capability to dynamically add and remove nodes and to implement custom drawing. These can be used to provide third-party frameworks with essential rendering and animation capabilities. 8 9## Creating and Removing Nodes 10 11You can create and remove nodes with **RenderNode**. You can create a custom instance of **RenderNode** using its constructor, and the instance thereby created corresponds to an entity node. You can use the [dispose](../reference/apis-arkui/js-apis-arkui-renderNode.md#dispose12) API in **RenderNode** to break the binding with the entity node. 12 13## Operating the Node Tree 14 15With **RenderNode**, you can add, delete, query, and modify nodes, thereby changing the subtree structure of nodes; you can also query the parent-child relationships to obtain the results. 16 17> **NOTE** 18> 19> - The subtree structure obtained through queries in **RenderNode** is constructed based on the parameters passed through the APIs of **RenderNode**. 20> 21> - To display a RenderNode in conjunction with built-in components, you need to mount the RenderNode obtained from a FrameNode onto the component tree. 22 23```ts 24import { FrameNode, NodeController, RenderNode } from '@kit.ArkUI'; 25 26const TEST_TAG: string = "RenderNode"; 27const renderNode = new RenderNode(); 28renderNode.frame = { 29 x: 0, 30 y: 0, 31 width: 200, 32 height: 350 33}; 34renderNode.backgroundColor = 0xffff0000; 35for (let i = 0; i < 5; i++) { 36 const node = new RenderNode(); 37 // Set the frame size of the node. 38 node.frame = { 39 x: 10, 40 y: 10 + 60 * i, 41 width: 50, 42 height: 50 43 }; 44 // Set the background color of the node. 45 node.backgroundColor = 0xff00ff00; 46 // Mount the new node to the RenderNode. 47 renderNode.appendChild(node); 48} 49 50class MyNodeController extends NodeController { 51 private rootNode: FrameNode | null = null; 52 53 makeNode(uiContext: UIContext): FrameNode | null { 54 this.rootNode = new FrameNode(uiContext); 55 56 const rootRenderNode = this.rootNode?.getRenderNode(); 57 if (rootRenderNode) { 58 rootRenderNode.appendChild(renderNode); 59 } 60 return this.rootNode; 61 } 62} 63 64@Entry 65@Component 66struct Index { 67 private myNodeController: MyNodeController = new MyNodeController(); 68 69 build() { 70 Row() { 71 NodeContainer(this.myNodeController) 72 .width(200) 73 .height(350) 74 Button('getNextSibling') 75 .onClick(() => { 76 const child = renderNode.getChild(1); 77 const nextSibling = child!.getNextSibling() 78 if (child === null || nextSibling === null) { 79 console.log(TEST_TAG + ' the child or nextChild is null'); 80 } else { 81 // Obtain the position of the child node. 82 console.log(`${TEST_TAG} the position of child is x: ${child.position.x}, y: ${child.position.y}, ` + 83 `the position of nextSibling is x: ${nextSibling.position.x}, y: ${nextSibling.position.y}`); 84 } 85 }) 86 } 87 } 88} 89``` 90 91## Setting and Obtaining Rendering-related Properties 92 93In **RenderNode**, you can set rendering-related properties, including the following: [backgroundColor](../reference/apis-arkui/js-apis-arkui-renderNode.md#backgroundcolor), [clipToFrame](../reference/apis-arkui/js-apis-arkui-renderNode.md#cliptoframe), [opacity](../reference/apis-arkui/js-apis-arkui-renderNode.md#opacity), [size](../reference/apis-arkui/js-apis-arkui-renderNode.md#size), [position](../reference/apis-arkui/js-apis-arkui-renderNode.md#position), [frame](../reference/apis-arkui/js-apis-arkui-renderNode.md#frame), [pivot](../reference/apis-arkui/js-apis-arkui-renderNode.md#pivot), [scale](../reference/apis-arkui/js-apis-arkui-renderNode.md#scale), [translation](../reference/apis-arkui/js-apis-arkui-renderNode.md#translation), [rotation](../reference/apis-arkui/js-apis-arkui-renderNode.md#rotation), [transform](../reference/apis-arkui/js-apis-arkui-renderNode.md#transform), [shadowColor](../reference/apis-arkui/js-apis-arkui-renderNode.md#shadowcolor), [shadowOffset](../reference/apis-arkui/js-apis-arkui-renderNode.md#shadowoffset), [shadowAlpha](../reference/apis-arkui/js-apis-arkui-renderNode.md#shadowalpha), [shadowElevation](../reference/apis-arkui/js-apis-arkui-renderNode.md#shadowelevation), [shadowRadius](../reference/apis-arkui/js-apis-arkui-renderNode.md#shadowradius), [borderStyle](../reference/apis-arkui/js-apis-arkui-renderNode.md#borderstyle12), [borderWidth](../reference/apis-arkui/js-apis-arkui-renderNode.md#borderwidth12), [borderColor](../reference/apis-arkui/js-apis-arkui-renderNode.md#bordercolor12), [borderRadius](../reference/apis-arkui/js-apis-arkui-renderNode.md#borderradius12), [shapeMask](../reference/apis-arkui/js-apis-arkui-renderNode.md#shapemask12), [shapeClip](../reference/apis-arkui/js-apis-arkui-renderNode.md#shapeclip12), [markNodeGroup](../reference/apis-arkui/js-apis-arkui-renderNode.md#marknodegroup12). For details about the supported attributes, see [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md#rendernode). 94 95> **NOTE** 96> 97> - The properties obtained from a query in **RenderNode** are the values that have been explicitly set. 98> 99> - If no parameters are provided or if the provided parameters are invalid, the query will return the default values. 100> 101> - Avoid modifying RenderNodes in a BuilderNode. In **BuilderNode**, how properties are applied and updated is governed by the state management system, independently of manual intervention. Be aware that setting the same **RenderNode** property in both BuilderNode and FrameNode could lead to unexpected behavior. 102 103```ts 104import { RenderNode, FrameNode, NodeController, ShapeMask, ShapeClip } from '@kit.ArkUI'; 105 106const TEST_TAG: string = "RenderNode"; 107const mask = new ShapeMask(); 108mask.setRectShape({ 109 left: 0, 110 right: 150, 111 top: 0, 112 bottom: 150 113}); 114mask.fillColor = 0X55FF0000; 115mask.strokeColor = 0XFFFF0000; 116mask.strokeWidth = 24; 117 118const clip = new ShapeClip(); 119clip.setCommandPath({ commands: "M100 0 L0 100 L50 200 L150 200 L200 100 Z" }); 120 121const renderNode = new RenderNode(); 122renderNode.backgroundColor = 0xffff0000; 123renderNode.size = { width: 100, height: 100 }; 124 125class MyNodeController extends NodeController { 126 private rootNode: FrameNode | null = null; 127 128 makeNode(uiContext: UIContext): FrameNode | null { 129 this.rootNode = new FrameNode(uiContext); 130 131 const rootRenderNode = this.rootNode.getRenderNode(); 132 if (rootRenderNode !== null) { 133 rootRenderNode.appendChild(renderNode); 134 } 135 136 return this.rootNode; 137 } 138} 139 140@Entry 141@Component 142struct Index { 143 private myNodeController: MyNodeController = new MyNodeController(); 144 145 build() { 146 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) { 147 Column() { 148 NodeContainer(this.myNodeController) 149 } 150 151 Button("position") 152 .width(300) 153 .onClick(() => { 154 renderNode.position = { x: 10, y: 10 }; 155 console.log(TEST_TAG + " position:" + JSON.stringify(renderNode.position)); 156 }) 157 Button("pivot") 158 .width(300) 159 .onClick(() => { 160 renderNode.pivot = { x: 0.5, y: 0.6 }; 161 console.log(TEST_TAG + " pivot:" + JSON.stringify(renderNode.pivot)); 162 }) 163 Button("scale") 164 .width(300) 165 .onClick(() => { 166 renderNode.scale = { x: 0.5, y: 1 }; 167 console.log(TEST_TAG + " scale:" + JSON.stringify(renderNode.scale)); 168 }) 169 Button("translation") 170 .width(300) 171 .onClick(() => { 172 renderNode.translation = { x: 100, y: 0 }; 173 console.log(TEST_TAG + " translation:" + JSON.stringify(renderNode.translation)); 174 }) 175 Button("rotation") 176 .width(300) 177 .onClick(() => { 178 renderNode.rotation = { x: 45, y: 0, z: 0 }; 179 console.log(TEST_TAG + " rotation:" + JSON.stringify(renderNode.rotation)); 180 }) 181 Button("transform") 182 .width(300) 183 .onClick(() => { 184 renderNode.transform = [ 185 1, 0, 0, 0, 186 0, 2, 0, 0, 187 0, 0, 1, 0, 188 0, 0, 0, 1 189 ]; 190 console.log(TEST_TAG + " transform:" + JSON.stringify(renderNode.transform)); 191 }) 192 Button("shadow") 193 .width(300) 194 .onClick(() => { 195 renderNode.shadowElevation = 10; 196 renderNode.shadowColor = 0XFF00FF00; 197 renderNode.shadowOffset = { x: 10, y: 10 }; 198 renderNode.shadowAlpha = 0.1; 199 console.log(TEST_TAG + " shadowElevation:" + JSON.stringify(renderNode.shadowElevation)); 200 console.log(TEST_TAG + " shadowColor:" + JSON.stringify(renderNode.shadowColor)); 201 console.log(TEST_TAG + " shadowOffset:" + JSON.stringify(renderNode.shadowOffset)); 202 console.log(TEST_TAG + " shadowAlpha:" + JSON.stringify(renderNode.shadowAlpha)); 203 }) 204 Button("shadowRadius") 205 .width(300) 206 .onClick(() => { 207 renderNode.shadowOffset = { x: 10, y: 10 }; 208 renderNode.shadowAlpha = 0.7 209 renderNode.shadowRadius = 30; 210 console.log(TEST_TAG + " shadowOffset:" + JSON.stringify(renderNode.shadowOffset)); 211 console.log(TEST_TAG + " shadowAlpha:" + JSON.stringify(renderNode.shadowAlpha)); 212 console.log(TEST_TAG + " shadowRadius:" + JSON.stringify(renderNode.shadowRadius)); 213 }) 214 Button("border") 215 .width(300) 216 .onClick(() => { 217 renderNode.borderWidth = { 218 left: 8, 219 top: 8, 220 right: 8, 221 bottom: 8 222 }; 223 renderNode.borderStyle = { 224 left: BorderStyle.Solid, 225 top: BorderStyle.Dotted, 226 right: BorderStyle.Dashed, 227 bottom: BorderStyle.Solid 228 } 229 renderNode.borderColor = { 230 left: 0xFF0000FF, 231 top: 0xFF0000FF, 232 right: 0xFF0000FF, 233 bottom: 0xFF0000FF 234 }; 235 renderNode.borderRadius = { 236 topLeft: 32, 237 topRight: 32, 238 bottomLeft: 32, 239 bottomRight: 32 240 }; 241 console.log(TEST_TAG + " borderWidth:" + JSON.stringify(renderNode.borderWidth)); 242 console.log(TEST_TAG + " borderStyle:" + JSON.stringify(renderNode.borderStyle)); 243 console.log(TEST_TAG + " borderColor:" + JSON.stringify(renderNode.borderColor)); 244 console.log(TEST_TAG + " borderRadius:" + JSON.stringify(renderNode.borderRadius)); 245 }) 246 Button("shapeMask") 247 .width(300) 248 .onClick(() => { 249 renderNode.shapeMask = mask; 250 console.log(TEST_TAG + " shapeMask:" + JSON.stringify(renderNode.shapeMask)); 251 }) 252 Button("shapeClip") 253 .width(300) 254 .onClick(() => { 255 renderNode.shapeClip = clip; 256 console.log(TEST_TAG + " shapeMask:" + JSON.stringify(renderNode.shapeMask)); 257 }) 258 } 259 .padding({ 260 left: 35, 261 right: 35, 262 top: 35, 263 bottom: 35 264 }) 265 .width("100%") 266 .height("100%") 267 } 268} 269``` 270 271## Using Custom Drawing 272 273Override the [draw](../reference/apis-arkui/js-apis-arkui-renderNode.md#draw) API in **RenderNode** to customize the drawing content and use the [invalidate](../reference/apis-arkui/js-apis-arkui-renderNode.md#invalidate) API to manually trigger a redraw of the node. 274 275> **NOTE** 276> 277> - Calling **invalidate()** multiple times synchronously will trigger only a single redraw. 278> 279> - Custom drawing can be implemented by calling ArkTS APIs or Node-APIs. 280 281**ArkTS API sample code** 282 283```ts 284import { FrameNode, NodeController, RenderNode } from '@kit.ArkUI'; 285import { drawing } from '@kit.ArkGraphics2D'; 286 287class MyRenderNode extends RenderNode { 288 width: number = 200; 289 290 draw(context: DrawContext) { 291 // Obtain the canvas object. 292 const canvas = context.canvas; 293 // Create a brush. 294 const brush = new drawing.Brush(); 295 // Set the brush color. 296 brush.setColor({ 297 alpha: 255, 298 red: 255, 299 green: 0, 300 blue: 0 301 }); 302 canvas.attachBrush(brush); 303 // Draw a rectangle. 304 canvas.drawRect({ 305 left: 0, 306 right: this.width, 307 top: 0, 308 bottom: 200 309 }); 310 canvas.detachBrush(); 311 console.log(`RenderNode draw width = ${this.width}`); 312 } 313} 314 315const renderNode = new MyRenderNode(); 316renderNode.frame = { 317 x: 0, 318 y: 0, 319 width: 300, 320 height: 300 321}; 322renderNode.backgroundColor = 0xff0000ff; 323renderNode.opacity = 0.5; 324 325class MyNodeController extends NodeController { 326 private rootNode: FrameNode | null = null; 327 328 makeNode(uiContext: UIContext): FrameNode | null { 329 this.rootNode = new FrameNode(uiContext); 330 331 const rootRenderNode = this.rootNode?.getRenderNode(); 332 if (rootRenderNode !== null) { 333 rootRenderNode.frame = { 334 x: 0, 335 y: 0, 336 width: 500, 337 height: 500 338 } 339 rootRenderNode.appendChild(renderNode); 340 } 341 342 return this.rootNode; 343 } 344} 345 346@Entry 347@Component 348struct Index { 349 private myNodeController: MyNodeController = new MyNodeController(); 350 351 build() { 352 Column() { 353 NodeContainer(this.myNodeController) 354 .width('100%') 355 Button('Invalidate') 356 .onClick(() => { 357 // Calling invalidate() multiple times synchronously will trigger only a single redraw. As a result, the log inside the draw callback will be printed only once. 358 renderNode.width += 10; 359 renderNode.invalidate(); 360 renderNode.invalidate(); 361 }) 362 } 363 } 364} 365``` 366 367**Node-API sample code** 368 369The C++ side can obtain the canvas through the Node-API and perform subsequent custom drawing operations. 370 371```c++ 372// native_bridge.cpp 373#include "napi/native_api.h" 374#include <native_drawing/drawing_canvas.h> 375#include <native_drawing/drawing_color.h> 376#include <native_drawing/drawing_path.h> 377#include <native_drawing/drawing_pen.h> 378 379static napi_value OnDraw(napi_env env, napi_callback_info info) 380{ 381 size_t argc = 4; 382 napi_value args[4] = { nullptr }; 383 napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); 384 385 int32_t id; 386 napi_get_value_int32(env, args[0], &id); 387 388 // Obtain the pointer to the canvas. 389 void* temp = nullptr; 390 napi_unwrap(env, args[1], &temp); 391 OH_Drawing_Canvas *canvas = reinterpret_cast<OH_Drawing_Canvas*>(temp); 392 393 // Obtain the canvas width. 394 int32_t width; 395 napi_get_value_int32(env, args[2], &width); 396 397 // Obtain the canvas height. 398 int32_t height; 399 napi_get_value_int32(env, args[3], &height); 400 401 // Pass in information such as the canvas, height, and width to the drawing API for custom drawing. 402 auto path = OH_Drawing_PathCreate(); 403 OH_Drawing_PathMoveTo(path, width / 4, height / 4); 404 OH_Drawing_PathLineTo(path, width * 3 / 4, height / 4); 405 OH_Drawing_PathLineTo(path, width * 3 / 4, height * 3 / 4); 406 OH_Drawing_PathLineTo(path, width / 4, height * 3 / 4); 407 OH_Drawing_PathLineTo(path, width / 4, height / 4); 408 OH_Drawing_PathClose(path); 409 410 auto pen = OH_Drawing_PenCreate(); 411 OH_Drawing_PenSetWidth(pen, 10); 412 OH_Drawing_PenSetColor(pen, OH_Drawing_ColorSetArgb(0xFF, 0xFF, 0x00, 0x00)); 413 OH_Drawing_CanvasAttachPen(canvas, pen); 414 415 OH_Drawing_CanvasDrawPath(canvas, path); 416 417 return nullptr; 418} 419 420EXTERN_C_START 421static napi_value Init(napi_env env, napi_value exports) 422{ 423 napi_property_descriptor desc[] = { 424 { "nativeOnDraw", nullptr, OnDraw, nullptr, nullptr, nullptr, napi_default, nullptr } 425 }; 426 napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); 427 return exports; 428} 429EXTERN_C_END 430 431static napi_module demoModule = { 432 .nm_version =1, 433 .nm_flags = 0, 434 .nm_filename = nullptr, 435 .nm_register_func = Init, 436 .nm_modname = "entry", 437 .nm_priv = ((void*)0), 438 .reserved = { 0 }, 439}; 440 441extern "C" __attribute__((constructor)) void RegisterEntryModule(void) 442{ 443 napi_module_register(&demoModule); 444} 445``` 446 447Add the following content to the **src/main/cpp/CMakeLists.txt** file of the project: 448```cmake 449# the minimum version of CMake. 450cmake_minimum_required(VERSION 3.4.1) 451project(NapiTest) 452 453set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) 454 455include_directories(${NATIVERENDER_ROOT_PATH} 456 ${NATIVERENDER_ROOT_PATH}/include) 457 458add_library(entry SHARED native_bridge.cpp) 459target_link_libraries(entry PUBLIC libace_napi.z.so) 460target_link_libraries(entry PUBLIC libace_ndk.z.so) 461target_link_libraries(entry PUBLIC libnative_drawing.so) 462``` 463 464Add the definition of the custom drawing API on the ArkTS side to the **src/main/cpp/types/libentry/index.d.ts** file of the project. The following is an example: 465```ts 466import { DrawContext } from '@kit.ArkUI' 467 468export const nativeOnDraw: (id: number, context: DrawContext, width: number, height: number) => number; 469``` 470 471Code in ArkTS: 472 473```ts 474// Index.ets 475import bridge from "libentry.so" // This .so file is written and generated by Node-API. 476import { DrawContext, FrameNode, NodeController, RenderNode } from '@kit.ArkUI' 477 478class MyRenderNode extends RenderNode { 479 draw(context: DrawContext) { 480 // The width and height in the context need to be converted from vp to px. 481 bridge.nativeOnDraw(0, context, vp2px(context.size.height), vp2px(context.size.width)); 482 } 483} 484 485class MyNodeController extends NodeController { 486 private rootNode: FrameNode | null = null; 487 488 makeNode(uiContext: UIContext): FrameNode | null { 489 this.rootNode = new FrameNode(uiContext); 490 491 const rootRenderNode = this.rootNode.getRenderNode(); 492 if (rootRenderNode !== null) { 493 const renderNode = new MyRenderNode(); 494 renderNode.size = { width: 100, height: 100 } 495 rootRenderNode.appendChild(renderNode); 496 } 497 return this.rootNode; 498 } 499} 500 501@Entry 502@Component 503struct Index { 504 private myNodeController: MyNodeController = new MyNodeController(); 505 506 build() { 507 Row() { 508 NodeContainer(this.myNodeController) 509 } 510 } 511} 512``` 513 514## Setting the Label 515 516You can use the [label](../reference/apis-arkui/js-apis-arkui-renderNode.md#label12) API to assign labels for RenderNodes. This makes it easier to distinguish between nodes under node **Inspector**. 517 518```ts 519import { RenderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'; 520 521class MyNodeController extends NodeController { 522 private rootNode: FrameNode | null = null; 523 524 makeNode(uiContext: UIContext): FrameNode | null { 525 this.rootNode = new FrameNode(uiContext); 526 const renderNode: RenderNode | null = this.rootNode.getRenderNode(); 527 if (renderNode !== null) { 528 const renderChildNode: RenderNode = new RenderNode(); 529 renderChildNode.frame = { x: 0, y: 0, width: 100, height: 100 }; 530 renderChildNode.backgroundColor = 0xffff0000; 531 renderChildNode.label = 'customRenderChildNode'; 532 console.log('label:', renderChildNode.label); 533 renderNode.appendChild(renderChildNode); 534 } 535 536 return this.rootNode; 537 } 538} 539 540@Entry 541@Component 542struct Index { 543 private myNodeController: MyNodeController = new MyNodeController(); 544 545 build() { 546 Column() { 547 NodeContainer(this.myNodeController) 548 .width(300) 549 .height(700) 550 .backgroundColor(Color.Gray) 551 } 552 } 553} 554``` 555