1# 自定义渲染节点 (RenderNode)
2
3## 概述
4
5对于不具备自己的渲染环境的三方框架,尽管已实现前端解析、布局及事件处理等功能,但仍需依赖系统的基础渲染和动画能力。[FrameNode](./arkts-user-defined-arktsNode-frameNode.md)上的通用属性与通用事件对这类框架而言是冗余的,会导致多次不必要的操作,涵盖布局、事件处理等逻辑。
6
7自定义渲染节点 ([RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md))是更加轻量的渲染节点,仅具备与渲染相关的功能。它提供了设置基础渲染属性的能力,以及节点的动态添加、删除和自定义绘制的能力。RenderNode能够为第三方框架提供基础的渲染和动画支持。
8
9## 创建和删除节点
10
11RenderNode提供了节点创建和删除的能力。可以通过RenderNode的构造函数创建自定义的RenderNode节点。通过构造函数创建的节点对应一个实体的节点。同时,可以通过RenderNode中的[dispose](../reference/apis-arkui/js-apis-arkui-renderNode.md#dispose12)接口来实现与实体节点的绑定关系的解除。
12
13## 操作节点树
14
15RenderNode提供了节点的增、删、查、改的能力,能够修改节点的子树结构;可以对所有RenderNode的节点的父子节点做出查询操作,并返回查询结果。
16
17> **说明:**
18>
19> - RenderNode中查询获取得到的子树结构按照开发通过RenderNode的接口传递的参数构建。
20>
21> - RenderNode如果要与系统直接结合显示,使用需要依赖FrameNode中获取的RenderNode进行挂载上树。
22
23```ts
24import { FrameNode, NodeController, RenderNode } from '@kit.ArkUI';
25
26const renderNode = new RenderNode();
27renderNode.frame = { x: 0, y: 0, width: 200, height: 350 };
28renderNode.backgroundColor = 0xffff0000;
29for (let i = 0; i < 5; i++) {
30  const node = new RenderNode();
31  // 设置node节点的Frame大小
32  node.frame = { x: 10, y: 10 + 60 * i, width: 50, height: 50 };
33  // 设置node节点的背景颜色
34  node.backgroundColor = 0xff00ff00;
35  // 将新增节点挂载在renderNode上
36  renderNode.appendChild(node);
37}
38
39class MyNodeController extends NodeController {
40  private rootNode: FrameNode | null = null;
41
42  makeNode(uiContext: UIContext): FrameNode | null {
43    this.rootNode = new FrameNode(uiContext);
44
45    const rootRenderNode = this.rootNode?.getRenderNode();
46    if (rootRenderNode) {
47      rootRenderNode.appendChild(renderNode);
48    }
49    return this.rootNode;
50  }
51}
52
53@Entry
54@Component
55struct Index {
56  private myNodeController: MyNodeController = new MyNodeController();
57
58  build() {
59    Row() {
60      NodeContainer(this.myNodeController)
61        .width(200)
62        .height(350)
63      Button('getNextSibling')
64        .onClick(() => {
65          const child = renderNode.getChild(1);
66          const nextSibling = child!.getNextSibling()
67          if (child === null || nextSibling === null) {
68            console.log('the child or nextChild is null');
69          } else {
70            // 获取子节点的位置信息
71            console.log(`the position of child is x: ${child.position.x}, y: ${child.position.y}, ` +
72            `the position of nextSibling is x: ${nextSibling.position.x}, y: ${nextSibling.position.y}`);
73          }
74        })
75    }
76  }
77}
78```
79
80## 设置和获取渲染相关属性
81
82RenderNode中可以设置渲染相关的属性,包括:[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)等。具体属性支持范围参考[RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md#rendernode)接口说明。
83
84> **说明:**
85>
86> - RenderNode中查询获取得到的属性为设置的属性值。
87>
88> - 若未传入参数或者传入参数为非法值则查询获得的为默认值。
89>
90> - 不建议对BuilderNode中的RenderNode进行修改操作。BuilderNode中具体属性设置是由状态管理实现的,属性更新的时序开发者不可控,BuilderNode和FrameNode中同时设置RenderNode属性可能会导致RenderNode属性设置与预期不相符。
91
92```ts
93import {  RenderNode, FrameNode, NodeController, ShapeMask, ShapeClip } from '@kit.ArkUI';
94
95const mask = new ShapeMask();
96mask.setRectShape({ left: 0, right: 150, top: 0, bottom: 150 });
97mask.fillColor = 0X55FF0000;
98mask.strokeColor = 0XFFFF0000;
99mask.strokeWidth = 24;
100
101const clip = new ShapeClip();
102clip.setCommandPath({ commands: "M100 0 L0 100 L50 200 L150 200 L200 100 Z" });
103
104const renderNode = new RenderNode();
105renderNode.backgroundColor = 0xffff0000;
106renderNode.size = { width: 100, height: 100 };
107
108class MyNodeController extends NodeController {
109  private rootNode: FrameNode | null = null;
110
111  makeNode(uiContext: UIContext): FrameNode | null {
112    this.rootNode = new FrameNode(uiContext);
113
114    const rootRenderNode = this.rootNode.getRenderNode();
115    if (rootRenderNode !== null) {
116      rootRenderNode.appendChild(renderNode);
117    }
118
119    return this.rootNode;
120  }
121}
122
123@Entry
124@Component
125struct Index {
126  private myNodeController: MyNodeController = new MyNodeController();
127
128  build() {
129    Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
130      Column() {
131        NodeContainer(this.myNodeController)
132      }
133      Button("position")
134        .width(300)
135        .onClick(() => {
136          renderNode.position = { x: 10, y: 10 };
137          console.log("renderNode position:" + JSON.stringify(renderNode.position));
138        })
139      Button("pivot")
140        .width(300)
141        .onClick(() => {
142          renderNode.pivot = { x: 0.5, y: 0.6 };
143          console.log("renderNode pivot:" + JSON.stringify(renderNode.pivot));
144        })
145      Button("scale")
146        .width(300)
147        .onClick(() => {
148          renderNode.scale = { x: 0.5, y: 1 };
149          console.log("renderNode scale:" + JSON.stringify(renderNode.scale));
150        })
151      Button("translation")
152        .width(300)
153        .onClick(() => {
154          renderNode.translation = { x: 100, y: 0 };
155          console.log("renderNode translation:" + JSON.stringify(renderNode.translation));
156        })
157      Button("rotation")
158        .width(300)
159        .onClick(() => {
160          renderNode.rotation = { x: 45, y: 0, z: 0 };
161          console.log("renderNode rotation:" + JSON.stringify(renderNode.rotation));
162        })
163      Button("transform")
164        .width(300)
165        .onClick(() => {
166          renderNode.transform = [
167            1, 0, 0, 0,
168            0, 2, 0, 0,
169            0, 0, 1, 0,
170            0, 0, 0, 1
171          ];
172          console.log("renderNode transform:" + JSON.stringify(renderNode.transform));
173        })
174      Button("shadow")
175        .width(300)
176        .onClick(() => {
177          renderNode.shadowElevation = 10;
178          renderNode.shadowColor = 0XFF00FF00;
179          renderNode.shadowOffset = { x: 10, y: 10 };
180          renderNode.shadowAlpha = 0.1;
181          console.log("renderNode shadowElevation:" + JSON.stringify(renderNode.shadowElevation));
182          console.log("renderNode shadowColor:" + JSON.stringify(renderNode.shadowColor));
183          console.log("renderNode shadowOffset:" + JSON.stringify(renderNode.shadowOffset));
184          console.log("renderNode shadowAlpha:" + JSON.stringify(renderNode.shadowAlpha));
185        })
186      Button("shadowRadius")
187        .width(300)
188        .onClick(() => {
189          renderNode.shadowOffset = { x: 10, y: 10 };
190          renderNode.shadowAlpha = 0.7
191          renderNode.shadowRadius = 30;
192          console.log("renderNode shadowOffset:" + JSON.stringify(renderNode.shadowOffset));
193          console.log("renderNode shadowAlpha:" + JSON.stringify(renderNode.shadowAlpha));
194          console.log("renderNode shadowRadius:" + JSON.stringify(renderNode.shadowRadius));
195        })
196      Button("border")
197        .width(300)
198        .onClick(() => {
199          renderNode.borderWidth = { left: 8, top: 8, right: 8, bottom: 8 };
200          renderNode.borderStyle = {
201            left: BorderStyle.Solid,
202            top: BorderStyle.Dotted,
203            right: BorderStyle.Dashed,
204            bottom: BorderStyle.Solid
205          }
206          renderNode.borderColor = { left: 0xFF0000FF, top: 0xFF0000FF, right: 0xFF0000FF, bottom: 0xFF0000FF };
207          renderNode.borderRadius = { topLeft: 32, topRight: 32, bottomLeft: 32, bottomRight: 32 };
208          console.log("renderNode borderWidth:" + JSON.stringify(renderNode.borderWidth));
209          console.log("renderNode borderStyle:" + JSON.stringify(renderNode.borderStyle));
210          console.log("renderNode borderColor:" + JSON.stringify(renderNode.borderColor));
211          console.log("renderNode borderRadius:" + JSON.stringify(renderNode.borderRadius));
212        })
213      Button("shapeMask")
214        .width(300)
215        .onClick(() => {
216          renderNode.shapeMask = mask;
217          console.log("renderNode shapeMask:" + JSON.stringify(renderNode.shapeMask));
218        })
219      Button("shapeClip")
220        .width(300)
221        .onClick(() => {
222          renderNode.shapeClip = clip;
223          console.log("renderNode shapeMask:" + JSON.stringify(renderNode.shapeMask));
224        })
225    }
226    .padding({ left: 35, right: 35, top: 35, bottom: 35 })
227    .width("100%")
228    .height("100%")
229  }
230}
231```
232
233## 自定义绘制
234
235通过重写RenderNode中的[draw](../reference/apis-arkui/js-apis-arkui-renderNode.md#draw)方法,可以自定义RenderNode的绘制内容,通过[invalidate](../reference/apis-arkui/js-apis-arkui-renderNode.md#invalidate)接口可以主动触发节点的重新绘制。
236
237> **说明:**
238>
239> - 同时同步触发多个invalidate仅会触发一次重新绘制。
240>
241> - 自定义绘制有两种绘制方式:通过ArkTS接口进行调用和通过Node-API进行调用。
242
243**ArkTS接口调用示例:**
244
245```ts
246import { FrameNode, NodeController, RenderNode } from '@kit.ArkUI';
247import { drawing } from '@kit.ArkGraphics2D';
248
249class MyRenderNode extends RenderNode {
250  draw(context: DrawContext) {
251    // 获取canvas对象
252    const canvas = context.canvas;
253    // 创建笔刷
254    const brush = new drawing.Brush();
255    // 设置笔刷颜色
256    brush.setColor({ alpha: 255, red: 255, green: 0, blue: 0 });
257    canvas.attachBrush(brush);
258    // 绘制矩阵
259    canvas.drawRect({ left: 0, right: 200, top: 0, bottom: 200 });
260    canvas.detachBrush();
261  }
262}
263
264const renderNode = new MyRenderNode();
265renderNode.frame = { x: 0, y: 0, width: 300, height: 300 };
266renderNode.backgroundColor = 0xff0000ff;
267renderNode.opacity = 0.5;
268
269class MyNodeController extends NodeController {
270  private rootNode: FrameNode | null = null;
271
272  makeNode(uiContext: UIContext): FrameNode | null {
273    this.rootNode = new FrameNode(uiContext);
274
275    const rootRenderNode = this.rootNode?.getRenderNode();
276    if (rootRenderNode !== null) {
277      rootRenderNode.frame = { x: 0, y: 0, width: 500, height: 500 }
278      rootRenderNode.appendChild(renderNode);
279    }
280
281    return this.rootNode;
282  }
283}
284
285@Entry
286@Component
287struct Index {
288  private myNodeController: MyNodeController = new MyNodeController();
289
290  build() {
291    Column() {
292      NodeContainer(this.myNodeController)
293        .width('100%')
294      Button('Invalidate')
295        .onClick(() => {
296          // 同步调用多次,仅触发一次重绘
297          renderNode.invalidate();
298          renderNode.invalidate();
299        })
300    }
301  }
302}
303```
304
305**Node-API调用示例:**
306
307C++侧可通过Node-API来获取Canvas,并进行后续的自定义绘制操作。
308
309```c++
310// native_bridge.cpp
311#include "napi/native_api.h"
312#include <native_drawing/drawing_canvas.h>
313#include <native_drawing/drawing_color.h>
314#include <native_drawing/drawing_path.h>
315#include <native_drawing/drawing_pen.h>
316
317static napi_value OnDraw(napi_env env, napi_callback_info info)
318{
319    size_t argc = 4;
320    napi_value args[4] = { nullptr };
321    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
322
323    int32_t id;
324    napi_get_value_int32(env, args[0], &id);
325
326    // 获取 Canvas 指针
327    void* temp = nullptr;
328    napi_unwrap(env, args[1], &temp);
329    OH_Drawing_Canvas *canvas = reinterpret_cast<OH_Drawing_Canvas*>(temp);
330
331    // 获取 Canvas 宽度
332    int32_t width;
333    napi_get_value_int32(env, args[2], &width);
334
335    // 获取 Canvas 高度
336    int32_t height;
337    napi_get_value_int32(env, args[3], &height);
338
339    // 传入canvas、height、width等信息至绘制函数中进行自定义绘制
340    auto path = OH_Drawing_PathCreate();
341    OH_Drawing_PathMoveTo(path, width / 4, height / 4);
342    OH_Drawing_PathLineTo(path, width * 3 / 4, height / 4);
343    OH_Drawing_PathLineTo(path, width * 3 / 4, height * 3 / 4);
344    OH_Drawing_PathLineTo(path, width / 4, height * 3 / 4);
345    OH_Drawing_PathLineTo(path, width / 4, height / 4);
346    OH_Drawing_PathClose(path);
347
348    auto pen = OH_Drawing_PenCreate();
349    OH_Drawing_PenSetWidth(pen, 10);
350    OH_Drawing_PenSetColor(pen, OH_Drawing_ColorSetArgb(0xFF, 0xFF, 0x00, 0x00));
351    OH_Drawing_CanvasAttachPen(canvas, pen);
352
353    OH_Drawing_CanvasDrawPath(canvas, path);
354
355    return nullptr;
356}
357
358EXTERN_C_START
359static napi_value Init(napi_env env, napi_value exports)
360{
361    napi_property_descriptor desc[] = {
362        { "nativeOnDraw", nullptr, OnDraw, nullptr, nullptr, nullptr, napi_default, nullptr }
363    };
364    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
365    return exports;
366}
367EXTERN_C_END
368
369static napi_module demoModule = {
370    .nm_version =1,
371    .nm_flags = 0,
372    .nm_filename = nullptr,
373    .nm_register_func = Init,
374    .nm_modname = "entry",
375    .nm_priv = ((void*)0),
376    .reserved = { 0 },
377};
378
379extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
380{
381    napi_module_register(&demoModule);
382}
383```
384
385修改工程中的`src/main/cpp/CMakeLists.txt`文件,添加如下内容:
386```cmake
387# the minimum version of CMake.
388cmake_minimum_required(VERSION 3.4.1)
389project(NapiTest)
390
391set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
392
393include_directories(${NATIVERENDER_ROOT_PATH}
394                    ${NATIVERENDER_ROOT_PATH}/include)
395
396add_library(entry SHARED native_bridge.cpp)
397target_link_libraries(entry PUBLIC libace_napi.z.so)
398target_link_libraries(entry PUBLIC libace_ndk.z.so)
399target_link_libraries(entry PUBLIC libnative_drawing.so)
400```
401
402同时在工程中的`src/main/cpp/types/libentry/index.d.ts`文件中,添加自定义绘制函数在ArkTS侧的定义,如:
403```ts
404import { DrawContext } from '@kit.ArkUI'
405
406export const nativeOnDraw: (id: number, context: DrawContext, width: number, height: number) => number;
407```
408
409ArkTS侧代码:
410
411```ts
412// Index.ets
413import bridge from "libentry.so" // 该 so 由 Node-API 编写并生成
414import { DrawContext, FrameNode, NodeController, RenderNode } from '@kit.ArkUI'
415
416class MyRenderNode extends RenderNode {
417  draw(context: DrawContext) {
418    // 需要将 context 中的宽度和高度从vp转换为px
419    bridge.nativeOnDraw(0, context, vp2px(context.size.height), vp2px(context.size.width));
420  }
421}
422
423class MyNodeController extends NodeController {
424  private rootNode: FrameNode | null = null;
425
426  makeNode(uiContext: UIContext): FrameNode | null {
427    this.rootNode = new FrameNode(uiContext);
428
429    const rootRenderNode = this.rootNode.getRenderNode();
430    if (rootRenderNode !== null) {
431      const renderNode = new MyRenderNode();
432      renderNode.size = { width: 100, height: 100 }
433      rootRenderNode.appendChild(renderNode);
434    }
435    return this.rootNode;
436  }
437}
438
439@Entry
440@Component
441struct Index {
442  private myNodeController: MyNodeController = new MyNodeController();
443
444  build() {
445    Row() {
446      NodeContainer(this.myNodeController)
447    }
448  }
449}
450```
451
452## 设置标签
453
454开发者可利用[label](../reference/apis-arkui/js-apis-arkui-renderNode.md#label12)接口向RenderNode设置标签信息,这有助于在节点Inspector中更清晰地区分各节点。
455
456```ts
457import {  RenderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
458
459class MyNodeController extends NodeController {
460  private rootNode: FrameNode | null = null;
461
462  makeNode(uiContext: UIContext): FrameNode | null {
463    this.rootNode = new FrameNode(uiContext);
464    const renderNode: RenderNode | null = this.rootNode.getRenderNode();
465    if (renderNode !== null) {
466      const renderChildNode: RenderNode = new RenderNode();
467      renderChildNode.frame = { x: 0, y: 0, width: 100, height: 100 };
468      renderChildNode.backgroundColor = 0xffff0000;
469      renderChildNode.label = 'customRenderChildNode';
470      console.log('label:', renderChildNode.label);
471      renderNode.appendChild(renderChildNode);
472    }
473
474    return this.rootNode;
475  }
476}
477
478@Entry
479@Component
480struct Index {
481  private myNodeController: MyNodeController = new MyNodeController();
482
483  build() {
484    Column() {
485      NodeContainer(this.myNodeController)
486        .width(300)
487        .height(700)
488        .backgroundColor(Color.Gray)
489    }
490  }
491}
492```
493