管理中心

LazyDynamicLayout

Phone26.0.0+PC/2in126.0.0+Tablet26.0.0+TV26.0.0+Wearable26.0.0+

该组件用于实现支持懒加载的动态布局容器,支持开发者自定义布局算法。适用于在可滚动组件中展示大量子组件的场景,通过按需加载和布局可视区域内的子组件,减少首帧渲染时间和内存开销。

其父组件仅限于List、Scroll、WaterFlow、FlowItem或LazyColumnLayout,并支持使用自定义组件或NodeContainer组件封装后应用在上述组件中。

说明
  • 本模块接口仅可在Stage模型下使用。

  • 该组件在不同父组件下的懒加载支持条件如下:

    1. 在WaterFlow组件下,仅在WaterFlow组件的单列模式或分段布局中的单列分段场景下使用时支持懒加载。
    2. 在List组件下,当lanes大于1、chainAnimation设置为true或scrollSnapAlign设置为ScrollSnapAlign.NONE以外的值时,List不使用嵌套懒加载测量流程,该组件按普通子项测量,懒加载功能失效。
    3. 在Scroll、List、WaterFlow组件下使用时,Scroll、List、WaterFlow的滚动方向(水平或垂直)必须和该组件布局方向相同,若布局方向不同会导致应用崩溃。
    4. 通过FlowItem、LazyColumnLayout、自定义组件或NodeContainer封装使用时,懒加载行为取决于其上层滚动组件(如WaterFlow、Scroll或List)的配置条件。
  • 此处的父组件指最靠近当前组件的上层滚动组件,其他文档下的具体含义请参考对应内容。

起始版本: 26.0.0

子组件

可以包含子组件。

接口

LazyDynamicLayout(algorithm: LazyLayoutAlgorithm)

懒加载动态布局容器。

起始版本: 26.0.0

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API版本26.0.0开始,该接口支持在元服务中使用。

系统能力: SystemCapability.ArkUI.ArkUI.Full

参数:

展开
参数名 类型 必填 说明
algorithm LazyLayoutAlgorithm 是 指定懒加载动态布局组件的布局算法。需传入LazyLayoutAlgorithm类型的实例,可通过继承LazyCustomLayoutAlgorithm自定义测量和布局逻辑。自定义算法中获取子组件或子组件总数时,需分别使用ExpandMode.LAZY_NOT_EXPAND和ChildrenCountMode.ALL_NOT_EXPAND,避免全量加载导致懒加载失效。

属性

支持通用属性。

说明

当布局算法为LazyCustomLayoutAlgorithm时,LazyDynamicLayout组件FrameNode的setMeasuredSize方法优先级高于尺寸设置和边框设置属性,子组件FrameNode的measure和layout方法优先级高于ignoreLayoutSafeArea属性。自定义算法完成测量或布局后,框架不再执行默认测量或布局流程,而是采用自定义算法设置的尺寸和位置。

事件

支持通用事件。

onVisibleIndexesChange

Phone26.0.0+PC/2in126.0.0+Tablet26.0.0+TV26.0.0+Wearable26.0.0+

onVisibleIndexesChange(callback: Callback<number[]> | undefined)

设置onVisibleIndexesChange回调函数。当LazyDynamicLayout可视区域内子组件索引列表发生变化时触发回调,返回可视区域内子组件索引列表。

起始版本: 26.0.0

模型约束: 此接口仅可在Stage模型下使用。

元服务API: 从API版本26.0.0开始,该接口支持在元服务中使用。

系统能力: SystemCapability.ArkUI.ArkUI.Full

参数:

展开
参数名 类型 必填 说明
callback Callback<number[]> | undefined 是 当LazyDynamicLayout可视区域内子组件索引列表发生变化时触发的回调函数。返回可视区域内子组件索引列表。入参为undefined时,取消监听。

示例

示例1(实现懒加载自定义布局)

通过List和LazyDynamicLayout组件实现自定义的懒加载列表布局,并通过onVisibleIndexesChange在可视区域发生变化时回调索引。

LazyListLayout实现了一个自定义懒加载列表布局算法,布局算法中通过setAdjustedOffset接口,确保子组件布局间隔变化时可视区域内第一个子组件的位置不变。

MyDataSource实现了LazyForEach数据源接口IDataSource,用于通过LazyForEach给LazyDynamicLayout提供子组件。

从API版本26.0.0开始,新增LazyDynamicLayout组件。

