← 技术文章

WaterFlow 布局与常见问题排查

对比滚动组件与 WaterFlow 布局模式,说明分组、缓存和惰性布局的用法,并通过日志与 dump 排查常见问题。附交互演示及 OpenHarmony 7.1 规划特性。

1. 滚动组件选型

WaterFlow、List、Grid 和 Scroll 都能构建滚动页面,主要区别是内容的排列方式。选型时先看卡片是否等高,以及页面需要列表、网格还是整块内容滚动。

四个组件的典型页面:瀑布流、列表、网格、长内容滚动。
四个组件的典型页面:瀑布流、列表、网格、长内容滚动。

WaterFlow

卡片高度不同,各列紧接排列

List

按列表项顺序排列

Grid

按行列格子对齐

Scroll

让一整块内容区域滚动

典型排列示意;List 也支持多列,Grid 也支持跨格。
组件排列方式常见使用场景
WaterFlow:瀑布流卡片高度可以不同,每一列各自接着往下排,尽量排紧商品推荐、图片社区、不等高内容卡片
List:列表按列表项和分组组织内容,也能配置多列消息、联系人、设置、带侧滑操作的列表
Grid:网格按行列格子组织内容,强调对齐,也支持跨格相册、应用入口、规则的商品网格
Scroll:滚动容器让一整块内容区域滚动,内部排列由子组件决定详情页、表单、图文混排的长页面

2. 布局规则与缓存

2.1 最短列规则

正向布局时,每张卡片放到当前最短的一列。
正向布局时,每张卡片放到当前最短的一列。

正向布局时,WaterFlow 把每张卡片放到当前最短的一列。以双列为例,前两张卡片分别放在左列和右列。之后逐张比较两列的底边,把卡片接到较短的列下方。

两种模式都遵循最短列规则,区别是保留的布局信息范围。顺排模式(ALWAYS_TOP_DOWN)从顶部布局,保留已计算的位置。滑窗模式(SLIDING_WINDOW)只维护当前视窗附近的布局。

左列和中列底边为 730.70,右列为 730.50。组件按布局值选择最短列。
左列和中列底边为 730.70,右列为 730.50。组件按布局值选择最短列。

三列看起来一样高,也可能存在布局值的差异。某应用的最后一项排到了最右列。查看 dump(组件状态转储)后发现,左列和中列的底边是 730.70,右列是 730.50。

0.20 的差值在像素取整后难以分辨,但组件仍按布局值选列。右列最短,最后一项就排到右列。只有三列底边完全相等时,组件才从左往右选择。

dump 不单独打印各列高度,可以用 FlowItem 的 FrameRect 计算。将 y 坐标与高度相加,得到该项的底边。同一列最后一项的底边,就是该列的底边。以下是输出示意,右侧标注了计算结果:

FlowItem ID: 120  FrameRect: RectT (0.00, 610.70) - [118.00 x 120.00]     左列底边 730.70
FlowItem ID: 121  FrameRect: RectT (124.00, 640.70) - [118.00 x 90.00]    中列底边 730.70
FlowItem ID: 122  FrameRect: RectT (248.00, 620.50) - [118.00 x 110.00]   右列底边 730.50
FlowItem ID: 123  FrameRect: RectT (248.00, 738.50) - [118.00 x 100.00]   最后一项放到右列

2.2 布局模式对比

下面的蓝框表示当前视窗。远跳后向上回滑,顺排模式读取已记录的位置;滑窗模式则逐项补入上方卡片。滑窗若没有历史列记录,就按当前各列的边界选列;有历史记录时,优先恢复原列。

C 段 · 两列 · 高度不变

顺排模式

已有位置固定,视窗向上移动
视窗内外的布局过程视窗框移动,纵坐标向下增大。左侧淡框是已有位置记录。右侧队列表示待补入的数据,各项的位置尚未确定。上方前缀已定;回滑读取原位置#29已定#28已定#27已定#26已定#25已定#24已定左列右列#22#23#24#25#26#27#28#29#30#31#32#33#34#35上方屏外当前视窗下方屏外#26:左列,y = -228 · 位置不随回滑重选淡框 = 内容位置记录;不代表节点常驻

