harmony 鸿蒙自定义组件节点 (FrameNode)
自定义组件节点 (FrameNode)
概述
对于拥有自定义前端的第三方框架(如JSON、XML、DOM树等),需将特定的DSL转换为ArkUI的声明式描述。如下图描述了JSON定义的前端框架和ArkUI声明式描述的对应关系。
上述转换过程需要依赖额外的数据驱动,绑定至Builder中,较为复杂且性能欠佳。这类框架通常依赖于ArkUI的布局、事件处理、基础的节点操作和自定义能力。大部分组件通过自定义实现,但需结合使用部分系统组件以实现混合显示,如下图示例既使用了FrameNode的自定义方法进行绘制,又使用了系统组件Column及其子组件Text,通过BuilderNode的方式将其挂载到根节点的FrameNode上混合显示。
FrameNode的设计初衷正是为了解决上述转换问题。FrameNode表示组件树中的实体节点,与自定义占位容器组件NodeContainer相配合,实现在占位容器内构建一棵自定义的节点树。该节点树支持动态操作,如节点的增加、修改和删除。基础的FrameNode具备设置通用属性和事件回调的功能,同时提供完整的自定义能力,涵盖自定义测量、布局和绘制等方面。
除此之外,ArkUI还提供了获取和遍历系统组件对应代理FrameNode对象的能力(下文简称代理节点)。代理节点能够用于遍历整个UI的树形结构,支持获取系统组件节点的详细信息,以及额外注册组件的事件监听回调。
创建和删除节点
FrameNode提供了节点创建和删除的能力。可以通过FrameNode的构造函数创建自定义FrameNode节点,通过构造函数创建的节点对应一个实体的节点。同时,可以通过FrameNode中的dispose接口来实现与实体节点的绑定关系的解除。
说明:
在创建FrameNode对象的时候需要传入必选参数UIContext,若未传入UIContext对象或者传入不合法,则节点创建抛出异常。
自定义占位组件将节点进行显示的时候需要保证UI上下文一致,否则会出现显示异常。
若不持有FrameNode对象,则该对象会在GC的时候被回收。
判断节点是否可修改
isModifiable用于查询当前节点类型是否为系统组件的代理节点。当FrameNode节点作为系统组件的代理节点的时候,该节点不可修改。即无法修改代理节点的自身属性以及其子节点的结构。
获取对应的RenderNode节点
FrameNode提供了getRenderNode接口,用于获取FrameNode中的RenderNode。可以通过对获取到的RenderNode对象进行操作,动态修改FrameNode上绘制相关的属性,具体可修改的属性参考RenderNode的接口。
说明:
无法获取系统组件代理FrameNode的RenderNode对象。
BuilderNode中调用getFrameNode获取得到的FrameNode节点对象中,可以通过getRenderNode获取对应的根节点的RenderNode对象。
操作节点树
FrameNode提供了节点的增、删、查、改的能力,能够修改非代理节点的子树结构。可以对所有FrameNode的节点的父子节点做出查询操作,并返回查询结果。
说明:
对节点进行增、删、改操作的时候,会对非法操作抛出异常信息。
通过查询获得的系统组件的代理节点,仅具备查询节点信息的作用,不具备修改节点属性的功能。代理节点不持有组件的实体节点,即不影响对应的节点的生命周期。
查询节点仅查询获得UI相关的节点,不返回语法节点。
使用自定义组件的场景下,可能查询获得自定义组件的新增节点,节点类型为“__Common__”。
import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
const TEST_TAG: string = "FrameNode"
class Params {
text: string = "this is a text"
}
@Builder
function buttonBuilder(params: Params) {
Column({ space: 10 }) {
Button(params.text)
.fontSize(12)
.borderRadius(8)
.borderWidth(2)
.backgroundColor(Color.Orange)
Button(params.text)
.fontSize(12)
.borderRadius(8)
.borderWidth(2)
.backgroundColor(Color.Pink)
}
}
class MyNodeController extends NodeController {
public buttonNode: BuilderNode<[Params]>|null = null;
public frameNode: FrameNode|null = null;
public childList: Array<FrameNode> = new Array<FrameNode>();
public rootNode: FrameNode|null = null;
private uiContext: UIContext|null = null;
private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(buttonBuilder);
makeNode(uiContext: UIContext): FrameNode|null {
this.uiContext = uiContext;
if (this.rootNode == null) {
this.rootNode = new FrameNode(uiContext);
this.rootNode.commonAttribute
.width("50%")
.height(100)
.borderWidth(1)
.backgroundColor(Color.Gray)
}
if (this.frameNode == null) {
this.frameNode = new FrameNode(uiContext);
this.frameNode.commonAttribute
.width("100%")
.height(50)
.borderWidth(1)
.position({ x: 200, y: 0 })
.backgroundColor(Color.Pink);
this.rootNode.appendChild(this.frameNode);
}
if (this.buttonNode == null) {
this.buttonNode = new BuilderNode<[Params]>(uiContext);
this.buttonNode.build(this.wrapBuilder, { text: "This is a Button" })
this.rootNode.appendChild(this.buttonNode.getFrameNode())
}
return this.rootNode;
}
operationFrameNodeWithFrameNode(frameNode: FrameNode|undefined|null) {
if (frameNode) {
console.log(TEST_TAG + " get ArkTSNode success.")
console.log(TEST_TAG + " check rootNode whether is modifiable " + frameNode.isModifiable());
}
if (this.uiContext) {
let frameNode1 = new FrameNode(this.uiContext);
let frameNode2 = new FrameNode(this.uiContext);
frameNode1.commonAttribute.size({ width: 50, height: 50 })
.backgroundColor(Color.Black)
.position({ x: 50, y: 60 })
frameNode2.commonAttribute.size({ width: 50, height: 50 })
.backgroundColor(Color.Orange)
.position({ x: 120, y: 60 })
try {
frameNode?.appendChild(frameNode1);
console.log(TEST_TAG + " appendChild success ");
} catch (err) {
console.log(TEST_TAG + " appendChild fail :" + (err as BusinessError).code + " : " +
(err as BusinessError).message);
}
try {
frameNode?.insertChildAfter(frameNode2, null);
console.log(TEST_TAG + " insertChildAfter success ");
} catch (err) {
console.log(TEST_TAG + " insertChildAfter fail : " + (err as BusinessError).code + " : " +
(err as BusinessError).message);
}
setTimeout(() => {
try {
frameNode?.removeChild(frameNode?.getChild(0))
console.log(TEST_TAG + " removeChild success ");
} catch (err) {
console.log(TEST_TAG + " removeChild fail : " + (err as BusinessError).code + " : " +
(err as BusinessError).message);
}
}, 2000)
setTimeout(() => {
try {
frameNode?.clearChildren();
console.log(TEST_TAG + " clearChildren success ");
} catch (err) {
console.log(TEST_TAG + " clearChildren fail : " + (err as BusinessError).code + " : " +
(err as BusinessError).message);
}
}, 4000)
}
}
testInterfaceAboutSearch(frameNode: FrameNode|undefined|null): string {
let result: string = "";
if (frameNode) {
result = result + `current node is ${frameNode.getNodeType()} \n`;
result = result + `parent node is ${frameNode.getParent()?.getNodeType()} \n`;
result = result + `child count is ${frameNode.getChildrenCount()} \n`;
result = result + `first child node is ${frameNode.getFirstChild()?.getNodeType()} \n`;
result = result + `second child node is ${frameNode.getChild(1)?.getNodeType()} \n`;
result = result + `previousSibling node is ${frameNode.getPreviousSibling()?.getNodeType()} \n`;
result = result + `nextSibling node is ${frameNode.getNextSibling()?.getNodeType()} \n`;
}
return result;
}
checkAppendChild(parent: FrameNode|undefined|null, child: FrameNode|undefined|null) {
try {
if (parent && child) {
parent.appendChild(child);
console.log(TEST_TAG + " appendChild success ");
}
} catch (err) {
console.log(TEST_TAG + " appendChild fail : " + (err as BusinessError).code + " : " +
(err as BusinessError).message);
}
}
}
@Entry
@Component
struct Index {
@State index: number = 0;
@State result: string = ""
private myNodeController: MyNodeController = new MyNodeController();
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
List({ space: 20, initialIndex: 0 }) {
ListItem() {
Column({ space: 5 }) {
Text("验证FrameNode子节点的增、删、改功能")
Button("对自定义FrameNode进行操作")
.fontSize(16)
.width(400)
.onClick(() => {
// 对FrameNode节点进行增、删、改操作,正常实现。
this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.frameNode);
})
Button("对BuilderNode中的代理节点进行操作")
.fontSize(16)
.width(400)
.onClick(() => {
// 对BuilderNode代理节点进行增、删、改操作,捕获异常信息。
this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.buttonNode?.getFrameNode());
})
Button("对系统组件中的代理节点进行操作")
.fontSize(16)
.width(400)
.onClick(() => {
// 对代理节点进行增、删、改操作,捕获异常信息。
this.myNodeController.operationFrameNodeWithFrameNode(this.myNodeController?.rootNode?.getParent());
})
}
}
ListItem() {
Column({ space: 5 }) {
Text("验证FrameNode添加子节点的特殊场景")
Button("新增BuilderNode的代理节点")
.fontSize(16)
.width(400)
.onClick(() => {
let buttonNode = new BuilderNode<[Params]>(this.getUIContext());
buttonNode.build(wrapBuilder<[Params]>(buttonBuilder), { text: "BUTTON" })
this.myNodeController.checkAppendChild(this.myNodeController?.frameNode, buttonNode?.getFrameNode());
})
Button("新增系统组件代理节点")
.fontSize(16)
.width(400)
.onClick(() => {
this.myNodeController.checkAppendChild(this.myNodeController?.frameNode,
this.myNodeController?.rootNode?.getParent());
})
Button("新增已有父节点的自定义节点")
.fontSize(16)
.width(400)
.onClick(() => {
this.myNodeController.checkAppendChild(this.myNodeController?.frameNode,
this.myNodeController?.rootNode);
})
}
}
ListItem() {
Column({ space: 5 }) {
Text("验证FrameNode节点的查询功能")
Button("对自定义FrameNode进行操作")
.fontSize(16)
.width(400)
.onClick(() => {
// 对FrameNode节点进行进行查询。当前节点为NodeContainer的子节点。
this.result = this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.rootNode);
setTimeout(() => {
// 对FrameNode节点进行进行查询。rootNode下的第一个子节点。
this.result = this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.frameNode);
}, 2000)
})
Button("对BuilderNode中的代理节点进行操作")
.fontSize(16)
.width(400)
.onClick(() => {
// 对BuilderNode代理节点进行进行查询。当前节点为BuilderNode中的Column节点。
this.result =
this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.buttonNode?.getFrameNode());
})
Button("对系统组件中的代理节点进行操作")
.fontSize(16)
.width(400)
.onClick(() => {
// 对代理节点进行查询。当前节点为NodeContainer。
this.result =
this.myNodeController.testInterfaceAboutSearch(this.myNodeController?.rootNode?.getParent());
})
}
}
}.height("50%")
Text(`Result:\n${this.result}`)
.fontSize(16)
.width(400)
.height(200)
.padding(30)
.borderWidth(1)
Column() {
Text("This is a NodeContainer.")
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.width('100%')
.fontSize(16)
NodeContainer(this.myNodeController)
.borderWidth(1)
.width(400)
.height(150)
}
}
.padding({
left: 35,
right: 35,
top: 35,
bottom: 35
})
.width("100%")
.height("100%")
}
}
设置节点通用属性和事件回调
FrameNode提供了commonAttribute和commonEvent两个对象用于设置节点的通用属性和设置事件回调。
说明:
由于代理节点的属性不可修改,因此通过代理节点的commonAttribute修改节点的基础属性不生效。
设置的基础事件与系统组件定义的事件平行,参与事件竞争。设置的基础事件不覆盖系统组件事件。同时设置两个事件回调的时候,优先回调系统组件事件。
import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'
class Params {
text: string = "this is a text"
}
@Builder
function buttonBuilder(params: Params) {
Button(params.text)
.fontSize(12)
.borderRadius(8)
.borderWidth(2)
.backgroundColor(Color.Orange)
.onClick((event: ClickEvent) => {
console.log(`Button ${JSON.stringify(event)}`);
})
}
class MyNodeController extends NodeController {
public buttonNode: BuilderNode<[Params]>|null = null;
public frameNode: FrameNode|null = null;
public rootNode: FrameNode|null = null;
private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(buttonBuilder);
makeNode(uiContext: UIContext): FrameNode|null {
if (this.rootNode == null) {
this.rootNode = new FrameNode(uiContext);
// 对rootNode进行属性修改,该节点为自定义的FrameNode节点,修改生效
this.rootNode.commonAttribute
.width("100%")
.height(100)
.borderWidth(1)
.backgroundColor(Color.Gray)
}
if (this.frameNode == null) {
this.frameNode = new FrameNode(uiContext);
// 对frameNode进行属性修改,该节点为自定义的FrameNode节点,修改生效
this.frameNode.commonAttribute
.width("50%")
.height(50)
.borderWidth(1)
.backgroundColor(Color.Pink);
this.rootNode.appendChild(this.frameNode);
}
if (this.buttonNode == null) {
this.buttonNode = new BuilderNode<[Params]>(uiContext);
this.buttonNode.build(this.wrapBuilder, { text: "This is a Button" })
// 对BuilderNode中获取的FrameNode进行属性修改,该节点非自定义的FrameNode节点,修改不生效
this.buttonNode?.getFrameNode()?.commonAttribute.position({ x: 100, y: 100 })
this.rootNode.appendChild(this.buttonNode.getFrameNode())
}
return this.rootNode;
}
modifyNode(frameNode: FrameNode|null|undefined, sizeValue: SizeOptions, positionValue: Position) {
if (frameNode) {
frameNode.commonAttribute.size(sizeValue).position(positionValue);
}
}
addClickEvent(frameNode: FrameNode|null|undefined) {
if (frameNode) {
frameNode.commonEvent.setOnClick((event: ClickEvent) => {
console.log(`FrameNode ${JSON.stringify(event)}`);
})
}
}
}
@Entry
@Component
struct Index {
private myNodeController: MyNodeController = new MyNodeController();
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
Column({ space: 10 }) {
Text("修改节点通用属性-宽高")
Button("modify ArkTS-FrameNode")
.onClick(() => {
// 获取到的是当前页面中的开发者创建的FrameNode对象,该节点可修改。即节点大小与位置。
console.log("Check the weather the node can be modified " + this.myNodeController?.frameNode
?.isModifiable());
this.myNodeController.modifyNode(this.myNodeController?.frameNode, { width: 150, height: 100 }, {
x: 100,
y: 0
})
})
Button("modify FrameNode get by BuilderNode")
.onClick(() => {
// 获取到的是当前页面中的BuilderNode的根节点,该节点不可修改。即节点大小与位置未发生改变。
console.log("Check the weather the node can be modified " +
this.myNodeController?.buttonNode?.getFrameNode()
?.isModifiable());
this.myNodeController.modifyNode(this.myNodeController?.buttonNode?.getFrameNode(), {
width: 100,
height: 100
}, { x: 50, y: 50 })
})
Button("modify proxyFrameNode get by search")
.onClick(() => {
// rootNode调用getParent()获取到的是当前页面中的NodeContainer节点,该节点不可修改。即节点大小与位置未发生改变。
console.log("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
?.isModifiable());
this.myNodeController.modifyNode(this.myNodeController?.rootNode?.getParent(), {
width: 500,
height: 500
}, {
x: 0,
y: 0
})
})
}
.padding({
left: 35,
right: 35,
top: 35,
bottom: 35
})
Column({ space: 10 }) {
Text("修改节点点击事件")
Button("add click event to ArkTS-FrameNode")
.onClick(() => {
// 获取到的是当前页面中的开发者创建的FrameNode对象,该节点可增加点击事件。
// 增加的点击事件参与事件竞争,即点击事件会在该节点被消费且不不再向父组件冒泡。
console.log("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
?.isModifiable());
this.myNodeController.addClickEvent(this.myNodeController?.frameNode)
})
Button("add click event to FrameNode get by BuilderNode")
.onClick(() => {
// 获取到的是当前页面中的BuilderNode的根节点,该类节点可增加点击事件。
// 点击的时候优先回调通过系统组件接口设置的click事件回调,然后回调通过commonEvent增加的click监听。
console.log("Check the weather the node can be modified " +
this.myNodeController?.buttonNode?.getFrameNode()
?.isModifiable());
this.myNodeController.addClickEvent(this.myNodeController?.buttonNode?.getFrameNode())
})
Button("add click event to proxyFrameNode get by search")
.onClick(() => {
// rootNode调用getParent()获取到的是当前页面中的NodeContainer节点,该类节点可增加点击事件。
console.log("Check the weather the node can be modified " + this.myNodeController?.rootNode?.getParent()
?.isModifiable());
this.myNodeController.addClickEvent(this.myNodeController?.rootNode?.getParent());
})
}
.padding({
left: 35,
right: 35,
top: 35,
bottom: 35
})
NodeContainer(this.myNodeController)
.borderWidth(1)
.width("100%")
.height(100)
.onClick((event: ClickEvent) => {
console.log(`NodeContainer ${JSON.stringify(event)}`);
})
}
.padding({
left: 35,
right: 35,
top: 35,
bottom: 35
})
.width("100%")
.height("100%")
}
}
自定义测量布局与绘制
通过重写onDraw方法,可以自定义FrameNode的绘制内容。invalidate接口可以主动触发节点的重新绘制。
通过重写onMeasure可以自定义FrameNode的测量方式,使用measure可以主动传递布局约束触发重新测量。
通过重写onLayout方法可以自定义FrameNode的布局方式,使用layout方法可以主动传递位置信息并触发重新布局。
setNeedsLayout可以将当前节点标记,在下一帧触发重新布局。
说明:
对节点进行dispose解引用后,由于FrameNode对象不再对应一个实体节点,invalidate无法触发原有绑定节点的刷新。
通过onDraw方法进行的自定义绘制,绘制内容大小无法超出组件大小。
import { DrawContext, FrameNode, NodeController, Position, Size, UIContext, LayoutConstraint } from '@kit.ArkUI';
import { drawing } from '@kit.ArkGraphics2D';
function GetChildLayoutConstraint(constraint: LayoutConstraint, child: FrameNode): LayoutConstraint {
const size = child.getUserConfigSize();
const width = Math.max(
Math.min(constraint.maxSize.width, size.width.value),
constraint.minSize.width
);
const height = Math.max(
Math.min(constraint.maxSize.height, size.height.value),
constraint.minSize.height
);
const finalSize: Size = { width, height };
const res: LayoutConstraint = {
maxSize: finalSize,
minSize: finalSize,
percentReference: finalSize
};
return res;
}
class MyFrameNode extends FrameNode {
public width: number = 100;
public offsetY: number = 0;
private space: number = 1;
uiContext: UIContext;
constructor(uiContext: UIContext) {
super(uiContext);
this.uiContext = uiContext;
}
onMeasure(constraint: LayoutConstraint): void {
let sizeRes: Size = { width: this.uiContext.vp2px(100), height: this.uiContext.vp2px(100) };
for (let i = 0; i < this.getChildrenCount(); i++) {
let child = this.getChild(i);
if (child) {
let childConstraint = GetChildLayoutConstraint(constraint, child);
child.measure(childConstraint);
let size = child.getMeasuredSize();
sizeRes.height += size.height + this.space;
sizeRes.width = Math.max(sizeRes.width, size.width);
}
}
this.setMeasuredSize(sizeRes);
}
onLayout(position: Position): void {
for (let i = 0; i < this.getChildrenCount(); i++) {
let child = this.getChild(i);
if (child) {
child.layout({
x: this.uiContext.vp2px(100),
y: this.uiContext.vp2px(this.offsetY)
});
let layoutPosition = child.getLayoutPosition();
console.log("child position:" + JSON.stringify(layoutPosition));
}
}
this.setLayoutPosition(position);
}
onDraw(context: DrawContext) {
const canvas = context.canvas;
const pen = new drawing.Pen();
pen.setStrokeWidth(15);
pen.setColor({
alpha: 255,
red: 255,
green: 0,
blue: 0
});
canvas.attachPen(pen);
canvas.drawRect({
left: 50,
right: this.width + 50,
top: 50,
bottom: this.width + 50,
});
canvas.detachPen();
}
addWidth() {
this.width = (this.width + 10) % 50 + 100;
}
}
class MyNodeController extends NodeController {
public rootNode: MyFrameNode|null = null;
makeNode(context: UIContext): FrameNode|null {
this.rootNode = new MyFrameNode(context);
this.rootNode?.commonAttribute?.size({ width: 100, height: 100 }).backgroundColor(Color.Green);
let frameNode: FrameNode = new FrameNode(context);
this.rootNode.appendChild(frameNode);
frameNode.commonAttribute.width(10).height(10).backgroundColor(Color.Pink);
return this.rootNode;
}
}
@Entry
@Component
struct Index {
private nodeController: MyNodeController = new MyNodeController();
build() {
Row() {
Column() {
NodeContainer(this.nodeController)
.width('100%')
.height(200)
.backgroundColor('#FFF0F0F0')
Button('Invalidate')
.margin(10)
.onClick(() => {
this.nodeController?.rootNode?.addWidth();
this.nodeController?.rootNode?.invalidate();
})
Button('UpdateLayout')
.onClick(() => {
let node = this.nodeController.rootNode;
node!.offsetY = (node!.offsetY + 10) % 110;
this.nodeController?.rootNode?.setNeedsLayout();
})
}
.width('100%')
.height('100%')
}
.height('100%')
}
}
查找节点及获取基础信息
FrameNode提供了查询接口用于返回实体节点的基础信息。具体返回的信息内容参考FrameNode中提供的接口。
查找获得FrameNode的方式包括三种:
使用getFrameNodeById获取。
通过无感监听获取。
说明:
1、当前接口提供的可查询的信息包括:
布局信息:getPositionToWindow,getPositionToParent,getLayoutPosition,getUserConfigBorderWidth,getUserConfigPadding,getUserConfigMargin
节点信息:getId,getUniqueId,getNodeType,getOpacity,isVisible,isClipToFrame,isAttached,getInspectorInfo,getCustomProperty
2、无法获取UINode类型节点,例如:JsView节点、Span、ContainerSpan、ContentSlot、ForEach、LazyForEach、if/else组件等。
获取节点位置偏移信息
FrameNode提供了查询节点相对窗口、父组件以及屏幕位置偏移的信息接口(getPositionToWindow,getPositionToParent,getPositionToScreen,getPositionToWindowWithTransform,getPositionToParentWithTransform,getPositionToScreenWithTransform,getLayoutPosition,getUserConfigBorderWidth,getUserConfigPadding,getUserConfigMargin)。
import { NodeController, FrameNode, UIContext } from '@kit.ArkUI';
const TEST_TAG: string = "FrameNode"
class MyNodeController extends NodeController {
public frameNode: FrameNode|null = null;
private rootNode: FrameNode|null = null;
makeNode(uiContext: UIContext): FrameNode|null {
this.rootNode = new FrameNode(uiContext);
this.frameNode = new FrameNode(uiContext);
this.rootNode.appendChild(this.frameNode);
return this.rootNode;
}
getPositionToWindow() {
let positionToWindow = this.rootNode?.getPositionToWindow(); // 获取FrameNode相对于窗口的位置偏移
console.log(`${TEST_TAG} ${JSON.stringify(positionToWindow)}`);
}
getPositionToParent() {
let positionToParent = this.rootNode?.getPositionToParent(); // 获取FrameNode相对于父组件的位置偏移
console.log(`${TEST_TAG} ${JSON.stringify(positionToParent)}`);
}
getPositionToScreen() {
let positionToScreen = this.rootNode?.getPositionToScreen(); // 获取FrameNode相对于屏幕的位置偏移
console.log(`${TEST_TAG} ${JSON.stringify(positionToScreen)}`);
}
getPositionToWindowWithTransform() {
let positionToWindowWithTransform =
this.rootNode?.getPositionToWindowWithTransform(); // 获取FrameNode相对于窗口带有绘制属性的位置偏移
console.log(`${TEST_TAG} ${JSON.stringify(positionToWindowWithTransform)}`);
}
getPositionToParentWithTransform() {
let positionToParentWithTransform =
this.rootNode?.getPositionToParentWithTransform(); // 获取FrameNode相对于父组件带有绘制属性的位置偏移
console.log(`${TEST_TAG} ${JSON.stringify(positionToParentWithTransform)}`);
}
getPositionToScreenWithTransform() {
let positionToScreenWithTransform =
this.rootNode?.getPositionToScreenWithTransform(); // 获取FrameNode相对于屏幕带有绘制属性的位置偏移
console.log(`${TEST_TAG} ${JSON.stringify(positionToScreenWithTransform)}`);
}
}
@Entry
@Component
struct Index {
private myNodeController: MyNodeController = new MyNodeController();
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
Button("getPositionToWindow")
.width(300)
.onClick(() => {
this.myNodeController.getPositionToWindow();
})
Button("getPositionToParent")
.width(300)
.onClick(() => {
this.myNodeController.getPositionToParent();
})
Button("getPositionToScreen")
.width(300)
.onClick(() => {
this.myNodeController.getPositionToScreen();
})
Button("getPositionToParentWithTransform")
.width(300)
.onClick(() => {
this.myNodeController.getPositionToParentWithTransform();
})
Button("getPositionToWindowWithTransform")
.width(300)
.onClick(() => {
this.myNodeController.getPositionToWindowWithTransform();
})
Button("getPositionToScreenWithTransform")
.width(300)
.onClick(() => {
this.myNodeController.getPositionToScreenWithTransform();
})
Column() {
Text("This is a NodeContainer.")
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.width('100%')
.fontSize(16)
NodeContainer(this.myNodeController)
.borderWidth(1)
.width(300)
.height(100)
}
}
.padding({
left: 35,
right: 35,
top: 35,
bottom: 35
})
.width("100%")
.height("100%")
}
}
通过typeNode创建具体类型的FrameNode节点
通过TypeNode创建具体类型的FrameNode节点,可以根据属性获取接口来检索用户设置的属性信息。
import { NodeController, FrameNode, UIContext, BuilderNode, typeNode } from '@kit.ArkUI';
class Params {
text: string = "";
constructor(text: string) {
this.text = text;
}
}
@Builder
function buildText(params: Params) {
Column() {
Text(params.text)
.id("buildText")
.border({ width: 1 })
.padding(1)
.fontSize(25)
.fontWeight(FontWeight.Bold)
.margin({ top: 10 })
.visibility(Visibility.Visible)
.opacity(0.7)
.customProperty("key1", "value1")
.width(300)
}
}
const TEST_TAG: string = "FrameNode"
class MyNodeController extends NodeController {
public frameNode: typeNode.Column|null = null;
public uiContext: UIContext|undefined = undefined;
private rootNode: FrameNode|null = null;
private textNode: BuilderNode<[Params]>|null = null;
public textTypeNode: typeNode.Text|null = null;
private message: string = "DEFAULT";
makeNode(uiContext: UIContext): FrameNode|null {
this.rootNode = new FrameNode(uiContext);
this.uiContext = uiContext;
this.frameNode = typeNode.createNode(uiContext, "Column");
this.frameNode.attribute
.width("100%")
.height("100%")
this.rootNode.appendChild(this.frameNode);
this.textNode = new BuilderNode(uiContext);
this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
this.frameNode.appendChild(this.textNode.getFrameNode());
this.textTypeNode = typeNode.createNode(uiContext, "Text");
this.textTypeNode.initialize("textTypeNode")
.fontSize(25)
.visibility(Visibility.Visible)
.id("textTypeNode")
this.frameNode.appendChild(this.textTypeNode);
return this.rootNode;
}
removeChild(frameNode: FrameNode) {
let parent = frameNode.getParent();
if (parent) {
parent.removeChild(frameNode);
}
}
getUserConfigBorderWidth(frameNode: FrameNode) {
let userConfigBorderWidth = frameNode?.getUserConfigBorderWidth(); // 获取用户设置的边框宽度
console.log(`${TEST_TAG} ${JSON.stringify(userConfigBorderWidth)}`);
}
getUserConfigPadding(frameNode: FrameNode) {
let userConfigPadding = frameNode?.getUserConfigPadding(); // 获取用户设置的内边距
console.log(`${TEST_TAG} ${JSON.stringify(userConfigPadding)}`);
}
getUserConfigMargin(frameNode: FrameNode) {
let userConfigMargin = frameNode?.getUserConfigMargin(); // 获取用户设置的外边距
console.log(`${TEST_TAG} ${JSON.stringify(userConfigMargin)}`);
}
getUserConfigSize(frameNode: FrameNode) {
let userConfigSize = frameNode?.getUserConfigSize(); // 获取用户设置的宽高
console.log(`${TEST_TAG} ${JSON.stringify(userConfigSize)}`);
}
getId(frameNode: FrameNode) {
let id = frameNode?.getId(); // 获取用户设置的节点ID
console.log(`${TEST_TAG} ${id}`);
}
getUniqueId(frameNode: FrameNode) {
let uniqueId = frameNode?.getUniqueId(); // 获取系统分配的唯一标识的节点UniqueID
console.log(`${TEST_TAG} ${uniqueId}`);
}
getNodeType(frameNode: FrameNode) {
let nodeType = frameNode?.getNodeType(); // 获取节点的类型
console.log(`${TEST_TAG} ${nodeType}`);
}
getOpacity(frameNode: FrameNode) {
let opacity = frameNode?.getOpacity(); // 获取节点的不透明度
console.log(`${TEST_TAG} ${JSON.stringify(opacity)}`);
}
isVisible(frameNode: FrameNode) {
let visible = frameNode?.isVisible(); // 获取节点是否可见
console.log(`${TEST_TAG} ${JSON.stringify(visible)}`);
}
isClipToFrame(frameNode: FrameNode) {
let clipToFrame = frameNode?.isClipToFrame(); // 获取节点是否是剪裁到组件区域
console.log(`${TEST_TAG} ${JSON.stringify(clipToFrame)}`);
}
isAttached(frameNode: FrameNode) {
let attached = frameNode?.isAttached(); // 获取节点是否被挂载到主节点树上
console.log(`${TEST_TAG} ${JSON.stringify(attached)}`);
}
getInspectorInfo(frameNode: FrameNode) {
let inspectorInfo = frameNode?.getInspectorInfo(); // 获取节点的结构信息
console.log(`${TEST_TAG} ${JSON.stringify(inspectorInfo)}`);
}
}
@Entry
@Component
struct Index {
private myNodeController: MyNodeController = new MyNodeController();
@State index: number = 0;
build() {
Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) {
Column() {
Text("This is a NodeContainer.")
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
.width('100%')
.fontSize(16)
NodeContainer(this.myNodeController)
.borderWidth(1)
.width(300)
.height(100)
}
Button("getUserConfigBorderWidth")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getUserConfigBorderWidth(node);
}
}
})
Button("getUserConfigPadding")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getUserConfigPadding(node);
}
}
})
Button("getUserConfigMargin")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getUserConfigMargin(node);
}
}
})
Button("getUserConfigSize")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getUserConfigSize(node);
}
}
})
Button("getId")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getId(node);
}
}
})
Button("getUniqueId")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getUniqueId(node);
}
}
})
Button("getNodeType")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getNodeType(node);
}
}
})
Button("getOpacity")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getOpacity(node);
}
}
})
Button("isVisible")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.isVisible(node);
}
}
})
Button("isClipToFrame")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.isClipToFrame(node);
}
}
})
Button("isAttached")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.isAttached(node);
}
}
})
Button("remove Text")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("textTypeNode")||null;
if (node) {
this.myNodeController.removeChild(node);
this.myNodeController.isAttached(node);
}
}
})
Button("getInspectorInfo")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
this.myNodeController.getInspectorInfo(node);
}
}
})
Button("getCustomProperty")
.width(300)
.onClick(() => {
const uiContext: UIContext = this.getUIContext();
if (uiContext) {
const node: FrameNode|null = uiContext.getFrameNodeById("buildText")||null;
if (node) {
const property = node.getCustomProperty("key1");
console.log(TEST_TAG, JSON.stringify(property));
}
}
})
}
.padding({
left: 35,
right: 35,
top: 35,
bottom: 35
})
.width("100%")
.height("100%")
}
}
解除当前FrameNode对象对实体FrameNode节点的引用关系
使用dispose接口可以立即解除当前FrameNode对象对实体FrameNode节点的引用关系。
说明:
在调用dispose方法后,FrameNode对象不再对应任何实际的FrameNode节点。此时,若尝试调用以下查询接口:getMeasuredSize、getLayoutPosition、getUserConfigBorderWidth、getUserConfigPadding、getUserConfigMargin、getUserConfigSize,将导致应用程序触发jscrash。
通过getUniqueId可以判断当前FrameNode是否对应一个实体FrameNode节点。当UniqueId大于0时表示该对象对应一个实体FrameNode节点。
import { NodeController, FrameNode, BuilderNode } from '@kit.ArkUI';
const TEST_TAG: string = "FrameNode";
@Component
struct TestComponent {
build() {
Column() {
Text('This is a BuilderNode.')
.fontSize(16)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.backgroundColor(Color.Gray)
}
aboutToAppear() {
console.error(TEST_TAG + ' aboutToAppear');
}
aboutToDisappear() {
console.error(TEST_TAG + ' aboutToDisappear');
}
}
@Builder
function buildComponent() {
TestComponent()
}
class MyNodeController extends NodeController {
private rootNode: FrameNode|null = null;
private builderNode: BuilderNode<[]>|null = null;
makeNode(uiContext: UIContext): FrameNode|null {
this.rootNode = new FrameNode(uiContext);
this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 200, height: 100 } });
this.builderNode.build(new WrappedBuilder(buildComponent));
const rootRenderNode = this.rootNode.getRenderNode();
if (rootRenderNode !== null) {
rootRenderNode.size = { width: 200, height: 200 };
rootRenderNode.backgroundColor = 0xff00ff00;
rootRenderNode.appendChild(this.builderNode!.getFrameNode()!.getRenderNode());
}
return this.rootNode;
}
printUniqueId(): void {
if (this.rootNode !== null && this.builderNode !== null) {
console.log(`${TEST_TAG} rootNode's uniqueId: ${this.rootNode.getUniqueId()}`);
const frameNode = this.builderNode.getFrameNode();
if (frameNode) {
console.log(`${TEST_TAG} the uniqueId of builderNode's framenode: ${frameNode.getUniqueId()}`);
} else {
console.log(`${TEST_TAG} builderNode's framenode is undefined`);
}
}
}
disposeFrameNode(): void {
if (this.rootNode !== null && this.builderNode !== null) {
console.log(`${TEST_TAG} disposeFrameNode`);
this.rootNode.removeChild(this.builderNode.getFrameNode());
this.builderNode.dispose();
this.rootNode.dispose();
}
}
removeBuilderNode(): void {
const rootRenderNode = this.rootNode!.getRenderNode();
if (rootRenderNode !== null && this.builderNode !== null && this.builderNode.getFrameNode() !== null) {
rootRenderNode.removeChild(this.builderNode!.getFrameNode()!.getRenderNode());
}
}
}
@Entry
@Component
struct Index {
private myNodeController: MyNodeController = new MyNodeController();
build() {
Column({ space: 4 }) {
NodeContainer(this.myNodeController)
Button('FrameNode dispose')
.onClick(() => {
this.myNodeController.printUniqueId();
this.myNodeController.disposeFrameNode();
this.myNodeController.printUniqueId();
})
.width('100%')
}
}
}
FrameNode的数据懒加载能力
提供NodeAdapter对象替代ArkTS侧的LazyForEach功能,提供自定义节点的数据懒加载功能,实现按需迭代数据。
说明:
入参不能为负数,入参为负数时不做处理。
import { FrameNode, NodeController, NodeAdapter, typeNode } from '@kit.ArkUI';
const TEST_TAG: string = "FrameNode";
class MyNodeAdapter extends NodeAdapter {
uiContext: UIContext
cachePool: Array<FrameNode> = new Array();
changed: boolean = false
reloadTimes: number = 0;
data: Array<string> = new Array();
hostNode?: FrameNode
constructor(uiContext: UIContext, count: number) {
super();
this.uiContext = uiContext;
this.totalNodeCount = count;
this.loadData();
}
reloadData(count: number): void {
this.reloadTimes++;
NodeAdapter.attachNodeAdapter(this, this.hostNode);
this.totalNodeCount = count;
this.loadData();
this.reloadAllItems();
}
refreshData(): void {
let items = this.getAllAvailableItems()
console.log(TEST_TAG + " get All items:" + items.length);
this.totalNodeCount -= 1;
this.reloadAllItems();
}
detachData(): void {
NodeAdapter.detachNodeAdapter(this.hostNode);
this.reloadTimes = 0;
}
loadData(): void {
for (let i = 0; i < this.totalNodeCount; i++) {
this.data[i] = "Adapter ListItem " + i + " r:" + this.reloadTimes;
}
}
changeData(from: number, count: number): void {
this.changed = !this.changed;
for (let i = 0; i < count; i++) {
let index = i + from;
this.data[index] = "Adapter ListItem " + (this.changed ? "changed:" : "") + index + " r:" + this.reloadTimes;
}
this.reloadItem(from, count);
}
insertData(from: number, count: number): void {
for (let i = 0; i < count; i++) {
let index = i + from;
this.data.splice(index, 0, "Adapter ListItem " + from + "-" + i);
}
this.insertItem(from, count);
this.totalNodeCount += count;
console.log(TEST_TAG + " after insert count:" + this.totalNodeCount);
}
removeData(from: number, count: number): void {
let arr = this.data.splice(from, count);
this.removeItem(from, count);
this.totalNodeCount -= arr.length;
console.log(TEST_TAG + " after remove count:" + this.totalNodeCount);
}
moveData(from: number, to: number): void {
let tmp = this.data.splice(from, 1);
this.data.splice(to, 0, tmp[0]);
this.moveItem(from, to);
}
onAttachToNode(target: FrameNode): void {
console.log(TEST_TAG + " onAttachToNode id:" + target.getUniqueId());
this.hostNode = target;
}
onDetachFromNode(): void {
console.log(TEST_TAG + " onDetachFromNode");
}
onGetChildId(index: number): number {
console.log(TEST_TAG + " onGetChildId:" + index);
return index;
}
onCreateChild(index: number): FrameNode {
console.log(TEST_TAG + " onCreateChild:" + index);
if (this.cachePool.length > 0) {
let cacheNode = this.cachePool.pop();
if (cacheNode !== undefined) {
console.log(TEST_TAG + " onCreateChild reused id:" + cacheNode.getUniqueId());
let text = cacheNode?.getFirstChild();
let textNode = text as typeNode.Text;
textNode?.initialize(this.data[index]).fontSize(20);
return cacheNode;
}
}
console.log(TEST_TAG + " onCreateChild createNew");
let itemNode = typeNode.createNode(this.uiContext, "ListItem");
let textNode = typeNode.createNode(this.uiContext, "Text");
textNode.initialize(this.data[index]).fontSize(20);
itemNode.appendChild(textNode);
return itemNode;
}
onDisposeChild(id: number, node: FrameNode): void {
console.log(TEST_TAG + " onDisposeChild:" + id);
if (this.cachePool.length < 10) {
if (!this.cachePool.includes(node)) {
console.log(TEST_TAG + " caching node id:" + node.getUniqueId());
this.cachePool.push(node);
}
} else {
node.dispose();
}
}
onUpdateChild(id: number, node: FrameNode): void {
let index = id;
let text = node.getFirstChild();
let textNode = text as typeNode.Text;
textNode?.initialize(this.data[index]).fontSize(20);
}
}
class MyNodeAdapterController extends NodeController {
rootNode: FrameNode|null = null;
nodeAdapter: MyNodeAdapter|null = null;
makeNode(uiContext: UIContext): FrameNode|null {
this.rootNode = new FrameNode(uiContext);
let listNode = typeNode.createNode(uiContext, "List");
listNode.initialize({ space: 3 }).borderWidth(2).borderColor(Color.Black);
this.rootNode.appendChild(listNode);
this.nodeAdapter = new MyNodeAdapter(uiContext, 100);
NodeAdapter.attachNodeAdapter(this.nodeAdapter, listNode);
return this.rootNode;
}
}
@Entry
@Component
struct ListNodeTest {
adapterController: MyNodeAdapterController = new MyNodeAdapterController();
build() {
Column() {
Text("ListNode Adapter");
NodeContainer(this.adapterController)
.width(300).height(300)
.borderWidth(1).borderColor(Color.Black);
Row() {
Button("Reload")
.onClick(() => {
this.adapterController.nodeAdapter?.reloadData(50);
})
Button("Change")
.onClick(() => {
this.adapterController.nodeAdapter?.changeData(5, 10)
})
Button("Insert")
.onClick(() => {
this.adapterController.nodeAdapter?.insertData(10, 10);
})
}
Row() {
Button("Remove")
.onClick(() => {
this.adapterController.nodeAdapter?.removeData(10, 10);
})
Button("Move")
.onClick(() => {
this.adapterController.nodeAdapter?.moveData(2, 5);
})
Button("Refresh")
.onClick(() => {
this.adapterController.nodeAdapter?.refreshData();
})
Button("Detach")
.onClick(() => {
this.adapterController.nodeAdapter?.detachData();
})
}
}.borderWidth(1)
.width("100%")
}
}
你可能感兴趣的鸿蒙文章
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