收起
自动换行
深色代码主题
复制
  1. import { LazyDynamicLayout, LazyDynamicLayoutAttribute } from '@kit.ArkUI';
  2. import { MyDataSource } from './MyDataSource';
  3. import { LazyListLayout } from './LazyListLayout';
  4. // 自定义懒加载列表布局组件
  5. @Component
  6. struct MyLazyListLayout {
  7. // 间隔大小,使用@Watch监听变化,变化时触发onSpaceChange方法
  8. @Prop @Watch('onSpaceChange') space: number;
  9. arr: MyDataSource<string> = new MyDataSource<string>();
  10. private itemHeight: number = 100;
  11. // 懒加载布局算法实例,将高度转换为像素单位
  12. private lazyAlgorithm: LazyListLayout = new LazyListLayout(this.getUIContext().vp2px(this.itemHeight));
  13. // 间隔变化时更新布局算法中的间隔值
  14. onSpaceChange(): void {
  15. this.lazyAlgorithm.setSpace(this.getUIContext().vp2px(this.space));
  16. }
  17. aboutToAppear(): void {
  18. this.lazyAlgorithm.setSpace(this.getUIContext().vp2px(this.space));
  19. }
  20. build() {
  21. // 使用LazyDynamicLayout组件,传入懒加载布局算法
  22. LazyDynamicLayout(this.lazyAlgorithm) {
  23. LazyForEach(this.arr, (item: string) => {
  24. Text(item)
  25. .height(this.itemHeight)
  26. .width('100%')
  27. .borderRadius(8)
  28. .backgroundColor('#E0E0FF')
  29. .padding(10)
  30. })
  31. }
  32. // 监听可视区域内子组件索引变化
  33. .onVisibleIndexesChange((child: number[]) => {
  34. console.info(`onVisibleIndexesChange:start:${child}`);
  35. })
  36. }
  37. }
  38. // 定义分组数据接口
  39. interface GroupData {
  40. title: string;
  41. data: MyDataSource<string>;
  42. }
  43. // 主页面组件
  44. @Entry
  45. @Component
  46. struct CustomListLayoutTest {
  47. @State groupArr: GroupData[] = []; // 分组数据数组
  48. @State space: number = 5; // 列表项间隔大小
  49. aboutToAppear(): void {
  50. for (let i = 0; i < 3; i++) {
  51. let data = new MyDataSource<string>();
  52. for (let j = 0; j < 10; j++) {
  53. data.pushData('item' + j.toString());
  54. }
  55. this.groupArr.push({ title: 'group' + i.toString(), data: data });
  56. }
  57. }
  58. build() {
  59. Stack({ alignContent: Alignment.Bottom }) {
  60. List() {
  61. ForEach(this.groupArr, (item: GroupData) => {
  62. ListItem() {
  63. Text(item.title).margin({ top: 20, bottom: 8 })
  64. }
  65. // 使用自定义懒加载布局组件
  66. MyLazyListLayout({ arr: item.data, space: this.space })
  67. })
  68. }
  69. .layoutWeight(1)
  70. .padding({ left: 12, right: 12 })
  71. .height('100%')
  72. .width('100%')
  73. Button('Space:' + this.space.toString())
  74. .onClick(() => {
  75. // 在5和10之间切换间隔大小,切换前后保持可视区域第一个子组件位置不变
  76. this.space = this.space === 5 ? 10 : 5;
  77. })
  78. }
  79. .height('100%')
  80. .width('100%')
  81. }
  82. }