滑窗模式

比较列头,逐项向上补入
视窗内外的布局过程视窗框移动,纵坐标向下增大。左侧淡框是已有位置记录。右侧队列表示待补入的数据,各项的位置尚未确定。待布局数据 · 尚未决定二维位置#29待补#28待补#27待补#26待补#25待补#24待补左列右列#30#31#32#33#34#35上方屏外当前视窗下方屏外#24~29:尚无位置回滑时才向上补入补入前列头 y:左 0 / 右 0#29 → 右列 · 同高取右
移动的视窗 已记录的屏外位置 窗口保留的项目 本步补入
左侧把已经计算的内容坐标固定在画布上,移动视窗来揭示原有卡片。右侧显示窗口两端和待处理数据,向下选列尾较小的列,向上在没有历史列记录时选列头较大的列。有历史时优先恢复原列。屏外位置框不代表节点存活。纵坐标向下增大。

2.3 顺排模式:保留已计算的位置

ALWAYS_TOP_DOWN 是默认模式。组件从第一张卡片开始向下布局,并保留已计算的位置。数据、卡片高度和列数不变时,回滑仍能读到原来的位置。

首次远跳需要补算前面的布局。例如,使用 scrollToIndex 跳到第 1000 项,需要先计算前 999 项。计算量较大时,跳转会出现停顿。列数固定、以连续滑动为主的页面,可以使用顺排模式。

2.4 滑窗模式:只维护视窗附近的布局

滑窗模式只维护当前视窗附近的布局。向下滑动时在下方补入卡片,向上滑动时在上方补入。远跳时无需补算前面所有卡片;列数变化时,也只需重排视窗附近的内容。这种模式适合较长的信息流,以及经常远跳或调整列数的页面。

使用滑窗模式时,需要接受两项限制。总滚动距离是估算值,滚动条位置也随估算结果变化。远跳后向上回滑,若上方卡片没有历史列记录,组件会重新选列。此时的排列可能与从顶部连续滑下来的结果不同。

下面的示例演示远跳后的回滑过程。卡片按当前两列的顶端位置向上补入。滑到顶部并停止后,组件检查两列顶端是否对齐。如果顶端不齐,就重新布局,画面会出现一次跳变。

0 / 9
当前视窗

2.5 sections:分组设置布局

sections:不同分组使用不同列数。
sections:不同分组使用不同列数。

sections 描述各段的项数、列数和间距。例如,A 段单列、B 段两列、C 段三列。分组配置只提供布局规则,数据仍由 LazyForEach 提供。两种布局模式都支持分组。

启用 sections 后,各段的 crossCount 决定列数,columnsTemplate 不再生效。所有段的 itemsCount 之和必须等于数据源的 totalCount。数量不一致时的排查方法见 3.1 节。

2.6 cachedCount:设置预加载范围

cachedCount 设置视窗外的预加载范围。按当前实现,WaterFlow 的两种模式都支持前后两侧的节点缓存。List 和 Grid 也设置两侧缓存,但计数单位不同。

下表以 cachedCount(2)、竖向正向滚动和普通数据项为例。此时,前后两侧分别对应视窗的上方和下方。

组件计数单位cachedCount(2) 的含义
WaterFlow(两种模式)项(FlowItem)可见索引范围前后各 2 项,项数不随列数增加
List行上方 2 行、下方 2 行;单列各 2 项,两列且每行排满时各 4 项
Grid竖向按行,横向按列竖向上方 2 行、下方 2 行;三列且每行排满时各 6 项
cachedCount(2):WaterFlow 前后各缓存 2 项,竖向 List 和 Grid 上下各缓存 2 行。
cachedCount(2):WaterFlow 前后各缓存 2 项,竖向 List 和 Grid 上下各缓存 2 行。

表中列出的是目标缓存范围。到达页面开头或末尾时,组件按剩余数据量预加载。List 使用 ListItemGroup 时,按组内的行计数,空组算一行。Grid 包含跨行、跨列项时,每行的实际卡片数会变化。

