如何解决Tabs和滚动组件的嵌套滑动问题
====================

#### 问题现象

Tabs组件存在横向滑动的控制手势，当其内部嵌套Tabs或横向的List、Scroll、Swiper、Grid等滚动与滑动组件时，会产生横向滚动手势冲突，导致外部的Tabs无法横向切换。以Grid为例问题现象如下：

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/49/v3/zRj0PjIGR5Cn2z6XFc_UyA/zh-cn_image_0000002628770204.png?HW-CC-KV=V1&HW-CC-Date=20260709T133958Z&HW-CC-Expire=86400&HW-CC-Sign=ACE6A29F30F317887A0D952C2C42FAF3784C7CDBBBC2E2555EC615020026C08E "点击放大")

当Grid组件滑动到右侧底部时，预期触发Tabs从“首页1”切换到“首页2”，实际未触发。

#### 背景知识

*   [Tabs](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs)是通过页签进行内容视图切换的容器组件，每个页签对应一个内容视图。可以通过控制器[TabsController](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs#tabscontroller)控制Tabs组件进行页签切换，不支持一个TabsController控制多个Tabs组件。
*   [List](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-list)、[Scroll](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scroll)、[Grid](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-grid)是官方提供的滑动与滚动组件，支持[滚动组件通用接口](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scrollable-common)。且都可以通过自身属性控制横向或纵向滚动，当为横向滚动时，与Tabs嵌套时会发生手势冲突。
*   [Swiper](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-swiper)组件是滑块视图容器，提供子组件滑动轮播显示的能力，可以通过[SwiperController](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-swiper#swipercontroller)控制Swiper的切换。

#### 解决方案

*   **方案一**：基于滚动组件通用接口[nestedScroll](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scrollable-common#nestedscroll11)解决滚动冲突。
    
    List、Scroll、Grid滚动组件支持nestedScroll通用属性，可以设置滚动优先级。以Tabs嵌套横向滚动的Grid为例，在开启Tabs滑动切换的前提下给Grid设置嵌套滚动nestedScroll属性。
    
    @Entry
    @Component
    struct TabsGridDemo {
      tabsController: TabsController = new TabsController();
      gridData: Resource\[\] = \[\];
      @State selectIndex: number = 0;
    
    
      @Builder
      tabContent(nam: string, targetIndex: number) {
        Text(nam)
          .fontColor(this.selectIndex === targetIndex ? '#0a59f7' : '#99000000')
      }
    
    
      aboutToAppear(): void {
        for (let i = 1; i <= 10; i++) {
          this.gridData.push($r(\`app.media.${i}\`));
        }
      }
    
    
      build() {
        Column() {
          Tabs({ controller: this.tabsController }) {
            TabContent() {
              Column() {
                Grid() {
                  ForEach(this.gridData, (item: Resource) => {
                    GridItem() {
                      Image(item)
                        .height(150)
                        .width(150)
                    };
                  });
                }
                .height('25%')
                .columnsGap(16)
                .rowsTemplate('1fr')
               _ // 设置Grid优先滚动_
                .nestedScroll({
                  scrollForward: NestedScrollMode.SELF\_FIRST,
                  scrollBackward: NestedScrollMode.SELF\_FIRST,
                });
              }
              .width('100%')
              .height('100%')
              .padding({
                left: 20
              })
            }.tabBar(this.tabContent('首页1', 0));
    
    
            TabContent() {
            }.tabBar(this.tabContent('首页2', 1));
          }
          .onSelected((index: number) => {
            this.selectIndex = index;
          })
    
    
        }.height('100%').width('100%');
      }
    }
    
*   **方案二**：通过自定义手势判断[PanGesture](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-basic-gestures-pangesture)实现Tabs嵌套内部组件滚动逻辑判断。
    *   Tabs嵌套Swiper组件的实现逻辑如下：
        1.  当Swiper显示位置为第一个卡片时，若继续往右滑，执行Tabs切换到上一个页签。
        2.  当Swiper显示位置为最后一个卡片时，若继续往左滑，执行Tabs切换到下一个页签。
        3.  其他时候左右滑动手势执行Swiper切换功能，左滑切换上一个卡片，右滑切换下一个卡片。
            
            @Entry
            @Component
            struct TabsSwiperDemo {
              tabsController: TabsController = new TabsController();
              swiperController: SwiperController = new SwiperController();
              data: number\[\] = \[1, 2, 3\];
              @State selectIndex: number = 0;
            
            
              @Builder
              tabContent(nam: string, targetIndex: number) {
                Text(nam)
                  .fontColor(this.selectIndex === targetIndex ? '#0a59f7' : '#99000000')
              }
            
            
              build() {
                Column() {
                  Tabs({ controller: this.tabsController }) {
                    TabContent() {
                      Text('首页的内容').fontSize(30);
                    }.tabBar(this.tabContent('首页', 0));
            
            
                    TabContent() {
                    _  // Swiper在第二个TabContent内。_
                      Swiper(this.swiperController) {
                        ForEach(this.data, (item: number, index: number) => {
                          Text(item.toString())
                            .width('100%')
                            .height('100%')
                            .textAlign(TextAlign.Center)
                            .fontSize(30)
                            .gesture(
                              PanGesture()
                                .onActionStart(() => {
                                  console.info('Pan start');
                                })
                                .onActionUpdate(() => {
                                  console.info('Pan update');
                                })
                                .onActionEnd((event: GestureEvent) => {
                               _   // Swiper在Tabs第二页内采用if/else逻辑优先判定Swiper边缘滑动情况。_
                                  if (index === 0 && event.offsetX > 0) {
                                    this.tabsController.changeIndex(0); _// Swiper滑动到第一页继续右滑，Tabs控制器跳转到第一页。_
                                  } else if (index === (this.data.length - 1) && event.offsetX < 0) {
                                    this.tabsController.changeIndex(2); _// Swiper滑动到最后一页继续左滑，Tabs控制器跳转到第三页。_
                                  } else if (event.offsetX < 0) {
                                    this.swiperController.showNext(); _// Swiper控制器。_
                                  } else if (event.offsetX > 0) {
                                    this.swiperController.showPrevious(); _// Swiper控制器。_
                                  }
                                })
                            );
                        }, (item: string) => item);
                      };
                    }.tabBar(this.tabContent('发现', 1));
            
            
                    TabContent() {
                      Text('推荐的内容').fontSize(30);
                    }.tabBar(this.tabContent('推荐', 2));
            
            
                    TabContent() {
                      Text('我的内容').fontSize(30);
                    }.tabBar(this.tabContent('我的', 3));
                  }
                  .onSelected((index: number) => {
                    this.selectIndex = index;
                  })
                }.width('100%').height('100%');
              }
            }
            
    *   Tabs嵌套Tabs组件的实现逻辑和上述Swiper相似，只需在内层Tabs的第一个和最后一个TabContent绑定手势处理PanGesture即可。完整示例代码如下：
        
        @Entry
        @Component
        struct TabsTabsDemo {
          tabsController: TabsController = new TabsController();
          @State selectIndex0: number = 0; _// 外部页签_
          @State selectIndex1: number = 0; _// 内部页签_
        
          @Builder
          tabContent0(nam: string, targetIndex: number) { _// 外层Tab栏_
            Text(nam)
              .fontColor(this.selectIndex0 === targetIndex ? '#0a59f7' : '#99000000')
          }
        
        
          @Builder
          tabContent1(nam: string, targetIndex: number) { _// 内层Tab栏_
            Text(nam)
              .fontColor(this.selectIndex1 === targetIndex ? '#0a59f7' : '#99000000')
          }
        
        
          build() {
            Column() {
              Tabs({ controller: this.tabsController }) {
                TabContent() {
                  Text('首页的内容').fontSize(30);
                }.tabBar(this.tabContent0('首页', 0));
        
        
                TabContent() {
                  Tabs({ barPosition: BarPosition.Start }) {
                    TabContent() {
                      Text('tab1').fontSize('30fp');
                    }.tabBar(this.tabContent1('tab1', 0))
                    .gesture(
                      PanGesture({ fingers: 1, distance: 1, direction: PanDirection.Right })
                        .onActionStart(() => {
                          console.info('Pan start');
                        })
                        .onActionEnd(() => {
                          this.tabsController.changeIndex(0);
                        })
                    );
        
        
               _     // 中间的其它TabContent_
                    TabContent() {
                      Text('tab2').fontSize('30fp');
                    }.tabBar(this.tabContent1('tab2', 1))
                    .gesture(
                      PanGesture({ fingers: 1, distance: 1, direction: PanDirection.Left })
                        .onActionStart(() => {
                          console.info('Pan start');
                        })
                        .onActionEnd(() => {
                          this.tabsController.changeIndex(2);
                        })
                    );
                  }
                  .onSelected((index: number) => {
                    this.selectIndex1 = index;
                  })
                }.tabBar(this.tabContent0('发现', 1));
        
        
                TabContent() {
                  Text('推荐的内容').fontSize(30);
                }.tabBar(this.tabContent0('推荐', 2));
        
        
                TabContent() {
                  Text('我的内容').fontSize(30);
                }.tabBar(this.tabContent0('我的', 3));
              }
              .onSelected((index: number) => {
                this.selectIndex0 = index;
              })
            }.width('100%').height('100%');
          }
        }
        
*   **方案三**：使用[onGestureRecognizerJudgeBegin](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-gesture-blocking-enhancement#ongesturerecognizerjudgebegin13)拦截内部组件滑动。
    
    可以使用手势拦截增强解决Tabs多层嵌套滑动冲突问题。使用示例请参考[嵌套场景下拦截内部容器手势](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-gesture-blocking-enhancement#示例2嵌套场景下拦截内部容器手势)，通过onGestureRecognizerJudgeBegin监听手势事件，内层Tabs到头/尾时拒绝手势传递，允许外层Tabs响应。
    

#### 总结

| 
方案

 | 

局限性

 | 

本知识适用场景

 | 

拓展适用场景

 |
| :-- | :-- | :-- | :-- |
| 

方案一：基于滚动组件通用接口nestedScroll解决滚动冲突。

 | 

内部组件必须支持滚动与滑动组件的通用nestedScroll接口。

 | 

Tabs嵌套List、Scroll、Grid组件。

 | 

List、Scroll、Grid组件嵌套List、Scroll、Grid组件。

 |
| 

方案二：通过自定义手势判断PanGesture实现Tabs嵌套内部组件滚动逻辑判断。

 | 

内部为List、Scroll、Grid等组件时实现较麻烦。

 | 

Tabs嵌套Swiper、Tabs组件。

 | 

Swiper组件嵌套Swiper、Tabs组件。

 |
| 

方案三：使用onGestureRecognizerJudgeBegin拦截内部组件滑动。

 | 

不支持内部为Scroll、List、Grid组件。

 | 

Tabs嵌套Swiper、Tabs组件。

 | 

Swiper组件嵌套Swiper、Tabs组件。

 |

综上所述，总结如下：

1.  Swiper组件的[nestedScroll](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-swiper#nestedscroll11)属性与Scroll、List、Grid的nestedScroll属性不一致。且Swiper不支持滚动组件通用属性。所以当Tabs嵌套Swiper时方案一并不适用。
2.  Tabs组件属于导航与切换组件，也不支持nestedScroll和滚动组件通用属性，所以当Tabs嵌套Tabs时方案一也不适用。
3.  当Tabs内部嵌套List、Scroll、Grid组件时，优先选用方案一，内部嵌套Tabs或Swiper时，优先选用方案二，方案三。

#### 常见FAQ

Q：Tabs嵌套Web组件出现左右滑动问题如何解决？

A：请参考[Tabs组件嵌套Web，无法左右滑动](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-arkweb-152)中的解决方案。

Q：Tabs嵌套Scroll组件，如何实现每个TabContent页签中的Scroll容器可以垂直滑动，横向滑动时可以切换页签？

A：Tabs组件的[vertical](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs#vertical)属性设置为false，[scrollable](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-tabs#scrollable)属性设置为true，即可通过左右滑动页面内容切换页签。Scroll组件的[scrollable](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-container-scroll#scrollable)属性设置为ScrollDirection.Vertical，可以竖直方向滚动其中内容。