鸿蒙高级特性 - 动态UI加载

参考文档:

文档中心

文档中心

背景

为了解决页面、组件加载缓慢的问题,ArkUI框架提供了动态操作以实现组件预创建,并允许应用在运行时根据实际需要加载渲染相应的组件。

说白了,就是为了解决声明式组件开发的痛点。

概述

动态操作包含

**动态创建组件(动态添加组件):**动态创建组件指在非build生命周期中进行组件创建,即在build生命周期前提前创建组件。

通过动态创建组件,不但可以节省组件创建的时间,提升用户体验,还可以将独立的逻辑进行封装,有助于应用模块化开发。

动态卸载组件(动态删除组件)

动态卸载组件是对动态创建的组件进行卸载、删除。

组件预创建原理

在声明式范式中,组件仅在build环节中被创建,开发者无法在其他生命周期阶段进行组件的创建,从而引起页面加载慢等问题。与声明式范式不同,ArkUI框架提供的UI动态操作支持组件的预创建。组件预创建可以满足开发者在非build生命周期中进行组件创建,创建后的组件可以进行属性设置、布局计算等操作。之后在页面加载时进行使用,可以极大提升页面响应速度。

如下图所示,利用组件预创建机制,可以利用动画执行过程空闲时间进行组件预创建和属性设置。在动画结束后,再进行属性和布局的更新,节省了组件创建的时间,从而加快了页面渲染。

组件动态化使用

接口说明和实例

添加/删除节点
// 实现NodeController
class TextNodeController extends NodeController {
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = '';

  constructor(message: string) {
    super();
    this.message = message;
  }

  makeNode(context: UIContext): FrameNode | null {
    // 创建BuilderNode实例
    this.textNode = new BuilderNode(context);
    // 设置selfIdealSize属性
    // this.textNode = new BuilderNode(context, {selfIdealSize: {width: 100, height :100}});
    // 使用build方法构建组件树
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
    // 返回需要显示的节点
    return this.textNode.getFrameNode();
  }
}

// 显示动态节点
// 使用if渲染语句,删除动态节点
@Entry
@Component
struct Index {
  @State message: string = "hello";
  private textNodeController: TextNodeController = new TextNodeController(this.message);
  
