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