鸿蒙5 如何解决Tabs组件嵌套同向可滚动组件后无法触发外层Tabs滑动的问题
方案局限性本知识适用场景拓展适用场景方案一内部组件必须支持滚动与滑动组件的通用nestedScroll接口。Tabs嵌套List、Scroll、Grid组件。List、Scroll、Grid组件嵌套List、Scroll、Grid组件。方案二内部组件必须支持滚动与滑动组件的通用onReachEnd、onReachStart等通用事件。Tabs嵌套List、Scroll、Grid组件。List、Sc
📢 重磅福利!参与活动赢好礼,马克杯、鼠标垫、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手动控制其页面切换逻辑。
- List、Scroll、Grid组件是官方提供的滑动与滚动组件,支持滚动通用属性的设置。且都可以通过自身属性控制横向或纵向滚动,当为横向滚动时,与Tabs嵌套时会发生手势冲突。
- Swiper组件是滑块视图容器,提供子组件滑动轮播显示的能力,也可以通过SwiperController手动控制其滑动逻辑。
解决方案
- 方案一:由于List、Scroll、Grid滚动组件存在nestedScroll通用属性,可以设置滚动优先级。以横向滚动的Grid为例,在开启Tabs滑动切换的前提下给Grid设置嵌套滚动nestedScroll属性,同时将Tabs的滑动属性设置为true。
- Tabs({
- ForEach(this.exampleModel, (model: exampleModel, index: number) => {
- TabContent() {
- Grid() {}
- .nestedScroll({ // 设置Grid的嵌套滚动
- scrollForward: NestedScrollMode.SELF_FIRST,
- scrollBackward: NestedScrollMode.SELF_FIRST,
- })
- }
- })
- .scrollable(true) // 开启Tabs的滑动
- 方案二:由于List、Scroll、Grid滚动组件存在onReachEnd、onReachStart等通用事件,可判断是否执行外层Tabs切换页签。使用Grid的事件来控制滑动的效果:用onScrollFrameBegin获取Grid的实际滑动量以及当前的滑动状态、onScrollStop设置Grid停止滑动后的行为、onReachStart设置Grid滑动到左侧时的行为、onReachEnd设置Grid滑动到右侧时的行为。结合以上说明,要在onScrollStop中处理Tabs切换:
- Grid(){
- ForEach(this.exampleList, (item: Resource, index: number) => {
- GridItem() {}
- })
- }
- .onReachEnd(() => {}) // 达到最右侧
- .onReachStart(() => {}) // 达到最左侧
- .onScrollFrameBegin((offset: number, state: ScrollState) => {}) // 获取实际滑动量以及当前滑动状态
- .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组件为例:
- 当Swiper显示位置为第一个卡片时,若继续往右滑,执行Tabs切换到上一个页签。
- 当Swiper显示位置为第最后个卡片时,若继续往左滑,执行Tabs切换到下一个页签。
- 其他时候左右滑动手势执行Swiper切换功能,左滑切换上一个卡片,右滑切换下一个卡片。
- // Swiper在第二个TabContent内
- Swiper(this.swiperController) {
- ForEach(this.data, (item: number, index: number) => {
- Text(item.toString())
- .width('100%')
- .height(160)
- .backgroundColor(0xAFEEEE)
- .textAlign(TextAlign.Center)
- .fontSize(30)
- .gesture(
- PanGesture()
- .onActionStart((event: GestureEvent) => {
- console.info('Pan start')
- })
- .onActionUpdate((event: GestureEvent) => {
- console.info('Pan update')
- })
- .onActionEnd((event: GestureEvent) => {
- // Swiper在Tabs第二页内采用if/else逻辑优先判定Swiper边缘滑动情况
- if (index === 0 && event.offsetX > 0) {
- this.controller.changeIndex(0) // Swiper滑动到第一页继续右滑,Tabs控制器跳转到第一页
- } else if (index === (this.data.length - 1) && event.offsetX < 0) {
- this.controller.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)
- }
Tabs嵌套Tabs的滚动也可使用PanGesture实现,只需在内层Tabs的第一个和最后一个TabContent绑定手势处理即可。
完整示例参考如下:
- @Entry
- @Component
- struct Index {
- tabsController: TabsController = new TabsController()
- build() {
- Column() {
- Tabs({ controller: this.tabsController }) {
- TabContent() {
- Text('首页的内容').fontSize(30)
- }
- .tabBar('首页')
- TabContent() {
- Tabs({ barPosition: BarPosition.Start }) {
- TabContent() {
- Text('tab0').fontSize('30fp')
- }.tabBar('tab0')
- .gesture(
- PanGesture({ fingers: 1, distance: 1, direction: PanDirection.Right })
- .onActionStart((event: GestureEvent) => {
- console.info('Pan start')
- })
- .onActionEnd((event: GestureEvent) => {
- this.tabsController.changeIndex(0)
- })
- )
- // 中间的其它TabContent
- TabContent() {
- Text('tab3').fontSize('30fp')
- }.tabBar('tab3')
- .gesture(
- PanGesture({ fingers: 1, distance: 1, direction: PanDirection.Left })
- .onActionStart((event: GestureEvent) => {
- console.info('Pan start')
- })
- .onActionEnd((event: GestureEvent) => {
- this.tabsController.changeIndex(2)
- })
- )
- }.backgroundColor(Color.Pink)
- }.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组件。 |
|
方案二 |
内部组件必须支持滚动与滑动组件的通用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时,优先选用方案三,方案四。
更多推荐



所有评论(0)