  build() {
    Row() {
      Column() {
        if (this.isShow) {
          NodeContainer(this.textNodeController)
            .width('100%')
            .height(100)
            .backgroundColor('#FFF0F0F0')
        }
        Button('isShow')
          .onClick(() => {
            this.isShow = false;
          })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}
更新节点

update(arg: Object): void

根据提供的参数更新BuilderNode,该参数为build方法调用时传入的参数类型相同。对自定义组件进行update的时候需要在自定义组件中使用的变量定义为@Prop类型。【@Trace 应该也行???有待试验!!!】

import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";

class Params {
  text: string = ""
  constructor(text: string) {
    this.text = text;
  }
}

class TextNodeController extends NodeController {
  private rootNode: FrameNode | null = null;
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = "";

  constructor(message: string) {
    super()
    this.message = message
  }

  makeNode(context: UIContext): FrameNode | null {
    this.textNode = new BuilderNode(context);
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
    return this.textNode.getFrameNode();
  }

  update(message: string) {
    if (this.textNode !== null) {
      this.textNode.update(new Params(message));
    }
  }
}

@Entry
@Component
struct Index {
  @State message: string = "hello"
  private textNodeController: TextNodeController = new TextNodeController(this.message);
  private count = 0;

  build() {
    Row() {
      Column() {
        NodeContainer(this.textNodeController)
          .width('100%')
          .height(200)
          .backgroundColor('#FFF0F0F0')
        Button('Update')
          .onClick(() => {
            this.count += 1;
            const message = "Update " + this.count.toString();
            this.textNodeController.update(message);
          })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}
动态更新组件

动态将NodeContainer上的节点替换,依赖于NodeController的makeNode和rebuild方法。rebuild方法会触发makeNode的回调,刷新NodeContainer上显示的节点;makeNode方法返回的为null,则移除NodeContainer下挂载的节点。

class TextNodeController extends NodeController {
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = '';

  constructor(message: string) {
    super();
    this.message = message;
  }

  makeNode(context: UIContext): FrameNode | null {
    // 加上判空处理,只有第一次创建BuilderNode时,才会执行下列代码;替换节点时,textNode不为null
    if (this.textNode == null) {
      this.textNode = new BuilderNode(context);
      this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
    }

    return this.textNode.getFrameNode();
  }

  replaceBuilderNode(newNode: BuilderNode<Object[]>) {
    this.textNode = newNode;
    // rebuild方法会重新调用makeNode方法
    this.rebuild();
  }
}

@Entry
@Component
struct Index {
  @State message: string = "hello";
  @State isShow: boolean = true;
  private textNodeController: TextNodeController = new TextNodeController(this.message);
  // private count = 0;

  build() {
    Row() {
      Column() {
        if (this.isShow) {
          NodeContainer(this.textNodeController)
            .width('100%')
            .height(100)
            .backgroundColor('#FFF0F0F0')
        }
        Button('replaceNode')
          .onClick(() => {
            this.textNodeController.replaceBuilderNode(this.buildNewNode());
          })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }

  buildNewNode(): BuilderNode<[Params]> {
    let uiContext: UIContext = this.getUIContext();
    let message = 'newNode';
    let textNode = new BuilderNode<[Params]>(uiContext);
    textNode.build(wrapBuilder<[Params]>(buildText), new Params(message))
    return textNode;
  }
}
NodeController生命周期

NodeController用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。下面,对其常用生命周期函数进行说明。

  • makeNode必须要重写的方法,用于构建节点树、返回节点挂载在对应NodeContainer中。在对应NodeContainer创建绑定当前NodeController的时候调用、或者通过rebuild方法调用刷新。

  • aboutToResize当controller对应的NodeContainer在Mesure的时候进行回调,入参为节点的布局大小。

  • aboutToAppear当controller对应的NodeContainer在onAppear的时候进行回调。

  • aboutToDisappear当controller对应的NodeContainer在onDisappear的时候进行回调。

    export abstract class NodeController {
    abstract makeNode(uiContext: UIContext): FrameNode | null;
    aboutToResize?(size: Size): void;
    aboutToAppear?(): void;
    aboutToDisappear?(): void;
    rebuild(): void;
    onTouchEvent?(event: TouchEvent): void;
    }

问题:

动态节点组件什么时候挂树???

三方广告SDK实践案例一

总结:动态UI加载优化了性能之外,也利于组件解除耦合封装;框架层不需要关心业务模块的自定义组件长什么样子,通过动态挂载进来就ok了。

广告SDK提供的UI自定义组件

import { NodeController, BuilderNode, FrameNode } from '@kit.ArkUI';

// 广告SDK对外提供的广告组件节点的参数
class ADNodeParams {
  adImg: string = '';
  adText: string = '';
  adLink: string = '';

  constructor(adImg: string, adText: string, adLink: string) {
    this.adImg = adImg;
    this.adText = adText;
    this.adLink = adLink;
  }
}

// 广告SDK对外提供的广告组件节点的自定义构建函数
@Builder
function ADNodeBuilder(params: ADNodeParams) {
  //该广告节点示例,包含一个广告图片
  Stack() {
    Image($r(params.adImg))
      .objectFit(ImageFit.Contain)
      .height('100%')
      .width('100%')
  }.height('100%')
  .width('100%')
  .onClick(() => {
    // 跳转至对应的应用或页面
  })
}

框架层通过动态UI加载来挂载广告SDK提供的组件

// entry\src\main\ets\pages\ADNodeController.ets
// ...
// 自定义广告节点控制器
class ADNodeController extends NodeController {
  private node: BuilderNode<[ADNodeParams]> | null = null;

  // 当NodeController绑定的NodeContainer挂载显示时,触发此回调
  // 可以加一个打点,记录标明广告被真实的显示出来的时间
  aboutToAppear(): void {
    console.info('ADController aboutToAppear');
  }

  // 当NodeController绑定的NodeContainer卸载消失时,触发此回调
  // 可以加一个打点,记录标明广告退出的时间
  aboutToDisappear(): void {
    console.info('ADController aboutToDisappear');
  }

  // 当NodeController实例绑定的NodeContainer创建的时候进行回调。回调方法将返回一个节点,将该节点挂载至NodeContainer。
  makeNode(uiContext: UIContext): FrameNode | null {
    this.node = new BuilderNode<[ADNodeParams]>(uiContext);
    this.node.build(new WrappedBuilder<[ADNodeParams]>(ADNodeBuilder),
      new ADNodeParams('app.media.al_pc', '点击跳转至官网', ''));
    return this.node.getFrameNode();
  }

  // 更新渲染节点
  update(params: ADNodeParams) {
    if (this.node != null) {
      console.info(`update params:${JSON.stringify(params)}`)
      this.node.update(params)
    }
  }
}

import { NodeController } from '@kit.ArkUI';
import { ADNodeController } from './ADNodeController';

@Entry
@Component
struct Index {
  @State isShow: boolean = true;
  private controller: NodeController | null = new ADNodeController();

  build() {
    Stack() {
      if (this.isShow) {
        NodeContainer(this.controller)
        Button('跳过')
          .onClick(() => {
            this.isShow = false;
          })
      } else {
        Text('应用首页')
      }
    }
    .alignContent(this.isShow ? Alignment.TopEnd : Alignment.TopStart)
    .width('100%')
    .height('100%')
  }

动态添加节点案例二

class CubeNodeController extends NodeController {
  public rootNode: FrameNode | null = null;
  private frameNodeMap : Map<LIVMTSingleCubeAdapter, FrameNode> = new Map();

  makeNode(uiContext: UIContext): FrameNode | null {
    if (this.rootNode == null) {
      this.rootNode = new FrameNode(uiContext);
      this.rootNode.commonAttribute
        .width("100%")
        .height("100%")
    }
    return this.rootNode
  }

  addBuilderNode(adapter:LIVMTSingleCubeAdapter, newNode: BuilderNode<[CSCardInstance]>) {
    if (this.rootNode) {
      let frameNode = newNode.getFrameNode()
      if(frameNode) {
        this.rootNode.appendChild(frameNode)
        this.frameNodeMap.set(adapter, frameNode);
      }
    }
  }

  removeBuilderNode(adapter:LIVMTSingleCubeAdapter) {
    let frameNode = this.frameNodeMap.get(adapter);
    if(frameNode) {
      this.rootNode?.removeChild(frameNode);
      frameNode.dispose();
    }
  }
}

@Builder
function buildCube(instance: LDCardInstance) {
  //双击点赞lottie
  LDCardComponent({ instance: instance })
}


//cube容器组件 list cube 组件
@Component
export struct LIVMTSingleCubeView {
  @Consume("livingContext")
  context: ILivingContext;
  @Require
  taskSingleCubeVM ?: LIVMTSingleCubeVM;

  private cubeNodeController = new CubeNodeController();

  aboutToAppear(): void {
    this.cubeNodeController?.aboutToAppear?.();

    if(this.taskSingleCubeVM) {
      this.taskSingleCubeVM.viewCallback = {
        onAddCube: (adapter:LIVMTSingleCubeAdapter,instance: LDCardInstance) => {
          if(this.context.ui.showFullScreenStyleOnTab3(true)) {
            this.cubeNodeController?.addBuilderNode(adapter, this.buildNewNode(instance));
            return true;
          }
          return false;
        },
        onRemoveCube: (adapter:LIVMTSingleCubeAdapter) => {
          this.cubeNodeController?.removeBuilderNode(adapter);
        }
      };
    }
  }

  aboutToDisappear(): void {
    this.cubeNodeController?.aboutToDisappear?.();
  }

  buildNewNode(instance: LDCardInstance): BuilderNode<[LDCardInstance]> {
    let uiContext: UIContext = this.getUIContext();
    let cubeNode = new BuilderNode<[LDCardInstance]>(uiContext)
    cubeNode.build(wrapBuilder<[LDCardInstance]>(buildCube), instance)
    return cubeNode
  }

  build() {
    Stack() {
      NodeContainer(this.cubeNodeController)
        .width('100%')
        .height('100%')
    }
    .width("100%")
    .height("100%")
    .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
      this.taskSingleCubeVM?.resize(oldValue, newValue)
    })
    .onTouch((event) => {
      this.taskSingleCubeVM?.handleTouch(event)
    })
  }
}

动态生成页面实践案例

未完待续

相关推荐
play_big_knife5 小时前
鸿蒙项目云捐助第十六讲云捐助使用云数据库实现登录注册
数据库·华为云·harmonyos·鸿蒙·云开发·云数据库·鸿蒙开发
葡萄城技术团队5 小时前
共创共建!葡萄城 SpreadJS 完成 HarmonyOS NEXT 操作系统兼容认证
华为·harmonyos
海绵宝宝_9 小时前
【HarmonyOS NEXT】ArkTs数据类型解析与使用
android·前端·华为·harmonyos·鸿蒙
特立独行的猫a9 小时前
HarmonyOS NEXT 应用开发实战:音乐播放器的完整实现
华为·harmonyos
破-风9 小时前
FTP华为设备上进行配置
java·华为·restful
SuperHeroWu710 小时前
【HarmonyOS】获取设备自定义名字
华为·harmonyos·鸿蒙·设备名字·设备名称·本地设备名
浅陌sss12 小时前
Unity UI SafeArea适配
ui·unity·游戏引擎
文火冰糖的硅基工坊13 小时前
[创业之路-199]:《华为战略管理法-DSTE实战体系》- 3 - 价值转移理论与利润区理论
华为·产品经理·需求分析·产品·创业·战略
马剑威(威哥爱编程)14 小时前
鸿蒙 NEXT 开发之后台任务开发服务框架学习笔记
笔记·学习·harmonyos