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