布局记录与缓存节点需要分开看。布局记录保存卡片的尺寸和位置,缓存节点则是视窗附近已构建的卡片。

顺排模式可以保留远处的位置记录,同时释放对应的卡片节点。视窗上方缓存范围内的节点,则可以保留或重新创建。

使用 LazyForEach 或开启虚拟滚动的 Repeat 时,组件会按需提前构建缓存项,并测量需要布局的项。show 默认是 false。组件利用空闲时间预加载,暂不显示缓存节点,部分节点保存在主树之外。

排查缓存问题时,先确认布局模式和缓存范围,再查看节点状态。设置 cachedCount(2, true) 后,缓存项参与显示。配合裁剪设置,可以显示视窗外的内容。

预加载会消耗时间和内存。建议从默认值开始,先确认掉帧是否发生在卡片构建阶段。如果确实需要提前构建,再逐档增加 cachedCount。卡片内容复杂时,先减少单张卡片的构建和测量开销。

接口说明:WaterFlow cachedCount、List cachedCount 和 Grid cachedCount。

2.7 组合惰性布局

WaterFlow 支持下表中的四种惰性布局组件。子项滚动到视窗附近时,组件才构建和排列这些子项。页面由外层 WaterFlow 统一处理滚动。

组件排列方式使用场景
LazyVGridLayout规则网格分类入口、相册
LazyColumnLayout单列列表动态、消息、说明条目
LazyVWaterFlowLayout不等高瀑布流商品、图片卡片
LazyDynamicLayout应用自定义排列规则预设布局满足不了的特殊卡片编排

前三种预设布局可以放在同一个 WaterFlow 中,分别组织网格、列表和瀑布流。

一个 WaterFlow 承载多种惰性布局,代替 Scroll 里的嵌套滚动。
一个 WaterFlow 承载多种惰性布局,代替 Scroll 里的嵌套滚动。

在 Scroll 中嵌套 Grid、List 和 WaterFlow 时,如果内层容器各自滚动,就需要处理手势分配和滚动位置。改用惰性布局后,页面由一个容器滚动,可以减少这类嵌套配置。

展开全部内层内容时会一次构建所有子项。惰性布局只构建视窗附近的子项。
展开全部内层内容时会一次构建所有子项。惰性布局只构建视窗附近的子项。

如果为了让 Scroll 统一滚动而展开全部内层内容,页面会一次构建所有子项。惰性布局只构建视窗附近的卡片,其余数据滚到附近再构建。各组共用外层 WaterFlow 的 cachedCount 和节点回收机制。

sections 方案要合并数据源并维护数量,惰性布局每组一个数据源。
sections 方案要合并数据源并维护数量,惰性布局每组一个数据源。

使用 sections 组织多组内容时,需要把数据合并到一个 LazyForEach 数据源,再用 itemsCount 划分各段。任意一组增删数据,都要同步更新分组数量。数量不一致会导致 3.1 节中的布局中断。

惰性布局用组件树表示分组。每组使用自己的 LazyForEach 和数据源,可以分别加载、分页和刷新,无需维护 sections 的数量。

WaterFlow() {
  LazyVGridLayout() {
    LazyForEach(this.categorySource, (item: Category) => { CategoryCard({ item: item }) }, (item: Category) => item.id)
  }
  .columnsTemplate('1fr 1fr 1fr 1fr')
  .header(() => { Text('推荐分类').height(40) })

  LazyColumnLayout() {
    LazyForEach(this.feedSource, (item: Feed) => { FeedRow({ item: item }) }, (item: Feed) => item.id)
  }
  .header(() => { Text('今日动态').height(40) })

  LazyVWaterFlowLayout() {
    LazyForEach(this.goodsSource, (item: Goods) => { GoodsCard({ item: item }) }, (item: Goods) => item.id)
  }
  .header(() => { Text('猜你喜欢').height(40) })
  .footer(() => { Text('加载更多').height(36) })
}

上例中,“猜你喜欢”触底追加 100 条数据时,只更新 goodsSource。“推荐分类”刷新时,只更新 categorySource。

