大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

在 ArkUI 页面里,Tabs 本身支持横向滑动切换页签。如果某个 TabContent 里又放了一个横向 Grid,两个组件就会同时关心同一方向的滑动手势:手指在 Grid 区域左右滑时,到底应该滚 Grid,还是切 Tabs?

这类问题很容易被误判成 Grid “滑不动”、Tabs “手势失效”,或者布局尺寸没配对。实际上,华为官方 FAQ 已经把 Tabs 嵌套 Grid、List、Scroll、Swiper 的冲突单独整理成解决方案。对于本文这个 Tabs + 横向 Grid 场景,最合适的切入点不是自己监听触摸坐标,而是先使用滚动组件的 nestedScroll 建立明确的嵌套滚动关系。

一、先把问题缩小:我们到底要什么效果

本文只处理一个最小场景:

外层是可以左右切换的 Tabs,第一个 TabContent 中有一个横向滚动的 Grid。用户在 Grid 内滑动时,希望行为是:

  • Grid 还有内容可滚时,优先滚 Grid;
  • Grid 已经到达左侧或右侧边界,继续沿同方向滑动时,把滚动交给外层 Tabs;
  • 不为了这个场景额外实现一套触摸距离、速度和边界判断逻辑。

这和官方 FAQ 给出的 Tabs 嵌套横向 Grid 场景基本一致。官方明确指出:Tabs 存在横向滑动控制手势,内部再嵌套横向 List、Scroll、Swiper、Grid 等组件时,会出现横向滚动手势冲突;以 Grid 为例,典型表现就是 Grid 到达右侧边界后继续滑动,却没有触发外层 Tabs 切页。

【建议插图1:页面结构示意图,标出外层 Tabs 与第一个 TabContent 内部横向 Grid 的嵌套关系】

二、为什么 Grid 会和 Tabs “抢”横向手势

先看 Grid 为什么会横向滚。

ArkUI 的 Grid 是二维网格容器。官方文档说明,如果 rowsTemplate 和 columnsTemplate 只设置其中一个,超出显示区域的内容可以滚动;只设置 rowsTemplate 时,Grid 主轴为水平方向,因此形成横向可滚动 Grid;只设置 columnsTemplate 时则是纵向滚动。

例如:

Grid() {
  // GridItem...
}
.rowsTemplate('1fr')

这不是单纯把 Grid 排成“一行”,它同时决定了这里是一个可以沿水平方向滚动的 Grid。

问题也就出来了:外层 Tabs 要识别横向滑动,内层 Grid 也要处理横向滚动。当触摸发生在 Grid 区域时,如果没有建立嵌套滚动规则,不能仅凭“Grid 已经到边界了”就假定剩余手势一定会自然交给 Tabs。

官方 FAQ 给出的第一方案,就是通过 nestedScroll 明确这种父子滚动关系。

三、版本、接口和前置条件先核对

如果以 HarmonyOS 7 为开发背景,需要先把版本关系说清楚。

华为当前升级适配资料明确说明,HarmonyOS 7.0 对应 API 26.0.0,并建议开发者使用 26.0.0 开发套件进行升级适配。与此同时,从 26.0.0 开始,HarmonyOS 开发套件 API 版本采用 X.Y.Z 的语义化版本格式。

本文核心使用的是 ArkUI 滚动组件的 nestedScroll。官方 API 参考显示,该接口从 API version 10 开始支持,签名为:

nestedScroll(value: NestedScrollOptions)

默认模式是:

{
  scrollForward: NestedScrollMode.SELF_ONLY,
  scrollBackward: NestedScrollMode.SELF_ONLY
}

该接口用于设置前后两个方向的嵌套滚动模式,实现与父组件之间的滚动联动,并且文档标明其仅可在 Stage 模型下使用。HarmonyOS 7 / API 26.0.0 自然覆盖这一接口版本。

