鸿蒙 自定义UI之组件节点(FrameNode)
鸿蒙ArkUI中的FrameNode提供了命令式UI操作能力,支持动态节点树管理。其核心功能包括:1.节点创建与销毁;2.节点树动态操作(增删改查);3.自定义测量、布局和绘制;4.类型化节点创建;5.数据懒加载机制。FrameNode解决了传统混合开发中的性能问题,支持节点复用、精确布局控制和第三方框架集成。通过NodeContainer容器可直接构建自定义节点树,提供完整的UI控制能力,包括获
本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新
FrameNode是鸿蒙ArkUI中组件树的实体节点,它表示UI组件树中的一个具体节点。与声明式UI不同,FrameNode提供了一种命令式的方式来操作UI节点树。
传统混合开发问题:
-
第三方框架(如JSON、XML、DOM树)需要转换为ArkUI声明式描述
-
转换过程依赖数据驱动绑定到Builder中,复杂且性能欠佳
-
需要结合系统组件实现混合显示,开发难度大
FrameNode的解决方案:
-
直接在自定义占位容器NodeContainer内构建自定义节点树
-
支持动态操作(增、删、改、查)
-
提供完整的自定义能力(测量、布局、绘制)
-
支持获取系统组件代理节点进行遍历和监听
二、节点类型分类
| 节点类型 | 说明 | 是否可修改 |
|---|---|---|
| 自定义FrameNode | 通过new FrameNode()创建 |
可修改 |
| BuilderNode代理节点 | 通过BuilderNode的getFrameNode()获取 |
不可修改 |
| 系统组件代理节点 | 通过查询获得的系统组件对应节点 | 不可修改 |
| TypedFrameNode | 通过typeNode.createNode()创建的具体类型节点 |
可修改 |
三、核心功能
3.1 节点创建与销毁
3.1.1 创建FrameNode
import { FrameNode, UIContext } from '@kit.ArkUI';
// 必须传入UIContext
const uiContext: UIContext = this.getUIContext();
const frameNode = new FrameNode(uiContext);
// 设置通用属性
frameNode.commonAttribute
.width(100)
.height(100)
.backgroundColor(Color.Red)
.borderWidth(2);
3.1.2 节点销毁与引用解除
// 解除FrameNode与实体节点的绑定
frameNode.dispose();
// 检查是否已解除引用(API 20+)
const isDisposed = frameNode.isDisposed();
// 获取唯一ID判断有效性
const uniqueId = frameNode.getUniqueId();
if (uniqueId > 0) {
// 节点有效
} else {
// 节点已销毁
}
限制:
-
调用
dispose()后,不能再调用测量/布局查询接口,否则会导致JSCrash -
不持有FrameNode对象时,会被GC自动回收
3.2 节点树操作
3.2.1 基本节点操作
class MyNodeController extends NodeController {
private rootNode: FrameNode | null = null;
private childNodes: FrameNode[] = [];
makeNode(uiContext: UIContext): FrameNode | null {
this.rootNode = new FrameNode(uiContext);
// 创建子节点
const child1 = new FrameNode(uiContext);
const child2 = new FrameNode(uiContext);
// 1. 添加子节点
this.rootNode.appendChild(child1);
// 2. 在指定节点后插入
this.rootNode.insertChildAfter(child2, child1);
// 3. 获取子节点
const firstChild = this.rootNode.getFirstChild();
const secondChild = this.rootNode.getChild(1);
// 4. 获取子节点数量
const count = this.rootNode.getChildrenCount();
// 5. 移除子节点
setTimeout(() => {
this.rootNode?.removeChild(child1);
}, 2000);
// 6. 清空所有子节点
setTimeout(() => {
this.rootNode?.clearChildren();
}, 4000);
return this.rootNode;
}
}
3.2.2 节点移动(moveTo)
// 将节点移动到新的父节点下
class MyNodeController extends NodeController {
public stackNode1: FrameNode | null = null;
public rowNode: FrameNode | null = null;
makeNode(uiContext: UIContext): FrameNode | null {
// 创建Stack和Row节点
const stack1 = typeNode.createNode(uiContext, 'Stack');
const row = typeNode.createNode(uiContext, 'Row');
this.stackNode1 = stack1;
this.rowNode = row;
// 初始将stack1添加到row
row.appendChild(stack1);
return row;
}
}
// 在另一个控制器中移动节点
Button('移动节点').onClick(() => {
// 将stackNode1移动到另一个控制器的rowNode下
controller1.stackNode1?.moveTo(controller2.rowNode, 2);
});
移动限制:
-
仅支持
Stack、XComponent类型的TypedFrameNode -
BuilderNode根节点需为
Stack、XComponent、EmbeddedComponent -
不可修改的节点无法移动
3.3 属性与事件设置
3.3.1 通用属性设置
// 自定义FrameNode可修改属性
const customNode = new FrameNode(uiContext);
customNode.commonAttribute
.size({ width: 200, height: 100 })
.position({ x: 50, y: 50 })
.backgroundColor(Color.Blue)
.opacity(0.8)
.visibility(Visibility.Visible);
// 代理节点属性修改不生效(但不会报错)
const proxyNode = builderNode.getFrameNode();
proxyNode?.commonAttribute.size({ width: 300, height: 200 }); // 无效
3.3.2 事件回调设置
// 添加点击事件
frameNode.commonEvent.setOnClick((event: ClickEvent) => {
console.info(`FrameNode被点击: ${JSON.stringify(event)}`);
});
// 事件竞争机制:
// 1. 同时设置系统组件事件和commonEvent时,优先回调系统组件事件
// 2. commonEvent事件被消费后不会向父组件冒泡
// 3. 代理节点也可以添加事件监听
3.4 自定义测量、布局与绘制
3.4.1 自定义测量(onMeasure)
class CustomFrameNode extends FrameNode {
private space: number = 10;
// 重写测量方法
onMeasure(constraint: LayoutConstraint): void {
let totalHeight = 0;
let maxWidth = 0;
// 遍历子节点计算总尺寸
for (let i = 0; i < this.getChildrenCount(); i++) {
const child = this.getChild(i);
if (child) {
// 为子节点创建约束
const childConstraint: LayoutConstraint = {
maxSize: constraint.maxSize,
minSize: { width: 0, height: 0 },
percentReference: constraint.maxSize
};
// 触发子节点测量
child.measure(childConstraint);
// 获取子节点测量结果
const childSize = child.getMeasuredSize();
totalHeight += childSize.height + this.space;
maxWidth = Math.max(maxWidth, childSize.width);
}
}
// 设置自身测量结果
const measuredSize: Size = {
width: Math.max(constraint.minSize.width, maxWidth),
height: Math.max(constraint.minSize.height, totalHeight)
};
this.setMeasuredSize(measuredSize);
}
}
3.4.2 自定义布局(onLayout)
class CustomFrameNode extends FrameNode {
// 重写布局方法
onLayout(position: Position): void {
let currentY = position.y;
for (let i = 0; i < this.getChildrenCount(); i++) {
const child = this.getChild(i);
if (child) {
const childSize = child.getMeasuredSize();
// 设置子节点位置
child.layout({
x: position.x,
y: currentY
});
currentY += childSize.height + this.space;
}
}
// 设置自身布局位置
this.setLayoutPosition(position);
}
// 触发重新布局
updateLayout() {
this.setNeedsLayout(); // 标记需要重新布局
}
}
3.4.3 自定义绘制(onDraw)
import { drawing } from '@kit.ArkGraphics2D';
class DrawableFrameNode extends FrameNode {
private width: number = 100;
// 重写绘制方法
onDraw(context: DrawContext): void {
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();
}
// 触发重绘
updateDraw() {
this.width = (this.width + 10) % 50 + 100;
this.invalidate(); // 触发重绘
}
}
3.4.4 Canvas变换矩阵操作(API 12+)
class MatrixFrameNode extends FrameNode {
onDraw(context: DrawContext): void {
const canvas = context.canvas;
const matrix = new drawing.Matrix();
// 使用concatMatrix进行变换(推荐)
matrix.setTranslation(100, 100);
canvas.concatMatrix(matrix); // 累加变换
// 错误做法:使用setMatrix会覆盖已有变换
// canvas.setMatrix(matrix); // 覆盖变换
// 绘制内容
const pen = new drawing.Pen();
pen.setStrokeWidth(5);
pen.setColor({ alpha: 255, red: 0, green: 0, blue: 255 });
canvas.attachPen(pen);
canvas.drawRect({ left: 10, top: 10, right: 110, bottom: 60 });
canvas.detachPen();
}
}
3.5 节点信息查询
3.5.1 获取节点基本信息
// 各种信息查询接口
const frameNode: FrameNode = ...;
// 尺寸信息
const measuredSize = frameNode.getMeasuredSize(); // 测量后的大小
const userConfigSize = frameNode.getUserConfigSize(); // 用户设置的大小
// 位置信息
const layoutPosition = frameNode.getLayoutPosition(); // 布局位置
const positionToWindow = frameNode.getPositionToWindow(); // 相对窗口位置
const positionToParent = frameNode.getPositionToParent(); // 相对父节点位置
const positionToScreen = frameNode.getPositionToScreen(); // 相对屏幕位置
// 带变换的位置信息(考虑旋转、缩放等)
const positionWithTransform = frameNode.getPositionToWindowWithTransform();
// 全局显示位置(多屏场景)
const globalPosition = frameNode.getGlobalPositionOnDisplay();
// 样式信息
const borderWidth = frameNode.getUserConfigBorderWidth();
const padding = frameNode.getUserConfigPadding();
const margin = frameNode.getUserConfigMargin();
const opacity = frameNode.getOpacity();
// 节点状态
const isVisible = frameNode.isVisible();
const isClipToFrame = frameNode.isClipToFrame();
const isAttached = frameNode.isAttached(); // 是否挂载到主节点树
// 标识信息
const id = frameNode.getId(); // 用户设置的ID
const uniqueId = frameNode.getUniqueId(); // 系统分配的唯一ID
const nodeType = frameNode.getNodeType(); // 节点类型
// 自定义属性
const customProp = frameNode.getCustomProperty('key1');
// 调试信息
const inspectorInfo = frameNode.getInspectorInfo();
3.6 通过typeNode创建类型化节点
3.6.1 创建具体类型节点
import { typeNode } from '@kit.ArkUI';
// 创建Column节点
const columnNode = typeNode.createNode(uiContext, 'Column');
columnNode.initialize({ space: 10 })
.width('100%')
.height('100%');
// 创建Text节点
const textNode = typeNode.createNode(uiContext, 'Text');
textNode.initialize('Hello World')
.fontSize(25)
.fontWeight(FontWeight.Bold)
.visibility(Visibility.Visible)
.opacity(0.7)
.id('myText');
// 创建Image节点
const imageNode = typeNode.createNode(uiContext, 'Image');
imageNode.initialize($r('app.media.icon'))
.syncLoad(true)
.width(100)
.height(100);
// 创建List节点(用于懒加载)
const listNode = typeNode.createNode(uiContext, 'List');
listNode.initialize({ space: 3 })
.borderWidth(2)
.borderColor(Color.Black);
3.6.2 节点属性操作分离
// typeNode创建的节点有明确的属性接口
const textNode = typeNode.createNode(uiContext, 'Text');
// 1. 初始化方法(部分组件需要)
textNode.initialize('初始文本');
// 2. 通用属性(所有节点都有)
textNode.commonAttribute
.width(100)
.height(50)
.backgroundColor(Color.Gray);
// 3. 类型特定属性
textNode.attribute
.fontSize(20)
.fontColor(Color.Red)
.textAlign(TextAlign.Center);
// 4. 避免切换闪烁(API 21+)
textNode.invalidateAttributes(); // 强制当前帧更新
3.7 数据懒加载(NodeAdapter)
3.7.1 NodeAdapter实现
class MyNodeAdapter extends NodeAdapter {
private uiContext: UIContext;
private cachePool: FrameNode[] = [];
private data: string[] = [];
public totalNodeCount: number = 0;
constructor(uiContext: UIContext, count: number) {
super();
this.uiContext = uiContext;
this.totalNodeCount = count;
this.loadData();
}
// 必须实现的方法
onGetChildId(index: number): number {
return index; // 返回子节点ID
}
onCreateChild(index: number): FrameNode {
// 优先使用缓存
if (this.cachePool.length > 0) {
const cachedNode = this.cachePool.pop()!;
const textNode = cachedNode.getFirstChild() as typeNode.Text;
textNode?.initialize(this.data[index]);
return cachedNode;
}
// 创建新节点
const itemNode = typeNode.createNode(this.uiContext, 'ListItem');
const textNode = typeNode.createNode(this.uiContext, 'Text');
textNode.initialize(this.data[index]).fontSize(20);
itemNode.appendChild(textNode);
return itemNode;
}
onUpdateChild(id: number, node: FrameNode): void {
const textNode = node.getFirstChild() as typeNode.Text;
textNode?.initialize(this.data[id]);
}
onDisposeChild(id: number, node: FrameNode): void {
// 缓存节点(最多10个)
if (this.cachePool.length < 10) {
this.cachePool.push(node);
} else {
node.dispose(); // 超出缓存限制则销毁
}
}
// 数据操作方法
reloadData(count: number): void {
this.totalNodeCount = count;
this.loadData();
this.reloadAllItems(); // 通知刷新
}
insertData(from: number, count: number): void {
this.data.splice(from, 0, ...new Array(count).fill(''));
this.insertItem(from, count); // 通知插入
this.totalNodeCount += count;
}
removeData(from: number, count: number): void {
this.data.splice(from, count);
this.removeItem(from, count); // 通知删除
this.totalNodeCount -= count;
}
moveData(from: number, to: number): void {
const [item] = this.data.splice(from, 1);
this.data.splice(to, 0, item);
this.moveItem(from, to); // 通知移动
}
private loadData(): void {
for (let i = 0; i < this.totalNodeCount; i++) {
this.data[i] = `Item ${i}`;
}
}
}
3.7.2 使用NodeAdapter
class MyNodeAdapterController extends NodeController {
private rootNode: FrameNode | null = null;
private nodeAdapter: MyNodeAdapter | null = null;
makeNode(uiContext: UIContext): FrameNode | null {
this.rootNode = new FrameNode(uiContext);
// 创建List节点
const listNode = typeNode.createNode(uiContext, 'List');
listNode.initialize({ space: 3 });
// 创建并关联Adapter
this.nodeAdapter = new MyNodeAdapter(uiContext, 100);
NodeAdapter.attachNodeAdapter(this.nodeAdapter, listNode);
this.rootNode.appendChild(listNode);
return this.rootNode;
}
}
3.8 LazyForEach节点查询
3.8.1 展开模式(ExpandMode)
// LazyForEach节点的三种查询模式
enum ExpandMode {
NOT_EXPAND = 0, // 不展开,只查询主节点树上的子节点
EXPAND = 1, // 完全展开,查询所有子节点
LAZY_EXPAND = 2 // 懒展开,按需展开子节点
}
// 查询示例
const rootNode: FrameNode = ...;
// 1. 不展开模式(只查询已加载的节点)
const child1 = rootNode.getChild(3, ExpandMode.NOT_EXPAND);
// 2. 完全展开模式(展开所有懒加载节点)
const child2 = rootNode.getChild(3, ExpandMode.EXPAND);
// 3. 懒展开模式(按需展开)
const child3 = rootNode.getChild(3, ExpandMode.LAZY_EXPAND);
// 获取第一个/最后一个主节点树上的子节点索引
const firstIndex = rootNode.getFirstChildIndexWithoutExpand();
const lastIndex = rootNode.getLastChildIndexWithoutExpand();
3.8.2 LazyForEach数据源实现
class MyDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private dataArray: string[] = [];
totalCount(): number {
return this.dataArray.length;
}
getData(index: number): string {
return this.dataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
if (!this.listeners.includes(listener)) {
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const index = this.listeners.indexOf(listener);
if (index >= 0) {
this.listeners.splice(index, 1);
}
}
// 数据变更通知方法
notifyDataReload(): void {
this.listeners.forEach(listener => listener.onDataReloaded());
}
notifyDataAdd(index: number): void {
this.listeners.forEach(listener => listener.onDataAdd(index));
}
notifyDataChange(index: number): void {
this.listeners.forEach(listener => listener.onDataChange(index));
}
notifyDataDelete(index: number): void {
this.listeners.forEach(listener => listener.onDataDelete(index));
}
notifyDataMove(from: number, to: number): void {
this.listeners.forEach(listener => listener.onDataMove(from, to));
}
}
四、FrameNode查找方式
4.1 三种查找方式
// 1. 通过ID查找
const nodeById = uiContext.getFrameNodeById('myNodeId');
// 2. 通过UniqueId查找
const nodeByUniqueId = uiContext.getFrameNodeByUniqueId(12345);
// 3. 通过无感监听获取
import { observer, ObservedFrameNodeInfo } from '@kit.ArkUI';
@observer
@Component
struct MyComponent {
@Link @ObservedFrameNodeInfo frameNodeInfo?: ObservedFrameNodeInfo;
build() {
// 自动获取节点信息
}
}
4.2 无法获取的节点类型
-
JsView节点 -
Span、ContainerSpan文本组件 -
ContentSlot内容插槽 -
ForEach、LazyForEach渲染控制 -
if/else条件渲染组件 -
其他UINode类型节点
总结
FrameNode是鸿蒙ArkUI中强大的命令式UI操作能力,核心价值:
-
打破声明式限制:支持动态、命令式的UI操作
-
性能优化:节点复用、懒加载、自定义绘制
-
灵活扩展:完美支持第三方UI框架集成
-
精细控制:像素级的测量、布局、绘制控制
更多推荐


所有评论(0)