每组自带 header 和 footer,可以吸顶吸底。
每组自带 header 和 footer,可以吸顶吸底。

前三种预设布局支持 header 和 footer。它们参与各组的高度计算和滚动边界计算。配置 sticky 后,可以让组头吸顶或组尾吸底。例如,滚动到某一组时,将该组的 header 固定在视窗顶部。

需要自定义排列规则时,可以使用 LazyDynamicLayout(algorithm)。应用传入布局算法,决定子项的尺寸和位置。这适合预设网格、单列或瀑布流无法满足的页面,例如包含多种尺寸卡片的特殊编排。

自定义算法根据视窗范围处理附近的子项,避免一次展开全部数据。可见项变化时,onVisibleIndexesChange 返回当前可见的索引列表,应用可以据此记录曝光。

接入 WaterFlow 时,先将外层或所在 section 设为单列,再让自定义布局方向与外层滚动方向一致。应用需要实现测量和排布逻辑。排查问题时,应一并提供自定义算法和最小复现示例。

接口说明:LazyDynamicLayout。

3. WaterFlow 常见问题

排查 WaterFlow 卡顿时,先区分现象:滑不动、不流畅和跳一下。

WaterFlow 卡顿可分为滑不动、不流畅和跳一下。
WaterFlow 卡顿可分为滑不动、不流畅和跳一下。

3.1 滑不动:数据源与 sections 数量不一致

sections 与数据不一致时,本轮布局被跳过。
sections 与数据不一致时,本轮布局被跳过。

加载下一页后滑不动时,先检查 sections 与数据源的数量是否一致。sections 描述每段的项数,LazyForEach 提供数据。所有 section 的 itemsCount 之和必须等于数据源的 totalCount。

组件在每次布局前核对数量。数量不一致时,组件输出告警并跳过本轮布局,于是出现“滑不动”的现象。需要先修正数据,增大缓存或切换布局模式无法解决数量不一致的问题。

分页追加时分两步更新,就可能出现数量不一致。例如,先向数据源追加 100 条并通知 LazyForEach,再到另一个回调中更新 sections,或者漏掉这次更新。在两次更新之间,组件读到 300 条数据,而 sections 只有 200 项,校验就会失败。

先查看 hilog 日志,确认是否有数量不一致的告警:

W C03923/AceWaterFlow: Children count = 300 and doesn't match the number provided in Sections, which is 200.

日志中的前一个数字是 LazyForEach 当前提供的数据项数。后一个数字是 sections 中各段 itemsCount 之和。

再用 dump 核对组件当前的配置和状态。以下命令依次查找窗口、查找节点和查看节点详情:

# 1. 列出窗口,找到应用的 WinId
hdc shell "hidumper -s WindowManagerService -a '-a'"

# 2. 保存组件树,在其中找到 WaterFlow 节点的 ID
hdc shell "hidumper -s WindowManagerService -a '-w 12 -element'" > waterflow-tree.txt

# 3. 查看该节点的详细状态
hdc shell "hidumper -s WindowManagerService -a '-w 12 -element -lastpage 345'" > waterflow-node.txt

将命令中的 12 和 345 替换为实际的窗口 ID 和节点 ID。同一页面有多个 WaterFlow 时,先在组件树中确认目标节点。

排查时重点查看布局模式、各段 itemCount 之和,以及是否已滚动到底部。
排查时重点查看布局模式、各段 itemCount 之和,以及是否已滚动到底部。

排查时重点检查以下 dump 字段:

  • 布局模式:检索 WaterFlowLayoutMode 或 Mode:。确认输出是 TOP_DOWN 还是 SLIDING_WINDOW,再与预期配置核对。
  • 分组数量:检索 itemCount:,每段对应一行。将各段 itemCount 之和与数据源总量比较。注意,dump 字段名为 itemCount,接口配置名为 itemsCount。
  • 滚动边界:检索 offsetEnd: 和 itemEnd:。两者均为 true 时,表示已经滚动到末尾。此时无法继续向后滚动,属于正常触边。

sections 的输出示例如下:

[section:0]{ itemCount:4 },{ crossCount:1 },...
[section:1]{ itemCount:2 },{ crossCount:2 },...
[section:2]{ itemCount:194 },{ crossCount:2 },...

数据总量应从应用侧获取。懒加载时,屏幕外的数据不一定已生成节点,因此 dump 中的 FlowItem 节点数不能代表数据总量。

日志有数量告警,且 dump 中的数量与数据源不一致时,应检查应用的分组数量和更新顺序。

初始化时,按数据总量生成分组,确保各段 itemsCount 之和等于 totalCount:

// 分组配置:单列、双列交替,最后一段兜底
oneColumnSection: SectionOptions = { itemsCount: 4, crossCount: 1, columnsGap: 5, rowsGap: 10 };
twoColumnSection: SectionOptions = { itemsCount: 2, crossCount: 2 };
lastSection: SectionOptions = { itemsCount: 20, crossCount: 2 };

aboutToAppear() {
  let sectionOptions: SectionOptions[] = [];
  let count = 0;                                  // 已分配的 FlowItem 数量
  let oneOrTwo = 0;
  while (count < this.dataCount) {
    if (this.dataCount - count < 20) {            // 剩余不足 20 个,交给最后一段
      this.lastSection.itemsCount = this.dataCount - count;
      sectionOptions.push(this.lastSection);
      break;
    }
    if (oneOrTwo++ % 2 == 0) {
      sectionOptions.push(this.oneColumnSection);
      count += this.oneColumnSection.itemsCount;
    } else {
      sectionOptions.push(this.twoColumnSection);
      count += this.twoColumnSection.itemsCount;
    }
  }
  this.sections.splice(0, 0, sectionOptions);     // 各段 itemsCount 之和 == dataCount
}

触底加载时,在同一个回调中更新数据源和最后一段的 itemsCount:

.onScrollIndex((first: number, last: number) => {
  if (last + 20 >= this.dataSource.totalCount()) {
    for (let i = 0; i < 100; i++) {
      this.dataSource.addLastItem();              // 数据源 +100,内部发出 LazyForEach 通知
    }
    const sections: Array<SectionOptions> = this.sections.values();
    let newSection: SectionOptions = sections[this.sections.length() - 1];
    newSection.itemsCount += 100;                 // 最后一段 +100
    this.sections.update(-1, newSection);         // -1 表示最后一个分组
  }
})

更新后,各段 itemsCount 之和必须等于 totalCount。数据源和分组数量应在同一帧内完成修改。

3.2 不流畅:一帧内工作量过大

不流畅:一帧内的工作量超过预算。
不流畅:一帧内的工作量超过预算。

快速滑动不流畅,可能是一帧内的工作量超过预算。需要检查卡片创建和测量的耗时、图片解码对主线程的占用,以及滚动回调中的业务任务。仅凭组件的错误日志,未必能定位这些耗时。

卡片的优化方法可参考华为文档《长列表加载丢帧优化》。

横竖屏切换或折叠展开后,顺排模式从索引 0 布局到视窗布满,滑窗模式只布局视窗范围内的卡片。
横竖屏切换或折叠展开后,顺排模式从索引 0 布局到视窗布满,滑窗模式只布局视窗范围内的卡片。

顺排模式还有一种需要检查的情况:滚动到较远位置后,切换横竖屏或展开折叠屏。容器宽度变化会重置已有布局信息。组件需要从索引 0 重新布局,直到视窗内布满卡片,因此可能出现卡顿。

滑窗模式只布局视窗范围内的卡片,可用于处理这类重新布局开销。先查看 dump 中的布局模式,抓取方法见 3.1 节:

WaterFlowLayoutMode: TOP_DOWN

若 dump 显示 TOP_DOWN,且卡顿发生在远距离滚动后的横竖屏切换或折叠屏展开,应重点检查这次重新布局的开销。

3.3 回滑时跳动:屏幕外卡片高度变化

屏幕外卡片变高,回滑重测后后续卡片位置变化,部分卡片换列。
屏幕外卡片变高,回滑重测后后续卡片位置变化,部分卡片换列。

