容器断点 (ContainerReader)

容器断点组件ContainerReader是ArkUI提供的一种响应式布局解决方案,从API版本26.0.0开始,允许开发者基于容器尺寸而非窗口尺寸实现自适应布局。与传统的窗口断点相比,容器断点提供了更细粒度的布局控制能力,使得组件能够在不同的容器尺寸下呈现不同的布局效果。

ContainerReader在实际开发中常用于FlexRowColumn容器内部、Navigation以及自定义组件内部场景,提供实时尺寸获取、断点值获取和自定义断点阈值核心功能。

能力范围

ContainerReader提供以下关键能力。

  • 容器级尺寸感知:基于组件自身实际尺寸确定断点值,而非窗口尺寸。适用于组件级响应式布局、嵌套布局场景和可复用组件开发。
  • 双向绑定实时获取:通过状态变量实时获取容器尺寸和断点信息。
  • 宽度/高度双模式:同时支持宽度断点和高度断点,满足不同维度的自适应需求。
  • 自定义断点阈值:通过breakpointConfig属性灵活配置不同尺寸区间对应的布局策略。

布局规格

从API版本26.0.0开始,ContainerReader组件在不同父容器类型下的布局规格如下。

说明:

  • 当父容器为Flex组件、Row组件或Column组件时,ContainerReader会占满父容器剩余空间,Flex的弹性特性生效优先级不变。
  • 当父容器为其他容器类型时,ContainerReader会撑满父容器。
  • ContainerReader的尺寸由父容器和自身布局确定,不受子组件影响。
父容器类型 无兄弟节点 有兄弟节点
Flex、Row、Column 撑满父容器。 撑满父容器剩余空间。
其他类型组件 撑满父组件。 撑满父组件。

ContainerReader作为子组件时,其尺寸由父容器决定。当父容器为Flex、Row或Column时,ContainerReader会根据父容器的布局方向自动撑满父容器剩余空间。

说明:

使用ContainerReader需要同时导入ContainerReaderAttribute,否则会导致编译报错。

import {ContainerReader, ContainerReaderAttribute, Size} from '@kit.ArkUI';
@Entry
@Component
struct Example {
  @State containerSize: Size = { width: 0, height: 0 };
  @State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  build() {
    Flex({ direction: FlexDirection.Row }) {
      ContainerReader({
        size: this.containerSize!!,
        widthBreakpoint: this.widthBp!!
      }) {
        Column() {
          Text('Adaptive Content')
        }
        .width('100%')
        .height('100%')
      }
      .backgroundColor('#F7F7F7')
    }
    .padding(10)
    .width('100%')
    .height(200)
    .backgroundColor('#D5D5D5')
  }
}

ContainerReader作为Flex、Row或Column的子组件使用时,会优先为非ContainerReader类型的子组件测算尺寸,再结合父容器剩余空间与开发者设置为ContainerReader组件分配空间。这在固定内容与自适应内容并存的场景中较为适用。

说明:

使用ContainerReader需要同时导入ContainerReaderAttribute,否则会导致编译报错。

