当前位置: 首页 > ds >正文

无侵入式的解决 ViewPager2 跟横向滑动子 View 手势冲突的一种思路

前言

在最近的开发中,我恰好遇到一个典型又棘手的问题 —— 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 内容较少、无法实际左右横向滚动时,滑动操作会没有反应,给人一种页面卡顿或者滑动失效的感觉。

我的预期

我希望手势响应遵循以下逻辑:

  1. 如果 子 view 是可横向滑动的 view,优先响应子 View 的滑动事件

  2. 如果子 View 无法左右滑动,再将事件交由 ViewPager2 处理,去响应 ViewPager2 的滑动事件,避免给用户一种假死的错觉


问题梳理以及解决思路

问题梳理

结合上文的现象与项目背景,当前我们面临的关键问题可以归纳为:
1. 滑动横向子 View 时,事件消费行为不确定,有可能是 ViewPager2 响应,有可能是子 view 响应。
2. 当子 View 本身无法横向滚动时,希望将滑动事件交由 ViewPager2 处理,避免“假死”状态。
3. RN 侧的代码不可修改,问题需在 Native 层解决,因此需要一种不修改其它代码的通用的方式来处理所有横向滚动 View 的冲突问题。

解决方案

这一方案其实经历了多次迭代优化,期间尝试过多种方式,但或多或少都存在边界问题。以下是最终实现,已在多种场景下验证,效果稳定,满足预期。

先来一个个的分析下上面问题的核心原因:

滑动横向子 View 时,事件消费行为不确定,有可能是 ViewPager2 响应,有可能是子 view 响应

原因:
当手指触摸到HorizontalScrollView的那一刻,根据手指按下的时长和滑动速度,会决定ViewPager2HorizontalScrollView 到底谁来响应手势。

如果手指按下停顿一下,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 获取焦点,并且通过调用 requestDisallowInterceptTouchEventViewPager2 不要拦截事件,确保子 View 能响应 ACTION_DOWN

示例代码:

    fun handleTouchEvent(viewPager2: ViewPager2) {this.viewPager = viewPager2recyclerView = viewPager2.getChildAt(0) as? RecyclerView ?: returnrecyclerView?.addOnItemTouchListener(object : RecyclerView.SimpleOnItemTouchListener() {override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean {when (e.action) {MotionEvent.ACTION_DOWN -> {runCatching {val targetView = findHorizontalScrollView(rv, e)if (targetView != null) {targetView.requestFocus()targetView.parent?.requestDisallowInterceptTouchEvent(true)}}}}return super.onInterceptTouchEvent(rv, e)}})}private fun findHorizontalScrollView(root: ViewGroup, event: MotionEvent): View? {val location = IntArray(2)root.getLocationOnScreen(location)val x = event.rawX.toInt() - location[0]val y = event.rawY.toInt() - location[1]return root.findViewTraversal { view ->if (view is HorizontalScrollView || (view is RecyclerView && view.layoutManager?.canScrollHorizontally() == true)) {val rect = Rect()//获取 view 的可见区域view.getGlobalVisibleRect(rect)rect.contains(x, y)} else {false}}}

这样一来,就能确保手指如果触摸的是可横向滚动的子 view 时,一定是子 View 消费事件。


当子 View 本身无法横向滚动时,出现不响应滑动手势的情况

原因
当子 view 消费手势事件时,如果子 view 本身内部内容不足时,是不会有滑动效果的,而且由于事件已经被消费掉了,所以 ViewPager2 也是不会响应手势的。此时就出现了假死的错觉。

解决
这个问题也比较好解决,当我们发现触摸的是可滚动 view时,给 view 添加setOnTouchListener,并监听事件,在 ACTION_MOVE中判断是否能左右滑动,如果无法左右滑动,就告诉父控件去拦截手势即可,测试就会走 ViewPager2 的滑动事件了。

示例代码:

@SuppressLint("ClickableViewAccessibility")private fun needChildScroll(view: View) {view.requestFocus()// 判断当前视图的滑动能力val canScrollLeft = view.canScrollHorizontally(-1)  // 能否向左滑动val canScrollRight = view.canScrollHorizontally(1)   // 能否向右滑动view.setOnTouchListener { v, event ->when (event.action) {MotionEvent.ACTION_MOVE -> {if (canScrollLeft || canScrollRight) {v.parent?.requestDisallowInterceptTouchEvent(true)} else {v.parent?.requestDisallowInterceptTouchEvent(false)}}MotionEvent.ACTION_UP -> {v.parent.requestDisallowInterceptTouchEvent(false)}}false}if (canScrollLeft || canScrollRight) {//告诉父控件不要拦截事件view.parent?.requestDisallowInterceptTouchEvent(true)} else {//告诉父控件可以拦截事件view.parent?.requestDisallowInterceptTouchEvent(false)}}

这样一来,就达到了不更改其它代码,只需要在初始化 ViewPager2 时调用一下handleTouchEvent即可统一的处理滑动冲突的问题了。

示例代码:

ViewPager2ConflictManager.handleTouchEvent(binding.viewPager)

好了,本篇文章就是这样,希望能帮到你。当然如果有更好的方式,还请指教。


感谢阅读,如果对你有帮助请三连(点赞、收藏、加关注)支持。有任何疑问或建议,欢迎在评论区留言讨论。如需转载,请注明出处:喻志强的博客

http://www.xdnf.cn/news/3265.html

相关文章:

  • 人工智能数学基础(五):概率论
  • Kafka Producer的acks参数对消息可靠性有何影响?
  • 阿里云服务器技术纵览:从底层架构到行业赋能​
  • PostgreSQL数据库操作基本命令
  • JAVA SE 反射,枚举与lambda表达式
  • 制作一款打飞机游戏36:调度编辑器
  • K8S - 命名空间实战 - 从资源隔离到多环境管理
  • 系统升级姿势解锁:绞杀、并行与隐藏开关
  • 拥抱 Kotlin Flow
  • 虚幻商城 Quixel 免费资产自动化入库(2025年版)
  • ArcGIS Pro几个小知识点分享
  • WebRtc09:网络基础P2P/STUN/TURN/ICE
  • 「动态规划::背包」01背包 / AcWing 2(C++)
  • OpenCV 图形API(75)图像与通道拼接函数-----将 4 个单通道图像矩阵 (GMat) 合并为一个 4 通道的多通道图像矩阵函数merge4()
  • 章越科技赋能消防训练体征监测与安全保障,从传统模式到智能跃迁的实践探索
  • Hbuilder 开发鸿蒙应用,打包成 hap 格式(并没有上架应用商店,只安装调试用)
  • 【Vue2】4-开发者工具安装
  • HOW - 经典详情页表单内容数据填充(基于 Antd 组件库)
  • 数据库服务器备份,数据库服备份到另一台服务器的方法有哪些?
  • 普通IT的股票交易成长史--20250430晚
  • python爬虫基础:requests库详解与案例
  • ESP32开发-作为TCP客户端发送数据到网络调试助手
  • 记录idea可以运行但是maven install打包却找不到问题
  • 【网络原理】从零开始深入理解HTTP的报文格式(二)
  • 第四节:权限管理
  • Vue3 Echarts 3D圆柱体柱状图实现教程以及封装一个可复用的组件
  • 仿腾信会议——密码MD5
  • 软件设计师-软考知识复习(1)
  • Deepseek应用技巧-批量生成读书金句
  • SAP 归档 自定义字段目录及归档信息结构