本文最小示例只涉及 ArkUI 的 Tabs、TabContent、Grid、GridItem 以及滚动通用能力,不需要为了嵌套滚动额外声明系统权限,也不涉及 module.json5 中的权限配置。

四、先搭一个最小可复现场景

下面不使用图片资源,直接用数字卡片构造横向 Grid,这样更容易把注意力放在滚动关系本身。

真正决定 Grid 横向滚动的是 .rowsTemplate('1fr');真正处理父子滚动关系的是 .nestedScroll(...)。

@Entry
@Component
struct TabsGridNestedScrollDemo {
  private data: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  build() {
    Column() {
      Tabs() {
        TabContent() {
          Column() {
            Text('横向 Grid')
              .fontSize(20)
              .margin({ bottom: 16 })

            Grid() {
              ForEach(this.data, (item: number) => {
                GridItem() {
                  Text(item.toString())
                    .width(120)
                    .height(120)
                    .textAlign(TextAlign.Center)
                    .fontSize(24)
                    .backgroundColor(0xFFEFF3F8)
                }
              }, (item: number) => item.toString())
            }
            .height(140)
            .columnsGap(12)
            .rowsTemplate('1fr')
            .nestedScroll({
              scrollForward: NestedScrollMode.SELF_FIRST,
              scrollBackward: NestedScrollMode.SELF_FIRST
            })
          }
          .width('100%')
          .height('100%')
          .padding(20)
        }
        .tabBar('Grid')

        TabContent() {
          Text('第二个 Tab')
            .width('100%')
            .height('100%')
            .textAlign(TextAlign.Center)
            .fontSize(24)
        }
        .tabBar('Second')
      }
      .vertical(false)
      .scrollable(true)
      .width('100%')
      .height('100%')
    }
    .width('100%')
    .height('100%')
  }
}

这段代码按照官方 Tabs + Grid FAQ 和 Grid 横向滚动规则整理成最小形式,没有加入业务数据、图片资源和控制器逻辑。官方 FAQ 给出的核心配置同样是在 Grid 上同时设置:

.nestedScroll({
  scrollForward: NestedScrollMode.SELF_FIRST,
  scrollBackward: NestedScrollMode.SELF_FIRST
})

并明确将其描述为“设置 Grid 优先滚动”。

【建议插图2:DevEco Studio 中运行最小示例,展示第一个 Tab 内横向排列的 Grid 卡片】

五、SELF_FIRST 真正解决了什么

这里最容易出现的理解偏差,是把 nestedScroll 当成“解决手势冲突的开关”。

它更准确的作用,是定义嵌套滚动中的处理顺序。

本文希望实现的是:

手势进入 Grid
      ↓
Grid 还能继续滚?
  ↓是        ↓否
Grid 消费    到达边界
              ↓
         交给父级处理
              ↓
         Tabs 切换页面

所以 Grid 应该是 SELF_FIRST,也就是内部滚动组件优先。官方在 Tabs + Grid FAQ 中正是对 scrollForward 和 scrollBackward 都使用 NestedScrollMode.SELF_FIRST。

为什么两个方向都配?

因为横向 Grid 有两个边界。只考虑“向后滑到末尾切下一个 Tab”,却忘记反方向,就可能导致从 Grid 起点向另一侧滑动时行为不一致。这里把 scrollForward、scrollBackward 都设为 SELF_FIRST,表达的就是两个方向统一采用“内层先处理”的策略。

【建议插图3:Grid 中间位置、Grid 右侧边界、切换到第二个 Tab 三个状态的连续截图】

六、为什么不直接上 onGestureRecognizerJudgeBegin

看到“手势冲突”,很容易马上想到:

.onGestureRecognizerJudgeBegin(...)

这个接口确实可以干预组件内置手势。官方当前文档中也存在通过判断内置 PAN_GESTURE 并返回 GestureJudgeResult.REJECT,让某个内部组件放弃当前手势的示例。

但 Tabs + Grid 并不是优先使用它的场景。

