文档管理中心
指南应用框架ArkUI(方舟UI框架)UI开发 (ArkTS声明式开发范式)列表与网格创建懒加载布局 (LazyColumnLayout/LazyVGridLayout/LazyVWaterFlowLayout)

创建懒加载布局 (LazyColumnLayout/LazyVGridLayout/LazyVWaterFlowLayout)

ArkUI提供了ScrollListGridWaterFlow四种滚动类组件。其中,Scroll不支持懒加载,List、Grid、WaterFlow虽支持配合LazyForEach实现懒加载,但各自仅支持特定的布局模式。在实际业务场景中,一个滚动页面往往需要混合使用多种布局模式。例如,电商首页可能同时包含多列网格分类入口、瀑布流商品卡片、线性列表推荐;社交应用信息流可能同时包含文本列表、九宫格图片、视频卡片。此时单一滚动组件无法灵活适配,存在一定局限性。

懒加载布局容器是一类嵌套在可滚动父组件(Scroll、List、WaterFlow)内部,负责按需加载子组件的布局容器。这类容器本身不提供滚动能力,由父组件统一处理滚动。它仅创建和布局处于可滚动父组件可视区域内的子组件,并在帧间空闲时隙预加载可视区域上方和下方各半屏的内容,从而减少首帧渲染时间和内存开销。ArkUI提供了三种支持懒加载的布局容器组件:垂直线性布局LazyColumnLayout、垂直网格布局LazyVGridLayout、垂直瀑布流布局LazyVWaterFlowLayout。不同的懒加载布局容器提供不同的布局模式,开发者可以将多种类型的懒加载布局容器组合在同一个父组件中使用,灵活实现混合布局。

从API版本19开始,支持LazyVGridLayout。从API版本26.0.0开始,支持LazyColumnLayout和LazyVWaterFlowLayout。

使用场景

懒加载布局容器适用于以下典型场景。

  • 混合布局页面:一个滚动页面中需要同时展示多种布局模式的内容,如电商首页、社交应用信息流。List、Grid、WaterFlow分别支持线性、网格、瀑布流布局模式,通过懒加载布局容器可以将不同布局模式灵活组合在同一个可滚动父组件中,每个容器独立配置各自的布局参数(如分组、列数),所有区域共享父组件的统一滚动,无需额外处理滚动组件嵌套导致的手势冲突。

  • 独立数据源管理:页面中不同区域的数据来源不同,需要分别管理各自的数据。每个懒加载布局容器可以使用独立的数据源,不同业务模块的数据无需耦合在一起,降低数据管理的复杂度。

  • Scroll大量子组件场景优化:Scroll组件作为通用滚动容器,本身不提供懒加载能力。通过在其中使用懒加载布局容器,可以实现子组件的按需加载,避免一次性创建所有子组件,保障大量子组件场景下的流畅体验。

能力对比

三种懒加载布局容器的能力对比如下。

