鸿蒙高级特性 - 动态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)
})
}
}
动态生成页面实践案例
未完待续