本文同步发表于我的微信公众号,微信搜索 程语新视界 即可关注,每个工作日都有文章更新

   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);
});

移动限制

  • 仅支持StackXComponent类型的TypedFrameNode

  • BuilderNode根节点需为StackXComponentEmbeddedComponent

  • 不可修改的节点无法移动

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节点

  • SpanContainerSpan文本组件

  • ContentSlot内容插槽

  • ForEachLazyForEach渲染控制

  • if/else条件渲染组件

  • 其他UINode类型节点

总结

FrameNode是鸿蒙ArkUI中强大的命令式UI操作能力,核心价值:

  1. 打破声明式限制:支持动态、命令式的UI操作

  2. 性能优化:节点复用、懒加载、自定义绘制

  3. 灵活扩展:完美支持第三方UI框架集成

  4. 精细控制:像素级的测量、布局、绘制控制

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