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

一、占位节点

  • 用于在声明式节点树上为自定义节点树预留位置的节点

  • 主要类型:NodeContainer 和 ContentSlot

为什么需要占位节点

  • 页面的主树采用声明式节点树(通过ArkTS组件描述)

  • 自定义节点使用命令式方式创建

  • 只有通过占位节点,才能将命令式创建的自定义节点挂载到声明式节点树上

二、两种占位节点对比

特性 NodeContainer ContentSlot
节点类型 容器节点(UI节点) 语法节点
通用属性 支持(如width、height等) 不支持
布局参与 参与布局 不参与布局
布局规则 类似左上角对齐的Stack组件 子节点按父容器布局规则排列
适用场景 一般自定义节点场景 混合模式开发(ArkTS容器 + Native子组件)
子节点布局 不受父容器布局约束 受父容器布局约束

三、NodeContainer 解析

1. 特性

  • 是系统组件,用于显示自定义节点树

  • 支持所有通用属性

  • 布局行为参考左上角对齐的Stack组件

  • 默认布局方式:子节点堆叠在左上角

2. 使用方式

通过NodeController控制NodeContainer的内容

核心接口:NodeController
class MyNodeController extends NodeController {
    makeNode(uiContext: UIContext): FrameNode | null
    aboutToAppear()
    aboutToDisappear()
    aboutToResize(size: Size)
    onTouchEvent(event: TouchEvent)
    rebuild()
}
生命周期回调
回调方法 触发时机 用途
makeNode 需要创建节点时 返回要挂载的自定义节点树根节点
aboutToAppear NodeContainer即将显示时 准备显示逻辑
aboutToDisappear NodeContainer即将消失时 清理资源
aboutToResize NodeContainer尺寸变化时 响应尺寸变化
onTouchEvent 触摸事件发生时 处理触摸交互
rebuild 需要重新构建节点时 手动触发重新构建

四、自定义节点类型

1. 支持的自定义节点

节点类型 描述 创建方式
FrameNode 自定义组件节点 new FrameNode(uiContext)
RenderNode 自定义渲染节点 new RenderNode(uiContext)
BuilderNode 自定义声明式节点 new BuilderNode<T>(uiContext)
ComponentContent 组件内容节点 通过接口获取

2. BuilderNode 示例

// 1. 定义Builder函数
@Builder
function buttonBuilder(params: Params) {
    Column() {
        Button(params.text)
            .fontSize(12)
            .borderRadius(8)
            .borderWidth(2)
            .backgroundColor(Color.Orange)
    }
}

// 2. 创建BuilderNode
let buttonNode = new BuilderNode<[Params]>(uiContext);
buttonNode.build(wrapBuilder(buttonBuilder), { text: "This is a Button" });

// 3. 获取FrameNode
let frameNode = buttonNode.getFrameNode();

五、使用流程

示例1:基本使用

// 1. 创建NodeController子类
class MyNodeController extends NodeController {
    private isShow: boolean = false;
    
    // 构造函数
    constructor(isShow: boolean) {
        super();
        this.isShow = isShow;
    }
    
    // 创建节点
    makeNode(uiContext: UIContext): FrameNode | null {
        if (!this.isShow) {
            return null;
        }
        // 创建或获取自定义节点
        let frameNode = getOrCreateNode(uiContext)?.getFrameNode();
        return frameNode ? frameNode : null;
    }
    
    // 生命周期回调
    aboutToAppear() {
        console.log('NodeContainer即将显示');
    }
    
    aboutToDisappear() {
        console.log('NodeContainer即将消失');
    }
    
    aboutToResize(size: Size) {
        console.log(`尺寸变化: ${size.width} x ${size.height}`);
    }
    
    // 控制方法
    toShow() {
        this.isShow = true;
        this.rebuild();  // 触发重新构建
    }
    
    toHide() {
        this.isShow = false;
        this.rebuild();  // 触发重新构建
    }
}

// 2. 在声明式组件中使用
@Entry
@Component
struct Index {
    private myNodeController: MyNodeController = new MyNodeController(true);
    
    build() {
        Column() {
            NodeContainer(this.myNodeController)
                .width("100%")
                .height("40%")
                .backgroundColor(Color.Brown)
            
            Button("切换显示")
                .onClick(() => {
                    this.myNodeController.toHide();
                })
        }
    }
}

示例2:节点移动(避免重复挂载)

Button("移动按钮位置")
    .onClick(() => {
        // 正确做法:先下树,再上树
        this.myNodeController1.toHide();  // 从原位置移除
        this.myNodeController2.toShow();  // 添加到新位置
    })

六、ContentSlot 使用示例

ContentSlot特点

  1. 只是语法节点,无通用属性

  2. 子节点按父容器规则布局

  3. 适用于混合模式开发

@Entry
@Component
struct Index {
    controller = new NodeContentCtrl(this.getUIContext());
    
    build() {
        Column() {
            // ContentSlot中的节点会按Column布局排列
            ContentSlot(this.controller.GetContent())
            
            Button("添加到Slot")
                .onClick(() => {
                    this.controller.AddNode();
                })
        }
    }
}

七、注意事项

1. 节点挂载限制

// NodeContainer只能挂载以下节点:
// 1. 自定义的FrameNode节点
// 2. BuilderNode创建的组件树的根节点
// 3. 不支持代理节点挂载(API Version 12+)

2. 节点唯一性原则

  • 一个节点只能有一个父节点

  • 如果将同一个节点挂载到多个NodeContainer:

    • 仅一个容器会显示节点

    • 多个容器的属性更新都会影响该节点

    • 可能导致显示异常或功能异常

//错误示例:重复挂载同一节点
let sharedNode = createNode(uiContext);
controller1.setNode(sharedNode);
controller2.setNode(sharedNode);  // 会导致问题

//正确做法:每个容器创建独立节点
controller1.setNode(createNode(uiContext));
controller2.setNode(createNode(uiContext));

3. 特殊场景处理

场景 处理方式
页面路由 确保节点正确挂载/卸载
动效场景 避免节点共享导致的动画异常
动态显示 使用rebuild()方法刷新

4. API Version 12+ 变化

  • 可以通过FrameNode查询接口获取系统组件的代理节点

  • 代理节点不能作为makeNode的返回值

  • 代理节点无法成功挂载到组件树

八、布局差异对比

// ContentSlot示例:子节点按Column布局
Column() {
    ContentSlot(content)  // 子节点会垂直排列
    // 添加的Text节点会按Column规则布局
}

// NodeContainer示例:子节点按Stack布局
Column() {
    NodeContainer(controller)  // 子节点堆叠在左上角
    // 添加的Text节点会重叠在一起
}

总结

  1. 占位节点是桥梁:连接声明式UI和命令式自定义节点

  2. 两种选择

    • NodeContainer:完整容器,Stack布局

    • ContentSlot:语法占位,继承父布局

  3. 核心控制器:NodeController管理节点生命周期

  4. 节点唯一性:一个节点只能属于一个父节点

  5. 适用场景

    • 复杂自定义UI组件

    • 动态节点管理

    • 混合开发模式

    通过使用NodeContainer和ContentSlot,可以在ArkUI中灵活地结合声明式和命令式编程模式,实现复杂的自定义UI效果。

Logo

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

更多推荐