📢 重磅福利!参与活动赢好礼,马克杯、鼠标垫、8月1日-12月31日等你来!
点击链接: 官方班级名称:HarmonyOS赋能资源丰富度建设(第四期)-GitCode

问题现象

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属性,同时将Tabs的滑动属性设置为true。
    
      
    1. Tabs({
    2. ForEach(this.exampleModel, (model: exampleModel, index: number) => {
    3. TabContent() {
    4. Grid() {}
    5. .nestedScroll({ // 设置Grid的嵌套滚动
    6. scrollForward: NestedScrollMode.SELF_FIRST,
    7. scrollBackward: NestedScrollMode.SELF_FIRST,
    8. })
    9. }
    10. })
    11. .scrollable(true) // 开启Tabs的滑动
  • 方案二:由于List、Scroll、Grid滚动组件存在onReachEndonReachStart等通用事件,可判断是否执行外层Tabs切换页签。使用Grid的事件来控制滑动的效果:用onScrollFrameBegin获取Grid的实际滑动量以及当前的滑动状态、onScrollStop设置Grid停止滑动后的行为、onReachStart设置Grid滑动到左侧时的行为、onReachEnd设置Grid滑动到右侧时的行为。结合以上说明,要在onScrollStop中处理Tabs切换:
    
      
    1. Grid(){
    2. ForEach(this.exampleList, (item: Resource, index: number) => {
    3. GridItem() {}
    4. })
    5. }
    6. .onReachEnd(() => {}) // 达到最右侧
    7. .onReachStart(() => {}) // 达到最左侧
    8. .onScrollFrameBegin((offset: number, state: ScrollState) => {}) // 获取实际滑动量以及当前滑动状态
    9. .onScrollStop(() => {}) // 通过判断Grid是否达到边界以及当前的滑动状态决定Grid停止滑动后是否切换Tab

    说明

    • 内部嵌套的List、Scroll、Grid等组件时,可以采用scrollToIndex、scrollTo方式,不建议采用scrollBy,没有动画效果。
    • 当采用scrollTo时需考虑偏移量的累积,如:this.listController.scrollTo({xOffset:this.xOffsets,yOffset:0,animation:true}),其中this.xOffsets+=(-event.offsetX)。
    • 该方案若是内部嵌套的List、Scroll、Grid等组件时,若每个Item不是和Swiper/Tabs类似占据整个屏幕宽度时,需要考虑判断的index是否准确,需要根据Item大小进行修正。
  • 方案三:通过自定义手势判断PanGesture实现Tabs嵌套内部组件滚动逻辑判断。以Tabs嵌套Swiper组件为例:
    1. 当Swiper显示位置为第一个卡片时,若继续往右滑,执行Tabs切换到上一个页签。
    2. 当Swiper显示位置为第最后个卡片时,若继续往左滑,执行Tabs切换到下一个页签。
    3. 其他时候左右滑动手势执行Swiper切换功能,左滑切换上一个卡片,右滑切换下一个卡片。
      
          
      1. // Swiper在第二个TabContent内
      2. Swiper(this.swiperController) {
      3. ForEach(this.data, (item: number, index: number) => {
      4. Text(item.toString())
      5. .width('100%')
      6. .height(160)
      7. .backgroundColor(0xAFEEEE)
      8. .textAlign(TextAlign.Center)
      9. .fontSize(30)
      10. .gesture(
      11. PanGesture()
      12. .onActionStart((event: GestureEvent) => {
      13. console.info('Pan start')
      14. })
      15. .onActionUpdate((event: GestureEvent) => {
      16. console.info('Pan update')
      17. })
      18. .onActionEnd((event: GestureEvent) => {
      19. // Swiper在Tabs第二页内采用if/else逻辑优先判定Swiper边缘滑动情况
      20. if (index === 0 && event.offsetX > 0) {
      21. this.controller.changeIndex(0) // Swiper滑动到第一页继续右滑,Tabs控制器跳转到第一页
      22. } else if (index === (this.data.length - 1) && event.offsetX < 0) {
      23. this.controller.changeIndex(2) // Swiper滑动到最后一页继续左滑,Tabs控制器跳转到第三页
      24. } else if (event.offsetX < 0) {
      25. this.swiperController.showNext() // Swiper控制器
      26. } else if (event.offsetX > 0) {
      27. this.swiperController.showPrevious() // Swiper控制器
      28. }
      29. })
      30. )
      31. }, (item: string) => item)
      32. }

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

    完整示例参考如下:

    
      
    1. @Entry
    2. @Component
    3. struct Index {
    4. tabsController: TabsController = new TabsController()
    5. build() {
    6. Column() {
    7. Tabs({ controller: this.tabsController }) {
    8. TabContent() {
    9. Text('首页的内容').fontSize(30)
    10. }
    11. .tabBar('首页')
    12. TabContent() {
    13. Tabs({ barPosition: BarPosition.Start }) {
    14. TabContent() {
    15. Text('tab0').fontSize('30fp')
    16. }.tabBar('tab0')
    17. .gesture(
    18. PanGesture({ fingers: 1, distance: 1, direction: PanDirection.Right })
    19. .onActionStart((event: GestureEvent) => {
    20. console.info('Pan start')
    21. })
    22. .onActionEnd((event: GestureEvent) => {
    23. this.tabsController.changeIndex(0)
    24. })
    25. )
    26. // 中间的其它TabContent
    27. TabContent() {
    28. Text('tab3').fontSize('30fp')
    29. }.tabBar('tab3')
    30. .gesture(
    31. PanGesture({ fingers: 1, distance: 1, direction: PanDirection.Left })
    32. .onActionStart((event: GestureEvent) => {
    33. console.info('Pan start')
    34. })
    35. .onActionEnd((event: GestureEvent) => {
    36. this.tabsController.changeIndex(2)
    37. })
    38. )
    39. }.backgroundColor(Color.Pink)
    40. }.tabBar('发现')
    41. TabContent() {
    42. Text('推荐的内容').fontSize(30)
    43. }.tabBar('推荐')
    44. }
    45. }
    46. .width('100%')
    47. .height('100%')
    48. }
    49. }
  • 方案四:使用onGestureRecognizerJudgeBegin()拦截内部组件滑动。

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

总结

方案

局限性

本知识适用场景

拓展适用场景

方案一

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

Tabs嵌套List、Scroll、Grid组件。

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

方案二

内部组件必须支持滚动与滑动组件的通用onReachEnd、onReachStart等通用事件。

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组件。

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

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

更多推荐