华为官方针对 Tabs 嵌套滚动组件的 FAQ 已经明确划分了适用范围:

内层组件官方建议优先思路
Grid / List / Scroll滚动组件通用 nestedScroll
Swiper自定义 PanGesture 或手势拦截方案
Tabs自定义 PanGesture 或手势拦截方案

而且 FAQ 对方案三,也就是 onGestureRecognizerJudgeBegin 的限制写得很明确:不支持内部为 Scroll、List、Grid 的这一解决路径,主要面向 Tabs 嵌套 Swiper、Tabs 等场景。

所以这里的排查原则很重要:

能用滚动组件自身的嵌套滚动协议表达,就不要先把问题升级成手势识别器拦截。

否则代码会很快变成:判断方向、判断边界、判断当前索引、识别内置 PanGesture,再决定 REJECT 还是 CONTINUE。对于一个 Grid 本身已经支持 nestedScroll 的场景,这些逻辑通常没有必要。

七、换成 List、Scroll、Swiper 怎么迁移

List / Scroll:思路基本可以直接迁移

官方 FAQ 将 List、Scroll、Grid 放在同一类中:它们支持滚动组件通用接口,可以通过 nestedScroll 设置嵌套滚动优先级。官方 API 参考中,List 的 nestedScroll 同样从 API version 10 开始提供,参数也是 NestedScrollOptions。

因此如果页面结构从:

Tabs
└─ Grid(横向)

改成:

Tabs
└─ List(横向)

排查方向不用推倒重来,仍然应该先确认滚动方向,再确认 nestedScroll。

Swiper:不要机械复制 Grid 的代码

Swiper 是这里最容易被混淆的一个。

Swiper 自身确实也存在 nestedScroll 能力,但它使用的是 SwiperNestedScrollMode,不是 Grid/List/Scroll 所使用的滚动组件通用 NestedScrollOptions。官方 FAQ 因此明确指出,Tabs 嵌套 Swiper 时不能直接套用本文的方案一。

Swiper 的官方 API 还特别说明:SwiperNestedScrollMode.SELF_FIRST 表示 Swiper 先滑动,到达边界以后再由父容器滚动;同时 Swiper 的甩动和翻页动画逻辑与普通滚动组件存在区别。

所以看到“都是 nestedScroll”时,不能只比接口名字。参数类型和组件自己的滚动模型同样要看。

对于 Tabs + Swiper,华为的专项 FAQ 优先给出了自定义 PanGesture 的边界处理方案,也提供了 onGestureRecognizerJudgeBegin 这一类手势拦截思路。

八、几个容易理解错的地方

1. Grid 横向滚动不是设置一个 horizontal(true)

在本文这种 Grid 布局中,只设置 rowsTemplate 就可以形成水平主轴;官方 Grid 指南明确说明,只设置 rowsTemplate 时 Grid 水平滚动,只设置 columnsTemplate 时 Grid 垂直滚动。

如果同时把 rowsTemplate 和 columnsTemplate 都固定下来,官方文档说明 Grid 会展示固定行列数量,其余元素不展示,并且不可滚动。排查“Grid 为什么不滑”时,这一点要比手势冲突更早检查。

2. SELF_FIRST 不是“永远只有 Grid 响应”

如果目标真是让 Grid 永远独占,就不存在到达边界后把操作继续交给 Tabs 的意义。

本文需要的是“Grid 有能力时 Grid 优先,到边界后父级继续处理”,所以重点是嵌套关系,而不是简单抢占手势。

3. Tabs 自己不是滚动组件通用 nestedScroll 的使用对象

官方 FAQ 明确指出,Tabs 属于导航与切换组件,不支持滚动组件通用 nestedScroll 属性。因此不要反过来尝试:

Tabs() {
  // ...
}
.nestedScroll(...)

来解决这个问题。正确的位置是支持滚动组件通用属性的内层 Grid、List 或 Scroll。

