加入下方官方CSDN班级,得鸿蒙礼盒

一起拿鸿蒙礼盒,戳我戳我!!

本期活动时间:2025年8月1日-12月31日

如有问题欢迎私聊我呀!


问题现象

Tabs组件存在横向滑动的控制手势,当其内部嵌套Tabs或横向的List、Scroll、Swiper、Grid等滚动与滑动组件时,会产生横向滚动手势冲突,导致外部的Tabs无法横向切换。

以Grid为例,问题效果预览:

Tabs组件嵌套Grid组件,当Grid组件滑动到右侧底部时,无法触发Tabs组件的滑动。如下图所示,使用手指左滑页面中的2行icon,预期触发tab从“首页1”切换到“首页2”,实际未触发。

背景知识

  • Tabs组件是官方提供的导航与切换组件,可以通过TabsController手动控制其页面切换逻辑。
  • ListScrollGrid组件是官方提供的滑动与滚动组件,支持滚动组件通用接口。且都可以通过自身属性控制横向或纵向滚动,当为横向滚动时,与Tabs嵌套时会发生手势冲突。
  • Swiper组件是滑块视图容器,提供子组件滑动轮播显示的能力,也可以通过SwiperController手动控制其滑动逻辑。

解决方案

  • 方案一:由于List、Scroll、Grid滚动组件存在nestedScroll通用属性,可以设置滚动优先级。以横向滚动的Grid为例,在开启Tabs滑动切换的前提下给Grid设置嵌套滚动nestedScroll属性。
    
      
    @Entry
    @Component
    struct TabsGridDemo {
    tabsController: TabsController = new TabsController();
    gridData: number[] = Array(10).fill(0);
    
    build() {
    Column() {
    Tabs({ controller: this.tabsController }) {
    TabContent() {
    Grid() {
    ForEach(this.gridData, () => {
    GridItem() {
    Image($r('app.media.startIcon'))
    .height('40%');
    };
    });
    }
    .columnsGap(16)
    .rowsTemplate('1fr 1fr')
    // 设置Grid优先滚动
    .nestedScroll({
    scrollForward: NestedScrollMode.SELF_FIRST,
    scrollBackward: NestedScrollMode.SELF_FIRST,
    });
    }.tabBar('首页1');
    
    TabContent() {
    }.tabBar('首页2');
    };
    
    }.height('100%').width('100%');
    }
    }

  • 方案二:通过自定义手势判断PanGesture实现Tabs嵌套内部组件滚动逻辑判断。
    1. 以Tabs嵌套Swiper组件为例:
    2. 当Swiper显示位置为第一个卡片时,若继续往右滑,执行Tabs切换到上一个页签。
    3. 当Swiper显示位置为最后一个卡片时,若继续往左滑,执行Tabs切换到下一个页签。
    4. 其他时候左右滑动手势执行Swiper切换功能,左滑切换上一个卡片,右滑切换下一个卡片。
    @Entry
    @Component
    struct TabsSwiperDemo {
    tabsController: TabsController = new TabsController();
    swiperController: SwiperController = new SwiperController();
    data: number[] = [1, 2, 3];
    
    build() {
    Column() {
    Tabs({ controller: this.tabsController }) {
    TabContent() {
    Text('首页的内容').fontSize(30);
    }.tabBar('首页');
    
    TabContent() {
    // Swiper在第二个TabContent内。
    Swiper(this.swiperController) {
    ForEach(this.data, (item: number, index: number) => {
    Text(item.toString())
    .width('100%')
    .height('100%')
    .backgroundColor(0xAFEEEE)
    .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('发现');
    
    TabContent() {
    Text('推荐的内容').fontSize(30);
    }.tabBar('推荐');
    
    TabContent() {
    Text('我的内容').fontSize(30);
    }.tabBar('我的');
    };
    }.width('100%').height('100%');
    }
    }

    Tabs嵌套Tabs的滚动也可使用PanGesture实现,只需在内层Tabs的第一个和最后一个TabContent绑定手势处理即可。

    
      
    @Entry
    @Component
    struct TabsTabsDemo {
    tabsController: TabsController = new TabsController();
    
    build() {
    Column() {
    Tabs({ controller: this.tabsController }) {
    TabContent() {
    Text('首页的内容').fontSize(30);
    }.tabBar('首页');
    
    TabContent() {
    Tabs({ barPosition: BarPosition.Start }) {
    TabContent() {
    Text('tab1').fontSize('30fp');
    }.tabBar('tab1')
    .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('tab2')
    .gesture(
    PanGesture({ fingers: 1, distance: 1, direction: PanDirection.Left })
    .onActionStart(() => {
    console.info('Pan start');
    })
    .onActionEnd(() => {
    this.tabsController.changeIndex(2);
    })
    );
    }.backgroundColor(Color.Pink);
    }.tabBar('发现');
    
    TabContent() {
    Text('推荐的内容').fontSize(30);
    }.tabBar('推荐');
    
    TabContent() {
    Text('我的内容').fontSize(30);
    }.tabBar('我的');
    };
    }.width('100%').height('100%');
    }
    }

  • 方案三:使用onGestureRecognizerJudgeBegin()拦截内部组件滑动。

    可以使用手势拦截增强解决Tabs多层嵌套滑动冲突问题。使用示例请参考嵌套场景下拦截内部容器手势,通过onGestureRecognizerJudgeBegin监听手势事件,内层Tabs到头/尾时拒绝手势传递,允许外层Tabs响应。

总结

方案

局限性

本知识适用场景

拓展适用场景

方案一

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

Tabs嵌套List、Scroll、Grid组件。

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

方案二

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

Tabs嵌套Swiper、Tabs组件。

Swiper组件嵌套Swiper、Tabs组件。

方案三

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

Tabs嵌套Swiper、Tabs组件。

Swiper组件嵌套Swiper、Tabs组件。

综上所述,总结如下:

  1. Swiper组件的nestedScroll属性与Scroll、List、Grid的nestedScroll属性不一致。且Swiper不支持滚动组件通用属性。所以当Tabs嵌套Swiper时方案一并不适用。
  2. Tabs组件属于导航与切换组件,也不支持nestedScroll和滚动组件通用属性,所以当Tabs嵌套Tabs时方案一也不适用。
  3. 当Tabs内部嵌套List、Scroll、Grid组件时,优先选用方案一,内部嵌套Tabs或Swiper时,优先选用方案二,方案三。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