展开
能力 LazyVGridLayout LazyVWaterFlowLayout LazyColumnLayout
API起始版本 19 26.0.0 26.0.0
设置行间距 支持(rowsGap 支持(rowsGap 支持(space
设置列间距(columnsGap 支持 支持 不支持
设置列数(columnsTemplate 支持 支持 不支持
设置子组件水平对齐方式(alignItems 不支持 不支持 支持
设置头部组件(header 从API版本26.0.0开始支持 支持 支持
设置尾部组件(footer 从API版本26.0.0开始支持 支持 支持
设置吸附效果(sticky 从API版本26.0.0开始支持 支持 支持
监听可视区域子组件索引变化(onVisibleIndexesChange 从API版本26.0.0开始支持 支持 支持
嵌套懒加载布局容器 不支持 不支持 支持
布局模式 垂直网格布局 垂直瀑布流布局 垂直线性布局
示例图

约束与限制

  1. 三种懒加载布局容器的高度默认自适应内容,不建议设置会固定或约束组件垂直方向尺寸的属性,设置后会导致显示异常或无法正常滚动。涉及的属性包括heightsize中的height、constraintSize中的minHeight/maxHeight、aspectRatiolayoutWeight,以及heightLayoutPolicy值的场景。

  2. 三种懒加载布局容器均需要配合可滚动父组件使用,不同容器支持的父组件范围有所差异。

  3. 三种懒加载布局容器在不同父组件下的懒加载支持条件如下。

    • 在List组件下,要求List组件布局方向必须是竖直方向(即listDirection属性设置为Axis.Vertical),在非竖直方向的List中使用懒加载布局容器会导致应用崩溃。当List设置了laneschainAnimationscrollSnapAlign属性中的任意一个或多个时,懒加载布局容器的懒加载功能会失效。

    • 在Scroll组件下,要求Scroll组件布局方向必须是竖直方向(即scrollable属性设置为ScrollDirection.Vertical),在非竖直方向的Scroll中使用懒加载布局容器会导致应用崩溃。

    • 在WaterFlow组件下,要求WaterFlow组件布局方向必须是竖直方向(即layoutDirection属性设置为FlexDirection.Column),在非竖直方向的WaterFlow中使用LazyColumnLayout或LazyVWaterFlowLayout会导致应用崩溃,使用LazyVGridLayout不会导致应用崩溃,但懒加载功能会失效。当WaterFlow为多列模式或分段布局中的多列分段时,三种懒加载布局容器的懒加载功能均会失效。此外,在布局方向为FlexDirection.ColumnReverse的WaterFlow组件下使用懒加载布局容器会导致显示异常。

创建懒加载网格布局 (LazyVGridLayout)

从API version 19开始,支持懒加载垂直网格布局LazyVGridLayout,其适用于等宽等高的多列网格展示场景,如九宫格图片展示、功能入口图标,也适用于不等宽的多列网格展示场景,如按比例分配列宽的数据面板、设置页面。

创建LazyVGridLayout

以下以在Scroll组件中为例,展示了LazyVGridLayout的创建方式。创建时,需要确保Scroll的布局方向为ScrollDirection.Vertical。

收起
自动换行
深色代码主题
复制
  1. Scroll() {
  2. LazyVGridLayout() {
  3. // 子组件
  4. // ...
  5. }
  6. // ...
  7. }
  8. .scrollable(ScrollDirection.Vertical)

设置列数

LazyVGridLayout组件提供了columnsTemplate属性用于设置当前网格布局的列数和每列尺寸占比。

columnsTemplate属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即网格布局的列数,fr前面的数值大小用于计算该列在网格布局宽度上的占比,最终决定该列宽度。不设置时默认1列。设置为'0fr'时,该列的列宽为0,不显示子组件。设置为其他非法值时,子组件显示为固定1列。

图1 列数占比示例图

如上图所示,构建的是一个三行三列的网格布局,其在水平方向上分为四等份,第一列占一份,第二列占两份,第三列占一份。只要将columnsTemplate设置为'1fr 2fr 1fr',即可实现上述网格布局。

收起
自动换行
深色代码主题
复制
  1. LazyVGridLayout() {
  2. // 子组件
  3. // ...
  4. }
  5. .columnsTemplate('1fr 1fr 1fr') // 设置为3列,每列等宽
  6. // ...
  7. LazyVGridLayout() {
  8. // 子组件
  9. // ...
  10. }
  11. .columnsTemplate('1fr 2fr') // 设置为2列,第一列占1份,第二列占2份

columnsTemplate还支持通过repeat关键字自动计算列数,格式为'repeat(auto-fit/auto-fill/auto-stretch, track-size)',其中repeat、auto-fit、auto-fill、auto-stretch为关键字,track-size为列宽,支持px、vp、%等单位,默认单位为vp,也支持无单位的有效数字。track-size至少包含一个有效列宽。

展开
模式 示例 说明
auto-fit 'repeat(auto-fit, 80vp)' 设置最小列宽,自动计算列数和实际列宽。仅支持一个有效列宽值。
auto-fill 'repeat(auto-fill, 80vp)' 设置固定列宽,自动计算列数。支持一个或多个有效列宽,如'repeat(auto-fill, 20 80px)'。
auto-stretch 'repeat(auto-stretch, 80vp)' 设置固定列宽,以columnsGap为最小列间距,自动计算列数和实际列间距。仅支持一个有效列宽值,不支持单位%。

设置行列间距

在两个网格单元之间的垂直间距称为行间距,水平间距称为列间距,如下图所示。

图2 网格的行列间距示例图

LazyVGridLayout组件提供了rowsGapcolumnsGap属性分别设置行间距和列间距。默认值均为LengthMetrics.vp(0),设置为小于0的值时按默认值显示。

收起
自动换行
深色代码主题
复制
  1. LazyVGridLayout() {
  2. // 子组件
  3. // ...
  4. }
  5. // ...
  6. .rowsGap(LengthMetrics.vp(10))
  7. .columnsGap(LengthMetrics.vp(10))

创建懒加载瀑布流布局 (LazyVWaterFlowLayout)

从API版本26.0.0开始,支持懒加载垂直瀑布流布局LazyVWaterFlowLayout,其适用于多列等宽但不等高的卡片展示场景,如图片展示、商品推荐。在瀑布流布局中,每个子节点都会放置在当前总高度最小的列。若多列总高度相同,则按照从左到右的顺序进行填充。

创建LazyVWaterFlowLayout

使用LazyVWaterFlowLayout前,需要通过import { LazyVWaterFlowLayout } from '@kit.ArkUI'导入该组件。

以下以在Scroll组件中为例,展示了LazyVWaterFlowLayout的创建方式。创建时,需要确保Scroll的布局方向为ScrollDirection.Vertical。

收起
自动换行
深色代码主题
复制
  1. Scroll() {
  2. LazyVWaterFlowLayout() {
  3. // 子组件
  4. // ...
  5. }
  6. // ...
  7. }
  8. .scrollable(ScrollDirection.Vertical)

设置列数

LazyVWaterFlowLayout组件提供了columnsTemplate属性用于设置当前瀑布流布局的列数和每列尺寸占比。

columnsTemplate属性值是一个由多个空格和'数字+fr'间隔拼接的字符串,fr的个数即瀑布流布局的列数,fr前面的数值大小用于计算该列在瀑布流布局宽度上的占比,最终决定该列宽度。不设置时默认1列。设置为'0fr'时,该列的列宽为0,不显示子组件。设置为其他非法值时,子组件显示为固定1列。

收起
自动换行
深色代码主题
复制
  1. LazyVWaterFlowLayout() {
  2. // 子组件
  3. // ...
  4. }
  5. .columnsTemplate('1fr 1fr 1fr') // 设置为3列,每列等宽
  6. // ...
  7. LazyVWaterFlowLayout() {
  8. // 子组件
  9. // ...
  10. }
  11. .columnsTemplate('1fr 2fr') // 设置为2列,第一列占1份,第二列占2份

columnsTemplate还支持通过repeat关键字自动计算列数,格式为'repeat(auto-fit/auto-fill/auto-stretch, track-size)',其中repeat、auto-fit、auto-fill、auto-stretch为关键字,track-size为列宽,支持px、vp、%等单位,默认单位为vp,也支持无单位的有效数字。track-size至少包含一个有效列宽。

与LazyVGridLayout组件不同的是,LazyVWaterFlowLayout组件的columnsTemplate属性还支持设置为ItemFillPolicy类型的枚举值,此时会根据组件宽度对应的栅格容器断点类型自动确定列数。例如,设置为ItemFillPolicy.BREAKPOINT_DEFAULT,组件宽度属于sm及更小的断点区间时LazyVWaterFlowLayout显示2列,属于md断点区间时显示3列,属于lg及更大的断点区间时显示5列,且每列均为1fr。

展开
模式 示例 说明
auto-fit 'repeat(auto-fit, 80vp)' 设置最小列宽,自动计算列数和实际列宽。仅支持一个有效列宽值。
auto-fill 'repeat(auto-fill, 80vp)' 设置固定列宽,自动计算列数。支持一个或多个有效列宽,如'repeat(auto-fill, 20 80px)'。
auto-stretch 'repeat(auto-stretch, 80vp)' 设置固定列宽,以columnsGap为最小列间距,自动计算列数和实际列间距。仅支持一个有效列宽值,不支持单位%。
断点适配 ItemFillPolicy.BREAKPOINT_DEFAULT 根据组件宽度对应断点类型确定列数。

设置行列间距

在两个子组件之间的垂直间距称为行间距,水平间距称为列间距。

LazyVWaterFlowLayout组件提供了rowsGapcolumnsGap属性分别设置行间距和列间距。默认值均为LengthMetrics.vp(0),设置为小于0的值时按默认值显示。

收起
自动换行
深色代码主题
复制
  1. LazyVWaterFlowLayout() {
  2. // 子组件
  3. // ...
  4. }
  5. // ...
  6. .rowsGap(LengthMetrics.vp(10))
  7. .columnsGap(LengthMetrics.vp(10))

创建懒加载线性布局 (LazyColumnLayout)

从API版本26.0.0开始,支持懒加载线性布局LazyColumnLayout,其子元素在垂直方向依次排列,常用于单列列表场景,如消息列表、设置项列表。

创建LazyColumnLayout

使用LazyColumnLayout前,需要通过import { LazyColumnLayout } from '@kit.ArkUI'导入该组件。

以下以在Scroll组件中为例,展示了LazyColumnLayout的创建方式。创建时,需要确保Scroll的布局方向为ScrollDirection.Vertical。

收起
自动换行
深色代码主题
复制
  1. Scroll() {
  2. LazyColumnLayout() {
  3. // 子组件
  4. // ...
  5. }
  6. // ...
  7. }
  8. .scrollable(ScrollDirection.Vertical)

设置子组件间距

LazyColumnLayout组件提供了space属性用于设置子组件在垂直方向上的间距。默认值为LengthMetrics.vp(0),设置为小于0的值时按默认值显示。

收起
自动换行
深色代码主题
复制
  1. LazyColumnLayout() {
  2. // 子组件
  3. // ...
  4. }
  5. .space(LengthMetrics.vp(10))

设置子组件对齐方式

LazyColumnLayout组件提供了alignItems属性用于设置子组件在水平方向上的对齐方式。未设置时,对齐方式默认值为HorizontalAlign.Center。

收起
自动换行
深色代码主题
复制
  1. LazyColumnLayout() {
  2. // 子组件
  3. // ...
  4. }
  5. // ...
  6. .alignItems(HorizontalAlign.Start)

嵌套懒加载布局容器

LazyColumnLayout支持嵌套使用LazyVGridLayout、LazyVWaterFlowLayout及其自身,以实现更复杂的混合布局。被嵌套的懒加载布局容器会作为LazyColumnLayout的子组件,在进入可视区域时按需加载。

收起
自动换行
深色代码主题
复制
  1. Scroll() {
  2. LazyColumnLayout() {
  3. // ...
  4. // 区域一:线性列表
  5. LazyColumnLayout() {
  6. // ...
  7. }
  8. // ...
  9. // 区域二:网格布局
  10. LazyVGridLayout() {
  11. // ...
  12. }
  13. // ...
  14. // 区域三:瀑布流布局
  15. LazyVWaterFlowLayout() {
  16. // ...
  17. }
  18. // ...
  19. }
  20. // ...
  21. }
  22. .scrollable(ScrollDirection.Vertical)

监听可视区域变化

三种懒加载布局容器均支持通过onVisibleIndexesChange事件监听可视区域内子组件索引值的变化。在组件初始化时或可视区域内子组件的索引值发生变化时触发回调,返回可视区域内子组件的起始索引值和终止索引值。当懒加载布局容器内没有子组件或可视区域内无可见子组件时,start和end均返回-1。

以下示例分别展示了三种懒加载布局容器注册onVisibleIndexesChange事件回调的方式。

收起
自动换行
深色代码主题
复制
  1. // 区域一:线性列表
  2. LazyColumnLayout() {
  3. // ...
  4. }
  5. .onVisibleIndexesChange((start: number, end: number) => {
  6. console.info('LazyColumnLayout visible indexes: start: ' + start + ', end: ' + end);
  7. })
  8. // ...
  9. // 区域二:网格布局
  10. LazyVGridLayout() {
  11. // ...
  12. }
  13. .onVisibleIndexesChange((start: number, end: number) => {
  14. console.info('LazyVGridLayout visible indexes: start: ' + start + ', end: ' + end);
  15. })
  16. // ...
  17. // 区域三:瀑布流布局
  18. LazyVWaterFlowLayout() {
  19. // ...
  20. }
  21. .onVisibleIndexesChange((start: number, end: number) => {
  22. console.info('LazyVWaterFlowLayout visible indexes: start: ' + start + ', end: ' + end);
  23. // ...
  24. })

利用onVisibleIndexesChange回调,可以在即将触底时提前加载更多数据,实现无限滚动。以下示例展示了LazyVWaterFlowLayout配合LazyForEach实现无限滚动:通过在onVisibleIndexesChange回调中判断当前可视区域的终止索引值(end)是否接近数据源的总数量(totalCount),当剩余数据不足时,向数据源中追加新数据,从而在用户滚动到底部前提前完成数据加载,实现无缝滚动体验。

收起
自动换行
深色代码主题
复制
  1. List({ space: 10 }) {
  2. // ...
  3. // 瀑布流布局
  4. LazyVWaterFlowLayout() {
  5. LazyForEach(this.flowData, (item: number) => {
  6. // ...
  7. }, (item: number) => item.toString())
  8. }
  9. .columnsTemplate('1fr 1fr')
  10. .rowsGap(LengthMetrics.vp(10))
  11. .columnsGap(LengthMetrics.vp(10))
  12. .onVisibleIndexesChange((start: number, end: number) => {
  13. console.info('LazyVWaterFlowLayout visible indexes: start: ' + start + ', end: ' + end);
  14. // 滚动监听:即将触底时提前加载更多数据
  15. if (end + 20 >= this.flowData.totalCount()) {
  16. let currentCount = this.flowData.totalCount();
  17. for (let i = currentCount; i < currentCount + 100; i++) {
  18. this.flowData.pushData(i);
  19. }
  20. }
  21. })
  22. }
  23. .listDirection(Axis.Vertical)

混合布局

  • 直接组合多种懒加载布局容器

通过将多种懒加载布局容器组合在同一个可滚动父组件中使用,可以灵活实现混合布局。

以下示例以List组件作为可滚动父组件为例,在其中同时使用LazyVGridLayout和LazyVWaterFlowLayout,并为每个容器分别配置独立的列数和行列间距,实现了混合布局。

收起
自动换行
深色代码主题
复制
  1. import { LengthMetrics, LazyVWaterFlowLayout, LazyVWaterFlowLayoutAttribute } from '@kit.ArkUI';
  2. class BasicDataSource<T> implements IDataSource {
  3. private listeners: DataChangeListener[] = [];
  4. protected dataArray: T[] = [];
  5. public totalCount(): number {
  6. return this.dataArray.length;
  7. }
  8. public getData(index: number): T {
  9. return this.dataArray[index];
  10. }
  11. registerDataChangeListener(listener: DataChangeListener): void {
  12. if (this.listeners.indexOf(listener) < 0) {
  13. this.listeners.push(listener);
  14. }
  15. }
  16. unregisterDataChangeListener(listener: DataChangeListener): void {
  17. const pos = this.listeners.indexOf(listener);
  18. if (pos >= 0) {
  19. this.listeners.splice(pos, 1);
  20. }
  21. }
  22. notifyDataReload(): void {
  23. this.listeners.forEach(listener => {
  24. listener.onDataReloaded();
  25. })
  26. }
  27. notifyDataAdd(index: number): void {
  28. this.listeners.forEach(listener => {
  29. listener.onDataAdd(index);
  30. })
  31. }
  32. notifyDataDelete(index: number): void {
  33. this.listeners.forEach(listener => {
  34. listener.onDataDelete(index);
  35. })
  36. }
  37. }
  38. class MyDataSource<T> extends BasicDataSource<T> {
  39. public pushData(data: T): void {
  40. this.dataArray.push(data);
  41. this.notifyDataAdd(this.dataArray.length - 1);
  42. }
  43. }
  44. @Entry
  45. @Component
  46. export struct ListNestedLazyLayout {
  47. // 网格区域数据源
  48. private gridData: MyDataSource<number> = new MyDataSource<number>();
  49. // 瀑布流区域数据源
  50. private flowData: MyDataSource<number> = new MyDataSource<number>();
  51. private itemHeight(index: number): number {
  52. return 80 + (index * 37 % 121)
  53. }
  54. aboutToAppear(): void {
  55. for (let i = 0; i < 6; i++) {
  56. this.gridData.pushData(i);
  57. }
  58. for (let i = 0; i < 100; i++) {
  59. this.flowData.pushData(i);
  60. }
  61. }
  62. build() {
  63. NavDestination() {
  64. Column() {
  65. List({ space: 10 }) {
  66. ListItem() {
  67. // 请将$r('app.string.list_nested_lazyLayout_grid')替换为实际资源文件
  68. // 在本示例中该资源文件的value值为"网格布局"
  69. Text($r('app.string.list_nested_lazyLayout_grid'))
  70. .fontSize(14)
  71. .fontColor(Color.Gray)
  72. }
  73. // 等宽的网格布局
  74. LazyVGridLayout() {
  75. LazyForEach(this.gridData, (item: number) => {
  76. Text('item' + item.toString())
  77. .height(96)
  78. .width('100%')
  79. .borderRadius(5)
  80. .backgroundColor('#ffe0e2e4')
  81. .textAlign(TextAlign.Center)
  82. }, (item: number) => item.toString())
  83. }
  84. .columnsTemplate('1fr 1fr 1fr')
  85. .rowsGap(LengthMetrics.vp(10))
  86. .columnsGap(LengthMetrics.vp(10))
  87. .onVisibleIndexesChange((start: number, end: number) => {
  88. console.info('LazyVGridLayout visible indexes: start: ' + start + ', end: ' + end);
  89. })
  90. // 不等宽的网格布局
  91. LazyVGridLayout() {
  92. LazyForEach(this.gridData, (item: number) => {
  93. Text('item' + (this.gridData.totalCount() + item).toString())
  94. .height(96)
  95. .width('100%')
  96. .borderRadius(5)
  97. .backgroundColor('#ffe0e2e4')
  98. .textAlign(TextAlign.Center)
  99. }, (item: number) => item.toString())
  100. }
  101. .columnsTemplate('1fr 2fr')
  102. .rowsGap(LengthMetrics.vp(10))
  103. .columnsGap(LengthMetrics.vp(10))
  104. .margin({ bottom: 16 })
  105. .onVisibleIndexesChange((start: number, end: number) => {
  106. console.info('LazyVGridLayout visible indexes: start: ' + (this.gridData.totalCount() + start) + ', end: ' +
  107. (this.gridData.totalCount() + end));
  108. })
  109. ListItem() {
  110. // 请将$r('app.string.list_nested_lazyLayout_waterFlow')替换为实际资源文件
  111. // 在本示例中该资源文件的value值为"瀑布流布局"
  112. Text($r('app.string.list_nested_lazyLayout_waterFlow'))
  113. .fontSize(14)
  114. .fontColor(Color.Gray)
  115. }
  116. // 瀑布流布局
  117. LazyVWaterFlowLayout() {
  118. LazyForEach(this.flowData, (item: number) => {
  119. Text('item' + item.toString())
  120. .height(this.itemHeight(item))
  121. .width('100%')
  122. .borderRadius(5)
  123. .backgroundColor('#ffe0e2e4')
  124. .textAlign(TextAlign.Center)
  125. }, (item: number) => item.toString())
  126. }
  127. .columnsTemplate('1fr 1fr')
  128. .rowsGap(LengthMetrics.vp(10))
  129. .columnsGap(LengthMetrics.vp(10))
  130. .onVisibleIndexesChange((start: number, end: number) => {
  131. console.info('LazyVWaterFlowLayout visible indexes: start: ' + start + ', end: ' + end);
  132. // 滚动监听:即将触底时提前加载更多数据
  133. if (end + 20 >= this.flowData.totalCount()) {
  134. let currentCount = this.flowData.totalCount();
  135. for (let i = currentCount; i < currentCount + 100; i++) {
  136. this.flowData.pushData(i);
  137. }
  138. }
  139. })
  140. }
  141. .listDirection(Axis.Vertical)
  142. .backgroundColor(Color.White)
  143. .borderRadius(12)
  144. .padding(12)
  145. .width('100%')
  146. .layoutWeight(1)
  147. }
  148. .width('100%')
  149. .height('100%')
  150. .padding({ left: 12, right: 12 })
  151. }
  152. .backgroundColor('#f1f2f3')
  153. // 请将$r('app.string.list_nested_lazyLayout_title')替换为实际资源文件
  154. // 在本示例中该资源文件的value值为"List嵌套懒加载布局容器"
  155. .title($r('app.string.list_nested_lazyLayout_title'))
  156. }
  157. }

图3 List嵌套懒加载布局容器效果示例图

  • 通过LazyColumnLayout嵌套组合多种懒加载布局容器

利用LazyColumnLayout的嵌套能力,可以进一步实现更复杂的混合布局。例如,在一个页面中同时包含线性列表、网格和瀑布流三种排列方式的内容区域。

以下示例以Scroll组件作为可滚动父组件为例,使用LazyColumnLayout作为主布局容器,嵌套LazyColumnLayout(线性列表区域)、LazyVGridLayout(网格区域)和LazyVWaterFlowLayout(瀑布流区域),实现了多种布局方式的混合展示。

收起
自动换行
深色代码主题
复制
  1. import {
  2. LengthMetrics,
  3. LazyVWaterFlowLayout,
  4. LazyVWaterFlowLayoutAttribute,
  5. LazyColumnLayout,
  6. LazyColumnLayoutAttribute
  7. } from '@kit.ArkUI';
  8. class BasicDataSource<T> implements IDataSource {
  9. private listeners: DataChangeListener[] = [];
  10. protected dataArray: T[] = [];
  11. public totalCount(): number {
  12. return this.dataArray.length;
  13. }
  14. public getData(index: number): T {
  15. return this.dataArray[index];
  16. }
  17. registerDataChangeListener(listener: DataChangeListener): void {
  18. if (this.listeners.indexOf(listener) < 0) {
  19. this.listeners.push(listener);
  20. }
  21. }
  22. unregisterDataChangeListener(listener: DataChangeListener): void {
  23. const pos = this.listeners.indexOf(listener);
  24. if (pos >= 0) {
  25. this.listeners.splice(pos, 1);
  26. }
  27. }
  28. notifyDataReload(): void {
  29. this.listeners.forEach(listener => {
  30. listener.onDataReloaded();
  31. })
  32. }
  33. notifyDataAdd(index: number): void {
  34. this.listeners.forEach(listener => {
  35. listener.onDataAdd(index);
  36. })
  37. }
  38. }
  39. class MyDataSource<T> extends BasicDataSource<T> {
  40. public pushData(data: T): void {
  41. this.dataArray.push(data);
  42. this.notifyDataAdd(this.dataArray.length - 1);
  43. }
  44. }
  45. @Entry
  46. @Component
  47. export struct LazyColumnLayoutNestedLazyLayout {
  48. // 线性列表区域数据源
  49. private listData: MyDataSource<number> = new MyDataSource<number>();
  50. // 网格区域数据源
  51. private gridData: MyDataSource<number> = new MyDataSource<number>();
  52. // 瀑布流区域数据源
  53. private flowData: MyDataSource<number> = new MyDataSource<number>();
  54. private itemHeight(index: number): number {
  55. return 80 + (index * 37 % 121)
  56. }
  57. private itemColor(index: number): string {
  58. const colors: string[] = ['#FFE0B2', '#C8E6C9', '#BBDEFB', '#F8BBD0', '#D1C4E9', '#FFF9C4']
  59. return colors[index % colors.length]
  60. }
  61. aboutToAppear(): void {
  62. for (let i = 0; i < 4; i++) {
  63. this.listData.pushData(i);
  64. }
  65. for (let i = 0; i < 9; i++) {
  66. this.gridData.pushData(i);
  67. }
  68. for (let i = 0; i < 100; i++) {
  69. this.flowData.pushData(i);
  70. }
  71. }
  72. build() {
  73. NavDestination() {
  74. Column() {
  75. Scroll() {
  76. LazyColumnLayout() {
  77. // 请将$r('app.string.lazyColumnLayout_nested_lazyLayout_following')替换为实际资源文件
  78. // 在本示例中该资源文件的value值为"推荐关注"
  79. Text($r('app.string.lazyColumnLayout_nested_lazyLayout_following'))
  80. .fontSize(14)
  81. .fontColor(Color.Gray)
  82. .margin({ bottom: 8 })
  83. // 区域一:线性列表
  84. LazyColumnLayout() {
  85. LazyForEach(this.listData, (item: number) => {
  86. Row() {
  87. Text() {
  88. // 请将$r('app.string.lazyColumnLayout_nested_lazyLayout_item')替换为实际资源文件
  89. // 在本示例中该资源文件的value值为"列表项"
  90. Span($r('app.string.lazyColumnLayout_nested_lazyLayout_item'))
  91. Span(item.toString())
  92. }
  93. Blank()
  94. SymbolGlyph($r('sys.symbol.chevron_forward'))
  95. .fontColor([Color.Gray])
  96. }
  97. .width('100%')
  98. .height(56)
  99. .padding({ left: 16, right: 16 })
  100. .borderRadius(8)
  101. .backgroundColor(Color.White)
  102. }, (item: number) => item.toString())
  103. }
  104. .onVisibleIndexesChange((start: number, end: number) => {
  105. console.info('LazyColumnLayout visible indexes: start: ' + start + ', end: ' + end);
  106. })
  107. .space(LengthMetrics.vp(10))
  108. // 请将$r('app.string.lazyColumnLayout_nested_lazyLayout_popular')替换为实际资源文件
  109. // 在本示例中该资源文件的value值为"热门分类"
  110. Text($r('app.string.lazyColumnLayout_nested_lazyLayout_popular'))
  111. .fontSize(14)
  112. .fontColor(Color.Gray)
  113. .margin({ top: 12, bottom: 8 })
  114. // 区域二:网格布局
  115. LazyVGridLayout() {
  116. LazyForEach(this.gridData, (item: number) => {
  117. Column() {
  118. SymbolGlyph($r('sys.symbol.folder_fill'))
  119. .fontSize(32)
  120. .fontColor([Color.Orange])
  121. Text() {
  122. // 请将$r('app.string.lazyColumnLayout_nested_lazyLayout_category')替换为实际资源文件
  123. // 在本示例中该资源文件的value值为"分类"
  124. Span($r('app.string.lazyColumnLayout_nested_lazyLayout_category'))
  125. Span(item.toString())
  126. }
  127. .fontSize(14)
  128. .margin({ top: 6 })
  129. }
  130. .width('100%')
  131. .height(80)
  132. .borderRadius(8)
  133. .backgroundColor(Color.White)
  134. .justifyContent(FlexAlign.Center)
  135. }, (item: number) => item.toString())
  136. }
  137. .onVisibleIndexesChange((start: number, end: number) => {
  138. console.info('LazyVGridLayout visible indexes: start: ' + start + ', end: ' + end);
  139. })
  140. .columnsTemplate('1fr 1fr 1fr')
  141. .rowsGap(LengthMetrics.vp(10))
  142. .columnsGap(LengthMetrics.vp(10))
  143. // 请将$r('app.string.lazyColumnLayout_nested_lazyLayout_recommend')替换为实际资源文件
  144. // 在本示例中该资源文件的value值为"为你推荐"
  145. Text($r('app.string.lazyColumnLayout_nested_lazyLayout_recommend'))
  146. .fontSize(14)
  147. .fontColor(Color.Gray)
  148. .margin({ top: 12, bottom: 8 })
  149. // 区域三:瀑布流布局
  150. LazyVWaterFlowLayout() {
  151. LazyForEach(this.flowData, (item: number) => {
  152. Text() {
  153. // 请将$r('app.string.lazyColumnLayout_nested_lazyLayout_recommendation')替换为实际资源文件
  154. // 在本示例中该资源文件的value值为"推荐内容"
  155. Span($r('app.string.lazyColumnLayout_nested_lazyLayout_recommendation'))
  156. Span(item.toString())
  157. }
  158. .height(this.itemHeight(item))
  159. .width('100%')
  160. .borderRadius(8)
  161. .backgroundColor(this.itemColor(item))
  162. .textAlign(TextAlign.Center)
  163. }, (item: number) => item.toString())
  164. }
  165. .onVisibleIndexesChange((start: number, end: number) => {
  166. console.info('LazyVWaterFlowLayout visible indexes: start: ' + start + ', end: ' + end);
  167. // 即将触底时加载更多数据
  168. if (end + 20 >= this.flowData.totalCount()) {
  169. let currentCount = this.flowData.totalCount();
  170. for (let i = currentCount; i < currentCount + 100; i++) {
  171. this.flowData.pushData(i);
  172. }
  173. }
  174. })
  175. .columnsTemplate('1fr 1fr')
  176. .rowsGap(LengthMetrics.vp(10))
  177. .columnsGap(LengthMetrics.vp(10))
  178. }
  179. .alignItems(HorizontalAlign.Start)
  180. }
  181. .scrollable(ScrollDirection.Vertical)
  182. .padding(12)
  183. .width('100%')
  184. .layoutWeight(1)
  185. }
  186. .width('100%')
  187. .height('100%')
  188. .padding({ left: 12, right: 12 })
  189. .backgroundColor('#f1f2f3')
  190. }
  191. .backgroundColor('#f1f2f3')
  192. // 请将$r('app.string.lazyColumnLayout_nested_lazyLayout_title')替换为实际资源文件
  193. // 在本示例中该资源文件的value值为"LazyColumnLayout嵌套懒加载布局容器"
  194. .title($r('app.string.lazyColumnLayout_nested_lazyLayout_title'))
  195. }
  196. }

在上面的示例中,整个页面仅使用一个Scroll组件提供滚动能力,由LazyColumnLayout作为主布局容器统一管理三个区域的排列。三个区域分别使用独立的LazyForEach数据源(listData、gridData、flowData),数据互不耦合,独立管理。滚动时,所有区域共享同一个Scroll的手势,无需额外的手势处理逻辑。每个区域中的子组件仅在进入可视区域时才会被创建和渲染,从而保障了页面的流畅体验。

图4 LazyColumnLayout嵌套懒加载布局容器效果示例图

分组展示与粘性标题

在混合布局页面中,不同内容区域通常需要分组展示,并配以独立的标题或操作栏,方便用户快速识别和定位内容。从API版本26.0.0开始,三种懒加载布局容器均提供了headerfooter属性,分别用于展示分组标题,提示数据加载完毕(如“已经到底了”)或提供快捷操作(如“查看更多”)。同时,三种容器还提供了sticky属性,可以将header或footer在滚动过程中分别吸附在可视区域的顶部或底部,实现粘性标题效果,帮助用户识别当前所在的内容区域。

添加分组标题

可以通过header属性为懒加载布局容器添加头部组件,用于展示分组标题。以下示例使用@Builder构建了一个带参数的分组标题组件,并通过header属性设置到LazyVGridLayout中。

收起
自动换行
深色代码主题
复制
  1. // 内层分组header:显示月份标题,滚动时吸顶
  2. @Builder
  3. MonthHeaderBuilder(title: string, count: number) {
  4. Row() {
  5. Text(title)
  6. .fontSize(16)
  7. .fontWeight(FontWeight.Bold)
  8. Blank()
  9. // 请将$r('app.string.lazyLayout_photo_count')替换为实际资源文件,在本示例中该资源文件的value值为"%d张",表示照片的张数
  10. Text($r('app.string.lazyLayout_photo_count', count))
  11. .fontSize(14)
  12. .fontColor(Color.Gray)
  13. }
  14. .width('100%')
  15. .height(48)
  16. .padding({ left: 16, right: 16 })
  17. .backgroundColor(Color.White)
  18. .alignItems(VerticalAlign.Center)
  19. }
  20. // ...
  21. build() {
  22. // ...
  23. // 通过LazyForEach动态创建每个月份分组
  24. LazyForEach(this.groupData, (group: PhotoGroup, index: number) => {
  25. // 内层:每个分组为一个网格布局
  26. LazyVGridLayout() {
  27. LazyForEach(group.photos, (item: number) => {
  28. // ...
  29. }, (item: number) => `${index}_${item}`)
  30. }
  31. // ...
  32. .header(this.MonthHeaderBuilder(group.title, group.photos.totalCount())) // 内层分组header:显示月份标题
  33. // ...
  34. }, (group: PhotoGroup) => group.title)
  35. // ...
  36. }

添加末尾提示

可以通过footer属性为懒加载布局容器添加尾部组件,用于提示数据加载完毕。以下示例使用@Builder构建了一个尾部提示组件,并通过footer属性设置到LazyColumnLayout中。

收起
自动换行
深色代码主题
复制
  1. // 外层footer:显示"已经到底了"
  2. @Builder
  3. GroupFooterBuilder() {
  4. // 请将$r('app.string.lazyLayout_no_more_content')替换为实际资源文件,在本示例中该资源文件的value值为"—— 已经到底了 ——"
  5. Text($r('app.string.lazyLayout_no_more_content'))
  6. .fontSize(14)
  7. .fontColor(Color.Gray)
  8. .width('100%')
  9. .height(48)
  10. .textAlign(TextAlign.Center)
  11. }
  12. build() {
  13. // ...
  14. Scroll() {
  15. LazyColumnLayout() {
  16. // ...
  17. }
  18. // ...
  19. .footer(this.GroupFooterBuilder()) // 外层footer:显示"已经到底了"
  20. }
  21. .scrollable(ScrollDirection.Vertical)
  22. .width('100%')
  23. .layoutWeight(1)
  24. // ...
  25. }

设置粘性标题

通过sticky属性,可以将headerfooter在滚动过程中吸附在可视区域的顶部或底部,帮助用户在滚动时识别当前所在的内容区域。sticky属性支持以下模式。

  • StickyStyle.Header:仅header吸附在可视区域顶部,常用于分组标题吸顶。
  • StickyStyle.Footer:仅footer吸附在可视区域底部,常用于汇总信息或操作入口吸底。
  • StickyStyle.BOTH:同时支持header吸附在顶部和footer吸附在底部。

图5 三种StickyStyle效果示例图

展开
StickyStyle.Header StickyStyle.Footer StickyStyle.BOTH

由于浮点数计算精度,设置sticky后,在滚动过程中小概率产生缝隙,可以通过pixelRound指定当前组件向下像素取整解决该问题。

以下示例模拟图库页面,展示了分组展示与粘性标题的效果。外层LazyColumnLayout通过footer显示”已经到底了”,提示数据已全部加载;内层通过LazyForEach动态创建多个LazyVGridLayout展示各月份照片网格,每个LazyVGridLayout设置了header和sticky(StickyStyle.Header),使月份标题在滚动时吸顶。

收起
自动换行
深色代码主题
复制
  1. import { LengthMetrics, LazyColumnLayout, LazyColumnLayoutAttribute } from '@kit.ArkUI';
  2. class BasicDataSource<T> implements IDataSource {
  3. private listeners: DataChangeListener[] = [];
  4. protected dataArray: T[] = [];
  5. public totalCount(): number {
  6. return this.dataArray.length;
  7. }
  8. public getData(index: number): T {
  9. return this.dataArray[index];
  10. }
  11. registerDataChangeListener(listener: DataChangeListener): void {
  12. if (this.listeners.indexOf(listener) < 0) {
  13. this.listeners.push(listener);
  14. }
  15. }
  16. unregisterDataChangeListener(listener: DataChangeListener): void {
  17. const pos = this.listeners.indexOf(listener);
  18. if (pos >= 0) {
  19. this.listeners.splice(pos, 1);
  20. }
  21. }
  22. notifyDataReload(): void {
  23. this.listeners.forEach(listener => {
  24. listener.onDataReloaded();
  25. })
  26. }
  27. notifyDataAdd(index: number): void {
  28. this.listeners.forEach(listener => {
  29. listener.onDataAdd(index);
  30. })
  31. }
  32. notifyDataDelete(index: number): void {
  33. this.listeners.forEach(listener => {
  34. listener.onDataDelete(index);
  35. })
  36. }
  37. }
  38. class MyDataSource<T> extends BasicDataSource<T> {
  39. public pushData(data: T): void {
  40. this.dataArray.push(data);
  41. this.notifyDataAdd(this.dataArray.length - 1);
  42. }
  43. }
  44. class PhotoGroup {
  45. public title: string
  46. public photos: MyDataSource<number> = new MyDataSource<number>()
  47. constructor(title: string) {
  48. this.title = title
  49. }
  50. }
  51. @Entry
  52. @Component
  53. export struct LazyLayoutGroup {
  54. private groupData: MyDataSource<PhotoGroup> = new MyDataSource<PhotoGroup>();
  55. aboutToAppear(): void {
  56. // 初始化数据
  57. const months: string[] = ['2026年1月', '2026年2月', '2026年3月'];
  58. for (let m = 0; m < months.length; m++) {
  59. let group = new PhotoGroup(months[m]);
  60. let photoCount = 6 + m * 3;
  61. for (let i = 0; i < photoCount; i++) {
  62. group.photos.pushData(i);
  63. }
  64. this.groupData.pushData(group);
  65. }
  66. }
  67. // 底部工具栏
  68. @Builder
  69. BottomToolBarBuilder() {
  70. Row() {
  71. Column() {
  72. SymbolGlyph($r('sys.symbol.picture_fill'))
  73. .fontSize(24)
  74. .fontColor(['#FF007DFF'])
  75. // 请将$r('app.string.lazyLayout_photo')替换为实际资源文件,在本示例中该资源文件的value值为"照片"
  76. Text($r('app.string.lazyLayout_photo'))
  77. .fontSize(12)
  78. .fontColor('#FF007DFF')
  79. .margin({ top: 2 })
  80. }
  81. Column() {
  82. SymbolGlyph($r('sys.symbol.square_fill_grid_2x2'))
  83. .fontSize(24)
  84. .fontColor([Color.Gray])
  85. // 请将$r('app.string.lazyLayout_album')替换为实际资源文件,在本示例中该资源文件的value值为"相册"
  86. Text($r('app.string.lazyLayout_album'))
  87. .fontSize(12)
  88. .fontColor(Color.Gray)
  89. .margin({ top: 2 })
  90. }
  91. .margin({ left: 36 })
  92. Blank()
  93. // 请将$r('app.string.lazyLayout_select')替换为实际资源文件,在本示例中该资源文件的value值为"选择"
  94. Text($r('app.string.lazyLayout_select'))
  95. .fontSize(14)
  96. .fontColor('#FF007DFF')
  97. }
  98. .width('100%')
  99. .height(64)
  100. .padding({ left: 16, right: 16 })
  101. .backgroundColor(Color.White)
  102. .alignItems(VerticalAlign.Center)
  103. }
  104. // 内层分组header:显示月份标题,滚动时吸顶
  105. @Builder
  106. MonthHeaderBuilder(title: string, count: number) {
  107. Row() {
  108. Text(title)
  109. .fontSize(16)
  110. .fontWeight(FontWeight.Bold)
  111. Blank()
  112. // 请将$r('app.string.lazyLayout_photo_count')替换为实际资源文件,在本示例中该资源文件的value值为"%d张",表示照片的张数
  113. Text($r('app.string.lazyLayout_photo_count', count))
  114. .fontSize(14)
  115. .fontColor(Color.Gray)
  116. }
  117. .width('100%')
  118. .height(48)
  119. .padding({ left: 16, right: 16 })
  120. .backgroundColor(Color.White)
  121. .alignItems(VerticalAlign.Center)
  122. }
  123. // 外层footer:显示"已经到底了"
  124. @Builder
  125. GroupFooterBuilder() {
  126. // 请将$r('app.string.lazyLayout_no_more_content')替换为实际资源文件,在本示例中该资源文件的value值为"—— 已经到底了 ——"
  127. Text($r('app.string.lazyLayout_no_more_content'))
  128. .fontSize(14)
  129. .fontColor(Color.Gray)
  130. .width('100%')
  131. .height(48)
  132. .textAlign(TextAlign.Center)
  133. }
  134. build() {
  135. NavDestination() {
  136. Column() {
  137. Scroll() {
  138. LazyColumnLayout() {
  139. // 通过LazyForEach动态创建每个月份分组
  140. LazyForEach(this.groupData, (group: PhotoGroup, index: number) => {
  141. // 内层:每个分组为一个网格布局
  142. LazyVGridLayout() {
  143. LazyForEach(group.photos, (item: number) => {
  144. Column() {
  145. SymbolGlyph($r('sys.symbol.picture'))
  146. .fontSize(24)
  147. .fontColor([Color.Gray])
  148. }
  149. .width('100%')
  150. .aspectRatio(1)
  151. .borderRadius(4)
  152. .backgroundColor('#e8e8e8')
  153. .justifyContent(FlexAlign.Center)
  154. }, (item: number) => `${index}_${item}`)
  155. }
  156. .columnsTemplate('1fr 1fr 1fr')
  157. .rowsGap(LengthMetrics.vp(2))
  158. .columnsGap(LengthMetrics.vp(2))
  159. .header(this.MonthHeaderBuilder(group.title, group.photos.totalCount())) // 内层分组header:显示月份标题
  160. .sticky(StickyStyle.Header) // header吸顶
  161. }, (group: PhotoGroup) => group.title)
  162. }
  163. .space(LengthMetrics.vp(12))
  164. .footer(this.GroupFooterBuilder()) // 外层footer:显示"已经到底了"
  165. }
  166. .scrollable(ScrollDirection.Vertical)
  167. .width('100%')
  168. .layoutWeight(1)
  169. .scrollBar(BarState.Off)
  170. .backgroundColor(Color.White)
  171. // 底部工具栏
  172. this.BottomToolBarBuilder()
  173. }
  174. .width('100%')
  175. .height('100%')
  176. }
  177. .backgroundColor('#f1f2f3')
  178. // 请将$r('app.string.lazyLayout_group_title')替换为实际资源文件
  179. // 在本示例中该资源文件的value值为"分组展示与粘性标题"
  180. .title($r('app.string.lazyLayout_group_title'))
  181. }
  182. }

图6 分组展示与粘性标题效果示例图

在 指南 中进行搜索
请输入您想要搜索的关键词