收起
自动换行
深色代码主题
复制
  1. // LazyListLayout.ets
  2. // 导入布局相关的接口和类
  3. import { LayoutConstraint, LazyLayoutHelper, LazyCustomLayoutAlgorithm, ExpandMode, ChildrenCountMode,
  4. LazyLayoutDirection } from '@kit.ArkUI';
  5. // 自定义懒加载列表布局算法,继承自LazyCustomLayoutAlgorithm
  6. export class LazyListLayout extends LazyCustomLayoutAlgorithm {
  7. private itemHeight: number = 320; // 每个列表项的高度(像素)
  8. private totalHeight: number = 0; // 列表总高度
  9. private childCnt: number = 0; // 子组件总数
  10. private startIndex: number = -1; // 当前可视区域的起始索引
  11. private endIndex: number = -1; // 当前可视区域的结束索引
  12. private space: number = 0; // 当前间隔大小
  13. private prevSpace: number = 0; // 上一次的间隔大小
  14. selfNode?: FrameNode; // 自身FrameNode节点引用
  15. // 构造函数,接收列表项高度参数
  16. constructor(itemHeight: number) {
  17. super();
  18. this.itemHeight = itemHeight;
  19. }
  20. // 设置列表项间隔大小
  21. setSpace(value: number): void {
  22. if (this.space == value) {
  23. return;
  24. }
  25. this.prevSpace = this.space;
  26. this.space = value;
  27. // 触发布局重新计算
  28. this.selfNode?.setNeedsLayout();
  29. }
  30. // 测量方法,测量子组件和计算组件大小
  31. onMeasure(self: FrameNode, constraint: LayoutConstraint, helper?: LazyLayoutHelper): void {
  32. // 获取子组件总数,getChildrenCount接口使用ChildrenCountMode.ALL_NOT_EXPAND,避免获取子组件总数时全量加载子组件导致懒加载失效。
  33. this.childCnt = self.getChildrenCount(ChildrenCountMode.ALL_NOT_EXPAND);
  34. this.selfNode = self;
  35. // 如果没有懒加载helper,则测量所有子组件
  36. if (!helper) {
  37. this.measureAllChildren(self, constraint);
  38. self.setMeasuredSize({ width: constraint.maxSize.width, height: this.totalHeight });
  39. this.prevSpace = this.space;
  40. return;
  41. }
  42. // 获取可视区域的起始和结束位置
  43. let viewStart = helper.getViewStart();
  44. let viewEnd = helper.getViewEnd();
  45. let prevTotalHeight = this.totalHeight;
  46. // 计算列表总高度:子组件数量 * (子组件高度 + 间隔) - 最后一个间隔
  47. this.totalHeight = Math.max(this.childCnt * (this.itemHeight + this.space) - this.space, 0);
  48. // 正向布局(从上到下)
  49. if (helper.getLazyLayoutDirection() == LazyLayoutDirection.FORWARD) {
  50. // 如果间隔变化,需要调整偏移量以保持可视区域第一个子组件位置不变
  51. if (this.startIndex > 0 && this.startIndex < this.childCnt && this.prevSpace != this.space) {
  52. let adjustStartOffset = this.startIndex * (this.prevSpace - this.space);
  53. console.info(`Top setAdjustedOffset:${adjustStartOffset}`);
  54. helper.setAdjustedOffset(adjustStartOffset);
  55. viewStart -= adjustStartOffset;
  56. viewEnd -= adjustStartOffset;
  57. }
  58. } else {
  59. // 反向布局(从下到上)
  60. if (this.endIndex >= 0 && this.endIndex < this.childCnt - 1 && this.prevSpace != this.space) {
  61. let adjustEndOffset = (this.childCnt - 1 - this.endIndex) * (this.space - this.prevSpace);
  62. let adjustStartOffset = this.totalHeight - prevTotalHeight - adjustEndOffset;
  63. console.info(`Bottom setAdjustedOffset:${adjustEndOffset}`);
  64. helper.setAdjustedOffset(adjustEndOffset);
  65. viewStart += adjustStartOffset;
  66. viewEnd += adjustStartOffset;
  67. } else if (this.totalHeight != prevTotalHeight) {
  68. let adjustOffset = this.totalHeight - prevTotalHeight;
  69. viewStart += adjustOffset;
  70. viewEnd += adjustOffset;
  71. }
  72. }
  73. this.prevSpace = this.space;
  74. // 如果可视区域不在内容范围内,清空索引
  75. if (viewStart > this.totalHeight || viewEnd < 0 || this.childCnt == 0) {
  76. this.startIndex = -1;
  77. this.endIndex = -1;
  78. this.totalHeight = Math.max(this.childCnt * (this.itemHeight + this.space) - this.space, 0);
  79. self.setMeasuredSize({ width: constraint.maxSize.width, height: this.totalHeight });
  80. return;
  81. }
  82. // 计算可视区域的起始和结束索引
  83. let prevStartIndex = this.startIndex;
  84. let prevEndIndex = this.endIndex;
  85. this.startIndex = Math.floor(viewStart / (this.itemHeight + this.space));
  86. this.startIndex = Math.max(this.startIndex, 0);
  87. this.endIndex = Math.floor(viewEnd / (this.itemHeight + this.space));
  88. this.endIndex = Math.min(this.endIndex, this.childCnt - 1);
  89. // 测量可视区域内的子组件
  90. for (let i = this.startIndex; i <= this.endIndex; i++) {
  91. // 调用getChild时使用ExpandMode.LAZY_NOT_EXPAND参数,避免获取子组件时全量加载导致懒加载失效。
  92. let child = self.getChild(i, ExpandMode.LAZY_NOT_EXPAND);
  93. if (child) {
  94. child.measure(constraint);
  95. } else {
  96. console.error(`Get child[${i}] error`);
  97. }
  98. }
  99. // 收集需要回收的子组件索引
  100. let recycleList: number[] = [];
  101. // 如果起始索引后移,回收之前的子组件
  102. if (prevStartIndex < this.startIndex) {
  103. for (let i = prevStartIndex; i < this.startIndex; i++) {
  104. recycleList.push(i);
  105. }
  106. }
  107. // 如果结束索引前移,回收之后的子组件
  108. if (prevEndIndex > this.endIndex) {
  109. for (let i = this.endIndex + 1; i <= prevEndIndex; i++) {
  110. recycleList.push(i);
  111. }
  112. }
  113. // 将不再可见的子组件设置为非激活态
  114. helper.setChildrenInactive(recycleList);
  115. // 设置测量后的尺寸
  116. self.setMeasuredSize({ width: constraint.maxSize.width, height: this.totalHeight });
  117. }
  118. // 测量所有子组件(非懒加载模式)
  119. private measureAllChildren(self: FrameNode, constraint: LayoutConstraint): void {
  120. for (let i = 0; i < this.childCnt; i++) {
  121. let child = self.getChild(i, ExpandMode.LAZY_NOT_EXPAND);
  122. if (child) {
  123. child.measure(constraint);
  124. } else {
  125. console.error(`Get child[${i}] error`);
  126. }
  127. }
  128. this.startIndex = 0;
  129. this.endIndex = this.childCnt - 1;
  130. this.totalHeight = Math.max(this.childCnt * (this.itemHeight + this.space) - this.space, 0);
  131. }
  132. // 布局方法,确定每个子组件的位置
  133. onLayout(self: FrameNode): void {
  134. if (this.childCnt == 0) {
  135. return;
  136. }
  137. // 布局可视区域内的子组件
  138. for (let i = this.startIndex; i <= this.endIndex; i++) {
  139. let child = self.getChild(i, ExpandMode.LAZY_NOT_EXPAND);
  140. child?.layout({ x: 0, y: i * (this.itemHeight + this.space) });
  141. }
  142. }
  143. }