import {ContainerReader, ContainerReaderAttribute, Size} from '@kit.ArkUI';
@Entry
@Component
struct Example {
  @State containerSize: Size = { width: 0, height: 0 };
  @State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  build() {
    Flex({ direction: FlexDirection.Row }) {
      Column() {
        Text('Text')
          .width(100)
          .height('100%')
      }
      .width(100)
      .height('100%')
      .backgroundColor('#D5D5D5')
      ContainerReader({
        size: this.containerSize!!,
        widthBreakpoint: this.widthBp!!
      }) {
        Column() {
          Text('ContainerReader')
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
      .backgroundColor('#F7F7F7')
    }
    .width('100%')
    .height(300)
    .backgroundColor('#F0FAFF')
    .padding(10)
  }
}

当Flex、Row或Column容器中有多个ContainerReader子组件时,按开发者书写顺序第一个ContainerReader会占满剩余空间,此时其余ContainerReader组件的主轴大小为0。但开发者可以通过layoutWeight属性使多个ContainerReader平分剩余空间。

说明:

使用ContainerReader需要同时导入ContainerReaderAttribute,否则会导致编译报错。

import {ContainerReader, ContainerReaderAttribute, Size} from '@kit.ArkUI';
@Entry
@Component
struct Example {
  @State containerSize: Size = { width: 0, height: 0 };
  @State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  @State containerSize1: Size = { width: 0, height: 0 };
  @State widthBp1: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  build() {
    Flex({ direction: FlexDirection.Row }) {
      // 固定宽度的兄弟组件,优先测算
      Column() {
        Text('Text')
          .width(80)
          .height('100%')

      }
      .width(80)
      .height('100%')
      .backgroundColor('#D5D5D5')

      // 第一个ContainerReader,通过layoutWeight(1)分配1/2剩余空间
      ContainerReader({
        size: this.containerSize!!,
        widthBreakpoint: this.widthBp!!
      }) {
        Column() {
          Text('ContainerReader1')
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
      .layoutWeight(1)
      .backgroundColor('#F7F7F7')

      // 第二个ContainerReader,同样layoutWeight(1)分配1/2剩余空间
      ContainerReader({
        size: this.containerSize1!!,
        widthBreakpoint: this.widthBp1!!
      }) {
        Column() {
          Text('ContainerReader2')
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
      .layoutWeight(1)
      .backgroundColor('#707070')
    }
    .width('100%')
    .height(300)
    .backgroundColor('#F0FAFF')
    .padding(10)
  }
}

约束与限制

状态变量要求

ContainerReaderInfo的所有参数都必须通过状态变量进行双向绑定。

// 正确用法 - 使用!!触发双向绑定
@State containerSize: Size = { width: 0, height: 0 };
@State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;

ContainerReader({
  size: this.containerSize!!,
  widthBreakpoint: this.widthBp!!
})

// 错误用法1 - 未使用!!后缀,双向绑定不生效
ContainerReader({
  size: this.containerSize,
  widthBreakpoint: this.widthBp
})

// 错误用法2 - 使用非状态变量
const containerSize: Size = { width: 0, height: 0 };

ContainerReader({
  size: containerSize,  // 必须是@State修饰的状态变量
  widthBreakpoint: WidthBreakpoint.WIDTH_MD
})

尺寸计算时机

ContainerReader的尺寸由父容器和自身布局确定,不受子组件影响。在ContainerReader组件尺寸测算时,先根据父组件和自身设置确认自身尺寸,然后再做子组件的展开尺寸测算。为防止在节点未尺寸测算前的生命周期中使用ContainerReader组件双向绑定的size状态变量,需要给双向绑定状态变量一个初始值。

因此请确保,父容器有明确尺寸,含有ContainerReader组件的父容器不应依赖子节点确认自身大小,ContainerReader组件不依赖自身子节点大小确认尺寸。

获取ContainerReader容器尺寸

ContainerReader的主要接口包括ContainerReader和breakpointConfig。

  • ContainerReader接口

    ContainerReader是实现容器断点的核心组件,其使用要求如下。ContainerReaderInfo所有参数都必须通过状态变量进行双向绑定。ContainerReader通过双向绑定机制,将后端计算的尺寸和断点值实时更新到状态变量中。不能通过改变此处ContainerReaderInfo的size来试图设置ContainerReader尺寸。在使用状态变量时需要添加!!后缀,触发双向绑定更新。

  • breakpointConfig属性

    通过breakpointConfig属性可以自定义断点阈值。断点数组必须为单调递增数组。其中,宽度断点最多支持5个,即数组最大长度为4;高度断点最多支持3个,即数组最大长度为2。断点区间为左闭右开区间[breakpoint[i], breakpoint[i+1])。宽度断点值单位为vp;高度断点值为组件高度与宽度的比值,无单位。

    异常处理规则:

    异常情况 处理方式
    数组大小超过最大数量。 使用系统默认断点。
    数组非递增。 取递增结束的子数组。
    数组中存在异常值(非数字等)。 跳过异常值,只处理有效值。

    示例:

    // 示例1:数组超过最大长度[320, 600, 840, 1440, 2000, 3000]
    // 超过部分被忽略,使用系统默认:[320, 600, 840, 1440]
    .breakpointConfig({ width: [320, 600, 840, 1440, 2000, 3000] })
    
    // 示例2:数组非递增[100, 50, 300, 400]
    // 取递增结束的子数组:[100]
    .breakpointConfig({ width: [100, 50, 300, 400] })
    
    // 示例3:数组包含异常值[100, undefined, 300, 400]
    // 跳过异常值undefined,处理有效值:[100, 300, 400]
    .breakpointConfig({ width: [100, undefined, 300, 400] })
    

下面介绍容器断点的简单开发步骤。

  1. 声明状态变量。

    首先需要声明用于存储容器尺寸和断点信息的状态变量并初始化,防止在未获取ContainerReader的大小和断点时使用造成异常。

    @State containerSize: Size = { width: 0, height: 0 };
    @State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
    
  2. 配置ContainerReader。

    将状态变量绑定到ContainerReader组件,使用!!后缀触发双向绑定更新。

    ContainerReader({
      size: this.containerSize!!,
      widthBreakpoint: this.widthBp!!
    }) {
      Column() {
        Text('Adaptive Content')
      }
      .width('100%')
      .height('100%')
    }
    
  3. 确定容器尺寸。

    情况一:ContainerReader不显式设置尺寸,它通过以下方式自动获取尺寸。

    • 撑满父容器:当ContainerReader是父容器的唯一子组件时,自动撑满父容器。
    • 撑满剩余空间:当父容器Flex、Row或Column有非ContainerReader类型的子组件时,ContainerReader占满剩余空间。
    • 按比例分配剩余空间:当父容器Flex、Row或Column有多个ContainerReader子组件时,ContainerReader通过layoutWeight分配剩余空间。

    情况二:可以为ContainerReader设置布局约束等尺寸属性来约束其尺寸。当Flex、Row或Column容器中有多个ContainerReader子组件时,一般情况下按开发者书写顺序第一个ContainerReader会占满剩余空间,此时其余ContainerReader组件的主轴大小为0。当开发者给书写顺序靠前的ContainerReader设置了尺寸约束时,会按尺寸约束分配给ContainerReader空间,再将剩余空间分配给开发者写的下一个ContainerReader。

    以情况一的撑满父容器为例,Flex给子组件ContainerReader分配与Flex等大的空间。

    说明:

    使用ContainerReader需要同时导入ContainerReaderAttribute,否则会导致编译报错。

    import {ContainerReader, ContainerReaderAttribute, Size} from '@kit.ArkUI';
    @Entry
    @Component
    struct Example {
      @State containerSize: Size = { width: 0, height: 0 };
      @State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
      build() {
        Flex({ direction: FlexDirection.Row }) {
          ContainerReader({
            size: this.containerSize!!,
            widthBreakpoint: this.widthBp!!
          }) {
            Column() {
              Text('Adaptive Content')
            }
            .width('100%')
            .height('100%')
          }
          .backgroundColor('#F7F7F7')
        }
        .padding(10)
        .width('100%')
        .height(200)
        .backgroundColor('#D5D5D5')
      }
    }
    

实现独立断点

在同一组件中,可以同时使用多个ContainerReader组件,每个组件可以拥有独立的断点状态,实现更精细化的布局控制。

说明:

使用ContainerReader需要同时导入ContainerReaderAttribute,否则会导致编译报错。

import {ContainerReader, ContainerReaderAttribute, Size} from '@kit.ArkUI';
@Entry
@Component
struct MultiContainerExample {
  // 左右两个ContainerReader各自拥有独立的状态变量,互不影响
  @State leftContainerSize: Size = { width: 0, height: 0 };
  @State rightContainerSize: Size = { width: 0, height: 0 };
  @State leftWidthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  @State rightWidthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;

  build() {
    Row({ space: 10 }) {
      // 左侧容器,独立感知自身尺寸和断点
      Flex({ direction: FlexDirection.Column }) {
        ContainerReader({
          size: this.leftContainerSize!!,
          widthBreakpoint: this.leftWidthBp!!
        }) {
          Column() {
            Text('Left Container')
          }
          .width('100%')
          .height('100%')
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#F0FAFF')
      }
      .layoutWeight(1)
      .height(300)

      // 右侧容器,独立感知自身尺寸和断点
      Flex({ direction: FlexDirection.Column }) {
        ContainerReader({
          size: this.rightContainerSize!!,
          widthBreakpoint: this.rightWidthBp!!
        }) {
          Column() {
            Text('Right Container')
          }
          .width('100%')
          .height('100%')
        }
        .width('100%')
        .height('100%')
        .backgroundColor('#D5D5D5')
      }
      .layoutWeight(1)
      .height(300)
    }
    .width('100%')
    .padding(20)
  }
}

网格组件根据自身容器断点设置列数

在多设备开发中,网格组件(如Grid)可以根据自身容器尺寸设置不同的列数,实现自适应的列表布局。

说明:

使用ContainerReader需要同时导入ContainerReaderAttribute,否则会导致编译报错。

import {ContainerReader, ContainerReaderAttribute, Size} from '@kit.ArkUI';
@Entry
@Component
struct GridBreakpointExample {
  @State containerSize: Size = { width: 0, height: 0 };
  @State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  @State bgColors: ResourceColor[] =
    ['#707070', '#004AAF', '#2787D9', '#F7F7F7', '#F0FAFF'];

  build() {
    Column({ space: 10 }) {
      Text('Grid with Container Breakpoint')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      // ContainerReader包裹Grid,根据容器宽度断点动态调整列数
      Flex({ direction: FlexDirection.Column }) {
        ContainerReader({
          size: this.containerSize!!,
          widthBreakpoint: this.widthBp!!
        }) {
          Grid() {
            ForEach(this.bgColors, (color: ResourceColor, index?: number) => {
              GridItem() {
                Row() {
                  Text(`${index}`)
                }
                .width('100%')
                .height('50vp')
                .justifyContent(FlexAlign.Center)
              }
              .backgroundColor(color)
            })
          }
          // 根据断点值动态设置列模板
          .columnsTemplate(this.getColumnsTemplate())
          .columnsGap(10)
          .rowsGap(10)
          .width('100%')
          .height('100%')
        }
        .width('100%')
        .height(300)
        .backgroundColor('#FFFFFF')
      }
      .width('80%')
      .height(320)
      .padding(10)
      .backgroundColor('#D5D5D5')
      .border({ width: 1, color: '#D5D5D5' })
    }
    .width('100%')
    .padding(20)
  }

  // 根据宽度断点返回对应的列模板
  getColumnsTemplate(): string {
    if (this.widthBp === WidthBreakpoint.WIDTH_XS) {
      return '1fr';
    } else if (this.widthBp === WidthBreakpoint.WIDTH_SM) {
      return '1fr 1fr';
    } else if (this.widthBp === WidthBreakpoint.WIDTH_MD) {
      return '1fr 1fr 1fr';
    } else {
      return '1fr 1fr 1fr 1fr';
    }
  }
}

自定义组件根据容器断点自适应布局

开发者可以创建自定义组件,组件内部使用ContainerReader来实现自适应的内部布局,使组件在不同的使用场景下都能呈现最佳效果。

说明:

使用ContainerReader需要同时导入ContainerReaderAttribute,否则会导致编译报错。

import {ContainerReader, ContainerReaderAttribute, Size} from '@kit.ArkUI';

// 自适应卡片组件,内部使用ContainerReader感知容器尺寸
@Component
struct AdaptiveCard {
  @State containerSize: Size = { width: 0, height: 0 };
  @State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  @Prop title: string = 'Card Title';
  @Prop content: string = 'Card content text';

  build() {
    ContainerReader({
      size: this.containerSize!!,
      widthBreakpoint: this.widthBp!!
    }) {
      Text('width' + this.containerSize?.width)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#D5D5D5')
    .border({ width: 1, color: '#D5D5D5', radius: 8 })
  }
}

// 使用不同尺寸的容器展示AdaptiveCard的自适应效果
@Entry
@Component
struct AdaptiveCardExample {
  build() {
    Column({ space: 20 }) {
      Text('Adaptive Card Components')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)

      Text('Small Container:')
        .fontSize(14)
        .fontColor('#707070')
        .alignSelf(ItemAlign.Start)

      AdaptiveCard({
        title: 'Small Card',
        content: 'This card adapts to small containers with vertical layout.'
      })
        .width(200)
        .height(200)

      Text('Large Container:')
        .fontSize(14)
        .fontColor('#707070')
        .alignSelf(ItemAlign.Start)

      AdaptiveCard({
        title: 'Large Card',
        content: 'This card adapts to large containers with horizontal layout.'
      })
        .width(300)
        .height(150)
    }
    .width('100%')
    .padding(20)
  }
}

左右分栏布局自适应

在主从结构页面中,左侧为固定宽度的Tab标签,右侧为自适应的详情区域。右侧详情区域使用ContainerReader,根据剩余宽度自动切换布局:窄屏时上下排列,宽屏时左右排列。

说明:

使用ContainerReader需要同时导入ContainerReaderAttribute,否则会导致编译报错。

import { ContainerReader, ContainerReaderAttribute, Size } from '@kit.ArkUI';

@Entry
@Component
struct SplitLayoutExample {
  @State containerSize: Size = { width: 0, height: 0 };
  @State widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
  @State currentTabIndex: number = 0;
  private menuItems: string[] = ['选项一', '选项二', '选项三', '选项四'];

  build() {
    Column() {
      Text('分栏布局')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 10 })

      Flex({ direction: FlexDirection.Row }) {
        // 左侧:垂直方向的Tabs标签栏,固定宽度100
        Tabs({ barPosition: BarPosition.Start, index: this.currentTabIndex }) {
          ForEach(this.menuItems, (item: string, index?: number) => {
            TabContent() {
              // 右侧:ContainerReader感知TabContent区域宽度
              ContainerReader({
                size: this.containerSize!!,
                widthBreakpoint: this.widthBp!!
              }) {
                this.buildDetailContent()
              }
              .padding(10)
            }
            .tabBar(item)
          }, (item: string, index?: number) => `${index}`)
        }
        .onChange((index: number) => {
          this.currentTabIndex = index;
        })
        .vertical(true)
        .barWidth(100)
        .backgroundColor('#F7F7F7')
      }
      .width('100%')
      .height(300)
    }
    .width('100%')
    .padding(20)
  }

  // 根据断点切换详情区域布局
  @Builder
  buildDetailContent() {
    if (this.widthBp === WidthBreakpoint.WIDTH_XS || this.widthBp === WidthBreakpoint.WIDTH_SM) {
      // 窄屏:标题和内容上下排列
      Column({ space: 10 }) {
        Column() {
          Text('标题区域')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
        }
        .width('100%')
        .height(60)
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)

        Column() {
          Text('内容区域')
            .fontSize(14)
        }
        .width('100%')
        .layoutWeight(1)
        .backgroundColor('#F0FAFF')
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .height('100%')
    } else {
      // 宽屏:标题和内容左右排列
      Row({ space: 10 }) {
        Column() {
          Text('标题区域')
            .fontSize(16)
            .fontWeight(FontWeight.Bold)
        }
        .width(120)
        .height('100%')
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)

        Column() {
          Text('内容区域')
            .fontSize(14)
        }
        .layoutWeight(1)
        .height('100%')
        .backgroundColor('#F0FAFF')
        .borderRadius(8)
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .height('100%')
    }
  }
}

窄屏时上下排列。

宽屏时左右排列。