4. 不要把所有嵌套滑动问题都归成一种

Grid、List、Scroll 可以是一类;Swiper、Tabs 则需要另外判断。

这也是排查滚动冲突时最值得保留的习惯:先确认组件属于哪一种滚动模型,再选择处理手段。

九、实际项目中的排查顺序

碰到“里面能滑,外面不能滑”“滑到头以后 Tabs 还是不切页”这类问题,可以按下面的顺序检查:

  1. 确认系统与 API 版本。 如果以 HarmonyOS 7 为目标环境,当前对应 API 26.0.0;再检查使用的接口最低支持版本。本文核心 nestedScroll 从 API version 10 开始支持。
  2. 确认两个组件是不是同方向滚动。 Tabs 横向切页,内层 Grid 又是横向滚动,才构成本文讨论的直接冲突。
  3. 确认 Grid 本身真的处于可滚动布局。 横向 Grid 可以只设置 rowsTemplate;如果行列模板同时固定,先解决 Grid 本身不可滚动的问题。
  4. 确认内层组件是否支持滚动组件通用 nestedScroll。 Grid、List、Scroll 可以优先走这条路径;不要把 Swiper 当成完全相同的接口模型。
  5. 检查两个方向的配置。 对本文场景,scrollForward 和 scrollBackward 都设置为 NestedScrollMode.SELF_FIRST。
  6. 再考虑手势拦截。 如果内层已经变成 Swiper 或 Tabs,普通滚动组件的方案不再适用,再去看 PanGesture、onGestureRecognizerJudgeBegin 等手段。

【建议插图4:把上述排查过程画成流程图——滚动方向 → Grid 是否可滚 → 是否支持通用 nestedScroll → 设置 SELF_FIRST → Swiper/Tabs 转手势方案】

开发经验总结

Tabs 里面再套一个横向 Grid,真正需要解决的不是“怎么让某一个组件抢赢手势”,而是同方向嵌套滚动时,谁先消费、到边界以后谁接手。

对于 Grid/List/Scroll,官方已经提供了更直接的处理路径:通过 nestedScroll 定义父子滚动关系。本文场景使用:

.nestedScroll({
  scrollForward: NestedScrollMode.SELF_FIRST,
  scrollBackward: NestedScrollMode.SELF_FIRST
})

让 Grid 优先处理自己的横向滚动,到达边界后再与父级 Tabs 联动。

另一个需要记住的点是:不要看到 Swiper 也有 nestedScroll 就直接复制 Grid 代码。Swiper 使用自己的嵌套滚动模式,Tabs 又不属于支持滚动组件通用接口的组件。组件换了,解决方案也可能跟着换。

以后再碰到 ArkUI 嵌套滚动冲突,可以先问三个问题:方向是不是相同?内层是什么类型的滚动组件?它有没有官方提供的嵌套滚动协议? 这三个问题通常比一上来监听触摸事件更接近问题本身。

参考资料

  1. 《如何解决Tabs和滚动组件的嵌套滑动问题》
    华为开发者联盟
    链接:华为开发者联盟官方 FAQ:如何解决 Tabs 和滚动组件的嵌套滑动问题

  2. 《创建网格 (Grid/GridItem)》
    华为开发者联盟
    链接:华为 HarmonyOS 官方文档:创建网格 (Grid/GridItem)

  3. 《List》
    华为开发者联盟
    链接:华为 HarmonyOS API 参考:List(含 nestedScroll)

  4. 《Application Upgrade and Adaptation Guide — Upgrading to 26.0.0》
    HUAWEI Developers
    链接:HarmonyOS 7 / API 26.0.0 官方升级适配指南

  5. 《版本号格式调整说明》
    华为开发者联盟
    链接:HarmonyOS API 版本号格式调整说明

  6. 《Swiper》
    HUAWEI Developers
    链接:HarmonyOS 官方 API 参考:Swiper nestedScroll

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