回滑时卡片“跳一下”,可能是屏幕外的卡片高度发生了变化。一张卡片变高后,后续卡片需要下移,也可能换列。

变高的卡片在屏幕外时,组件不会立即重排。回滑到附近并重新测量后,组件才读到新高度,再一次性调整后续卡片的位置,画面就会出现跳动。

先验证新内容是否已更新到组件。只修改普通对象的字段,却未发送 LazyForEach 变化通知,可能让复用节点继续显示旧内容。下面的示例通过更新变化项的 key 并发送通知,让组件重建该项。

再确认跳动是否由高度变化引起。补发通知只能保证内容及时更新;卡片高度改变后,组件仍需重排。要保持卡片高度稳定,可以预先设置图片宽高比,并限制文本行数。

可通过 hilog 日志验证高度是否变化。重新测量的高度与记录值不同时,组件会输出:

I C03923/AceWaterFlow: item size change. currentIdx:40,cacheHeight:160.000000,itemHeight:240.000000

currentIdx 是发生变化的卡片索引,cacheHeight 是此前记录的高度,itemHeight 是重新测得的高度。

以下写法只修改对象字段,组件无法获知变化,复用节点会继续显示旧内容:

this.items[index].height = newHeight; // 只改了普通对象

修改变化项的 key,并发送 onDataChange 通知,可让组件重建这一项。示例中的 key 由 id 和版本号生成。只递增变化项的版本号,其他项的 key 保持不变:

this.items[index].height = newHeight;
this.items[index].version++;   // key = id + ':' + version
this.listeners.forEach(l => l.onDataChange(index));

4. OpenHarmony 7.1 规划特性

本章整理 OpenHarmony 7.1 的规划特性,涉及 WaterFlow 和其他滚动容器。

内容整理自 7.1 需求库。部分需求仍在评审或实现中,接口名和参数以最终发布的 SDK(Software Development Kit,软件开发工具包)为准。

4.1 scrollToIndex 带动画快速跳转

带动画的 scrollToIndex:原有方式逐项创建到目标,7.1 计划支持估算终点并跳帧。
带动画的 scrollToIndex:原有方式逐项创建到目标,7.1 计划支持估算终点并跳帧。

enableFastScroll 用于带动画的远距离跳转,减少中间卡片的创建。原有 scrollToIndex 需要逐项创建并布局中间卡片,才能到达目标。7.1 计划在 ScrollToIndexOptions 中增加这一选项,默认值为 false。

开启后,组件根据已布局卡片的位置和平均高度估算目标位置,再启动动画。动画允许跳帧,无需逐项创建中间卡片。新卡片完成布局后,组件更新位置数据,并修正动画终点。未开启时,行为保持不变。

this.scroller.scrollToIndex(1000, true, ScrollAlign.START, { enableFastScroll: true })

4.2 滚动时创建占位组件

真实卡片创建超出帧预算时,先显示占位组件,再在后续帧中替换。
真实卡片创建超出帧预算时,先显示占位组件,再在后续帧中替换。

滚动占位组件用于分摊真实卡片的创建开销。快速滚动时,单张卡片可能需要几毫秒才能创建。同一帧创建多张卡片,就可能超过帧预算。

7.1 计划为 List、Grid 和 WaterFlow 引入滚动负载预测。组件根据当帧剩余预算和近期卡片创建耗时,决定同步创建真实卡片,还是先显示占位组件。耗时按组件类型和模板分别统计。真实卡片在后续帧中分批构建,完成后替换占位组件。

占位组件使用全局、无参的 @Builder,只支持静态渲染,不响应状态和手势。WaterFlow 的两种布局模式都在支持范围内,section 和 footer 不使用占位组件。

@Builder({ isScrollPlaceholder: true })
function CardPlaceholder() {
  Column().width('100%').height(100).backgroundColor('#eeeeee')   // 只做静态渲染
}

WaterFlow() {
  LazyForEach(this.dataSource, (item: number) => {
    FlowItem() { HeavyCard({ item: item }) }                        // 单张创建约 5 ms
  }, (item: number) => item.toString())
}
.scrollPlaceholder((index: number) => {
  return { id: 'CardPlaceholder', size: { width: 100, height: 100 } }  // 尺寸尽量接近真实卡片
})

