前言
在最近的开发中,我恰好遇到一个典型又棘手的问题 —— ViewPager2 与横向滚动 View(如 HorizontalScrollView 或 ReactHorizontalScrollView)之间的手势冲突
网上关于 ViewPager2 滑动冲突的文章一搜一大把,但不少内容已过时,且大同小异,真正符合实际需求的少之又少。
因此,这里我把自己的解决思路与实践过程做个记录,希望能为有类似困扰的大佬们提供点参考。
ViewPager2 滑动冲突
页面结构
先来看一下页面的基本结构:
- 最外层是一个 Activity,其中包含了 ViewPager2、底部的 Tab 和顶部的 TabLayout。
- ViewPager2 中承载着多个 Fragment,其中第一个是原生页面,其余则是加载的 RN(React Native)页面。
- 每个 Fragment 中都有可能存在横向可滑的组件,例如 HorizontalScrollView 或ReactHorizontalScrollView。
手势冲突的表现
在未做任何手势处理的情况下,主要问题表现如下:
- 正常情况:当手指在页面的非横向滑动区域左右滑动(无论是 Native 页还是 RN 页),事件会正确地传递给 ViewPager2,切换页面一切正常 —— 符合预期。
- Native 页问题:当滑动落点在
HorizontalScrollView
上时,手势响应行为就变得不可预期。可能会触发 ViewPager2 的滑动,也可能响应HorizontalScrollView
,具体取决于手指按压的时长和滑动速度,这种不确定性就不是我所预期的行为。 - RN 页问题:如果落点在
ReactHorizontalScrollView
上,则一定是响应自己的滑动事件,ViewPager2
的滑动彻底被拦截。在手势优先级上这个是符合我的预期的,但当ReactHorizontalScrollView
内容较少、无法实际左右横向滚动时,滑动操作会没有反应,给人一种页面卡顿或者滑动失效的感觉。
我的预期
我希望手势响应遵循以下逻辑:
-
如果 子 view 是可横向滑动的 view,优先响应子 View 的滑动事件
-
如果子 View 无法左右滑动,再将事件交由 ViewPager2 处理,去响应 ViewPager2 的滑动事件,避免给用户一种假死的错觉
问题梳理以及解决思路
问题梳理
结合上文的现象与项目背景,当前我们面临的关键问题可以归纳为:
1. 滑动横向子 View 时,事件消费行为不确定,有可能是 ViewPager2 响应,有可能是子 view 响应。
2. 当子 View 本身无法横向滚动时,希望将滑动事件交由 ViewPager2 处理,避免“假死”状态。
3. RN 侧的代码不可修改,问题需在 Native 层解决,因此需要一种不修改其它代码的通用的方式来处理所有横向滚动 View 的冲突问题。
解决方案
这一方案其实经历了多次迭代优化,期间尝试过多种方式,但或多或少都存在边界问题。以下是最终实现,已在多种场景下验证,效果稳定,满足预期。
先来一个个的分析下上面问题的核心原因:
滑动横向子 View 时,事件消费行为不确定,有可能是 ViewPager2 响应,有可能是子 view 响应
原因:
当手指触摸到HorizontalScrollView
的那一刻,根据手指按下的时长和滑动速度,会决定ViewPager2
和 HorizontalScrollView
到底谁来响应手势。
如果手指按下停顿一下,HorizontalScrollView
大概率能收到 ACTION_DOWN
事件,然后后续的 ACTION_MOVE
事件也是由HorizontalScrollView
来响应。
但是如果手指按下后立刻就滑动,此时,HorizontalScrollView
就大概率收不到ACTION_DOWN
事件了,被 ViewPager2
给拦截并消费掉了,此时,滑动事件也是由 ViewPager2
响应的。
这就导致了滑动时可能是宫格内部的滑动,也可能会触发 ViewPaiger2
的切换。
解决:
核心要解决的就是当手指触摸的是可横向滚动的子 view 时,要确保子 view 来消费事件,而不是ViewPager2
。
但是由于 ViewPager2
是相对上层的父 view,因此,事件一定是先到 ViewPager2
的,如果 ViewPager2
没有消费事件,子 view 才有机会响应事件。
查看源码发现 ViewPage2
本质上就是个 RecyclerView
,那么可以在初始化 ViewPager2
时,给 内部的 RecyclerView
加个addOnItemTouchListener
,在 ACTION_DOWN
的时候去找一下当前手指是否是触摸在可横向滚动的 View 上,如果是,则让 子 view 获取焦点,并且通过调用 requestDisallowInterceptTouchEvent
让 ViewPager2
不要拦截事件,确保子 View 能响应 ACTION_DOWN
示例代码:
fun handleTouchEvent(viewPager2: ViewPager2) {
this.viewPager = viewPager2
recyclerView = viewPager2.getChildAt(<