• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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