• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 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  // 设置node节点的Frame大小
38  node.frame = {
39    x: 10,
40    y: 10 + 60 * i,
41    width: 50,
42    height: 50
43  };
44  // 设置node节点的背景颜色
45  node.backgroundColor = 0xff00ff00;
46  // 将新增节点挂载在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            // 获取子节点的位置信息
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## 设置和获取渲染相关属性
92
93RenderNode中可以设置渲染相关的属性,包括:[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)接口说明。
94
95> **说明:**
96>
97> - RenderNode中查询获取得到的属性为设置的属性值。
98>
99> - 若未传入参数或者传入参数为非法值则查询获得的为默认值。
100>
101> - 不建议对BuilderNode中的RenderNode进行修改操作。BuilderNode中具体属性设置是由状态管理实现的,属性更新的时序开发者不可控,BuilderNode和FrameNode中同时设置RenderNode属性可能会导致RenderNode属性设置与预期不相符。
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## 自定义绘制
272
273通过重写RenderNode中的[draw](../reference/apis-arkui/js-apis-arkui-renderNode.md#draw)方法,可以自定义RenderNode的绘制内容,通过[invalidate](../reference/apis-arkui/js-apis-arkui-renderNode.md#invalidate)接口可以主动触发节点的重新绘制。
274
275> **说明:**
276>
277> - 同时同步触发多个invalidate仅会触发一次重新绘制。
278>
279> - 自定义绘制有两种绘制方式:通过ArkTS接口进行调用和通过Node-API进行调用。
280
281**ArkTS接口调用示例:**
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    // 获取canvas对象
292    const canvas = context.canvas;
293    // 创建笔刷
294    const brush = new drawing.Brush();
295    // 设置笔刷颜色
296    brush.setColor({
297      alpha: 255,
298      red: 255,
299      green: 0,
300      blue: 0
301    });
302    canvas.attachBrush(brush);
303    // 绘制矩阵
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          // 同步调用多次,仅触发一次重绘,draw回调中的日志仅打印一次
358          renderNode.width += 10;
359          renderNode.invalidate();
360          renderNode.invalidate();
361        })
362    }
363  }
364}
365```
366
367**Node-API调用示例:**
368
369C++侧可通过Node-API来获取Canvas,并进行后续的自定义绘制操作。
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    // 获取 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    // 获取 Canvas 宽度
394    int32_t width;
395    napi_get_value_int32(env, args[2], &width);
396
397    // 获取 Canvas 高度
398    int32_t height;
399    napi_get_value_int32(env, args[3], &height);
400
401    // 传入canvas、height、width等信息至绘制函数中进行自定义绘制
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
447修改工程中的`src/main/cpp/CMakeLists.txt`文件,添加如下内容:
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
464同时在工程中的`src/main/cpp/types/libentry/index.d.ts`文件中,添加自定义绘制函数在ArkTS侧的定义,如:
465```ts
466import { DrawContext } from '@kit.ArkUI'
467
468export const nativeOnDraw: (id: number, context: DrawContext, width: number, height: number) => number;
469```
470
471ArkTS侧代码:
472
473```ts
474// Index.ets
475import bridge from "libentry.so" // 该 so 由 Node-API 编写并生成
476import { DrawContext, FrameNode, NodeController, RenderNode } from '@kit.ArkUI'
477
478class MyRenderNode extends RenderNode {
479  uiContext: UIContext;
480
481  constructor(uiContext: UIContext) {
482    super();
483    this.uiContext = uiContext;
484  }
485
486  draw(context: DrawContext) {
487    // 需要将 context 中的宽度和高度从vp转换为px
488    bridge.nativeOnDraw(0, context, this.uiContext.vp2px(context.size.height), this.uiContext.vp2px(context.size.width));
489  }
490}
491
492class MyNodeController extends NodeController {
493  private rootNode: FrameNode | null = null;
494
495  makeNode(uiContext: UIContext): FrameNode | null {
496    this.rootNode = new FrameNode(uiContext);
497
498    const rootRenderNode = this.rootNode.getRenderNode();
499    if (rootRenderNode !== null) {
500      const renderNode = new MyRenderNode(uiContext);
501      renderNode.size = { width: 100, height: 100 }
502      rootRenderNode.appendChild(renderNode);
503    }
504    return this.rootNode;
505  }
506}
507
508@Entry
509@Component
510struct Index {
511  private myNodeController: MyNodeController = new MyNodeController();
512
513  build() {
514    Row() {
515      NodeContainer(this.myNodeController)
516    }
517  }
518}
519```
520
521## 设置标签
522
523开发者可利用[label](../reference/apis-arkui/js-apis-arkui-renderNode.md#label12)接口向RenderNode设置标签信息,这有助于在节点Inspector中更清晰地区分各节点。
524
525```ts
526import {  RenderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
527
528class MyNodeController extends NodeController {
529  private rootNode: FrameNode | null = null;
530
531  makeNode(uiContext: UIContext): FrameNode | null {
532    this.rootNode = new FrameNode(uiContext);
533    const renderNode: RenderNode | null = this.rootNode.getRenderNode();
534    if (renderNode !== null) {
535      const renderChildNode: RenderNode = new RenderNode();
536      renderChildNode.frame = { x: 0, y: 0, width: 100, height: 100 };
537      renderChildNode.backgroundColor = 0xffff0000;
538      renderChildNode.label = 'customRenderChildNode';
539      console.log('label:', renderChildNode.label);
540      renderNode.appendChild(renderChildNode);
541    }
542
543    return this.rootNode;
544  }
545}
546
547@Entry
548@Component
549struct Index {
550  private myNodeController: MyNodeController = new MyNodeController();
551
552  build() {
553    Column() {
554      NodeContainer(this.myNodeController)
555        .width(300)
556        .height(700)
557        .backgroundColor(Color.Gray)
558    }
559  }
560}
561```
562