收起
自动换行
深色代码主题
复制
  1. // MyDataSource.ets
  2. // 基础数据源类,实现IDataSource接口
  3. export class BasicDataSource<T> implements IDataSource {
  4. private listeners: DataChangeListener[] = [];
  5. protected dataArray: T[] = [];
  6. public totalCount(): number {
  7. return this.dataArray.length;
  8. }
  9. public getData(index: number): T {
  10. return this.dataArray[index];
  11. }
  12. registerDataChangeListener(listener: DataChangeListener): void {
  13. if (this.listeners.indexOf(listener) < 0) {
  14. console.info('add listener');
  15. this.listeners.push(listener);
  16. }
  17. }
  18. unregisterDataChangeListener(listener: DataChangeListener): void {
  19. const pos = this.listeners.indexOf(listener);
  20. if (pos >= 0) {
  21. console.info('remove listener');
  22. this.listeners.splice(pos, 1);
  23. }
  24. }
  25. notifyDataReload(): void {
  26. this.listeners.forEach(listener => {
  27. listener.onDataReloaded();
  28. });
  29. }
  30. notifyDataAdd(index: number): void {
  31. this.listeners.forEach(listener => {
  32. listener.onDataAdd(index);
  33. });
  34. }
  35. notifyDataChange(index: number): void {
  36. this.listeners.forEach(listener => {
  37. listener.onDataChange(index);
  38. });
  39. }
  40. notifyDataDelete(index: number): void {
  41. this.listeners.forEach(listener => {
  42. listener.onDataDelete(index);
  43. });
  44. }
  45. notifyDataMove(from: number, to: number): void {
  46. this.listeners.forEach(listener => {
  47. listener.onDataMove(from, to);
  48. });
  49. }
  50. notifyDatasetChange(operations: DataOperation[]): void {
  51. this.listeners.forEach(listener => {
  52. listener.onDatasetChange(operations);
  53. });
  54. }
  55. }
  56. export class MyDataSource<T> extends BasicDataSource<T> {
  57. public shiftData(): void {
  58. this.dataArray.shift();
  59. this.notifyDataDelete(0);
  60. }
  61. public unshiftData(data: T): void {
  62. this.dataArray.unshift(data);
  63. this.notifyDataAdd(0);
  64. }
  65. public pushData(data: T): void {
  66. this.dataArray.push(data);
  67. this.notifyDataAdd(this.dataArray.length - 1);
  68. }
  69. public popData(): void {
  70. if (this.dataArray.length > 0) {
  71. this.dataArray.pop();
  72. this.notifyDataDelete(this.dataArray.length);
  73. }
  74. }
  75. public clearData(): void {
  76. this.dataArray = [];
  77. this.notifyDataReload();
  78. }
  79. }