先调用 UIContext 的 scrollPlaceholderRegister,注册占位 Builder,再按 id 引用。注册接口形态以最终发布的 SDK 为准。

4.3 滚动限位

点击任一对齐方式,自动模拟一次手势滚动和松手后的吸附。蓝色卡片是目标,橙线是对齐位置。

1. 滑动2. 松手3. 吸附对齐

选择上方按钮,观察卡片从松手位置移动到限位线。

scrollSnapStrategy 用于在滚动结束后将卡片吸附到限位线。原有 WaterFlow 没有卡片级吸附。应用若要在停止滚动时将卡片顶部对齐,需要监听滚动、计算位置,再发起第二次滚动,过程中容易抖动。

7.1 计划增加这一策略。滚动结束后,组件从可见卡片和缓存卡片生成候选锚点。低速滚动按阈值选择目标,高速滚动按惯性落点选择目标,再用可打断的弹簧动效对齐到限位线。

滚动限位提供以下配置:

  • 对齐位置:START、CENTER 和 END。
  • 限位线偏移 snapOffset:默认值为 0 vp,不支持百分比。
  • 吸附阈值 snapThreshold:取值范围为 0~1,默认值为 0.5。使用默认值时,滚动距离超过相邻候选间距的一半,就切换到下一个目标。

触摸、滚轮、滚动条和惯性滚动结束时,会触发吸附。scrollToIndex、键盘和焦点引起的滚动不触发二次吸附。该策略默认关闭,未配置时行为保持不变。

WaterFlow() { ... }
  .scrollSnapStrategy(ScrollSnapAlign.START)   // 停下时卡片顶部对齐视窗顶部;不传或 undefined 清除策略

4.4 内置 header、footer 与 sticky

内置 header、footer 和 sticky,支持吸顶和吸底。
内置 header、footer 和 sticky,支持吸顶和吸底。

7.1 计划为 WaterFlow 新增三个接口:header(builder)、footer(builder) 和 sticky(StickyStyle)。header 和 footer 参与总高度、滚动边界、跳转和事件索引计算,内容尺寸变化时同步更新。

sticky 提供 None、Header、Footer 和 BOTH 四个值,用于配置吸顶和吸底。已有 footer、footerContent 和 sections 的行为保持不变。与新接口同时设置时,旧接口的优先级也保持不变。

WaterFlow() { ... }
  .header(() => { Text('推荐').height(48) })
  .footer(() => { Text('加载更多').height(40) })
  .sticky(StickyStyle.BOTH)

4.5 惰性布局组件的两项增强

惰性布局组件与滚动容器之间可以嵌套其他容器。
惰性布局组件与滚动容器之间可以嵌套其他容器。

7.1 计划允许惰性布局组件与滚动容器之间嵌套其他容器。原有 LazyVGridLayout 等组件必须是滚动容器的直接子节点,中间增加一层 Column 就会崩溃。

新的实现从惰性布局组件向上查找最近的同轴滚动祖先。中间可以嵌套多层 Column、Stack 和 Flex。普通兄弟节点、padding 和 margin 仍参与布局,无需修改现有接口调用。惰性布局组件仍须位于滚动容器的子树中。

WaterFlow() {
  FlowItem() {
    Column() {                       // 以前这里的 Column 会导致崩溃
      Text('标题')
      LazyVGridLayout() { ... }
    }
  }
}
LazyVGridLayout 按容器断点自适应列数。
LazyVGridLayout 按容器断点自适应列数。

LazyVGridLayout 还计划支持按容器断点调整列数。columnsTemplate 增加 string 或 ItemFillPolicy 重载,与 Grid 保持一致。可配置为窄屏 1 列、中屏 2 列和宽屏 3 列,无需自行监听宽度并切换模板。固定模板的写法保持不变。

LazyVGridLayout() { ... }
  .columnsTemplate({ fillType: ResponsiveFillType.BREAKPOINT_SM1MD2LG3 })