一、滑动冲突的根源与核心场景
滑动冲突的根本原因,是父容器和子 View都想要处理同一个触摸事件。
1. 父子滑动方向不同
- 场景:
ViewPager(横向)嵌套RecyclerView(纵向)。 - 表现:当用户进行斜向滑动时,系统无法确定是应该由
ViewPager还是RecyclerView来处理事件。
2. 父子滑动方向相同
- 场景:
RecyclerView嵌套另一个RecyclerView(均垂直滑动)。 - 表现:父容器和子 View 争抢事件,导致滑动不流畅。
二、解决方案:外部拦截法与内部拦截法
解决事件冲突的本质,是建立一个清晰的规则,明确在什么条件下由谁来处理事件。
1. 外部拦截法(父容器主导)
-
核心思想:由父容器通过重写
onInterceptTouchEvent()方法,来决定是否拦截事件。 -
工作原理:
- 在
ACTION_DOWN时,父容器不拦截,让子 View 能够正常接收事件。 - 在
ACTION_MOVE时,父容器根据滑动方向、滑动距离等条件,判断是否应该由自己处理事件。 - 如果条件满足,
onInterceptTouchEvent()返回true,父容器拦截事件,并向子 View 发送一个ACTION_CANCEL事件,从而接管控制权。
- 在
-
代码示例:
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { // 在 ACTION_MOVE 中判断滑动方向,决定是否拦截 if (ev.action == MotionEvent.ACTION_MOVE) { val dx = abs(ev.x - startX) val dy = abs(ev.y - startY) if (dx > dy && dx > scaledTouchSlop) { // 横向滑动距离更大,且超过最小滑动阈值 return true // 拦截,自己处理 } } return false // 默认不拦截 }
2. 内部拦截法(子View协商)
-
核心思想:子 View 通过
requestDisallowInterceptTouchEvent()方法,来请求父容器不要拦截事件。 -
工作原理:
- 子 View 在
ACTION_DOWN时,调用parent.requestDisallowInterceptTouchEvent(true),告诉父容器“我现在要处理事件了,请不要拦截”。 - 当子 View 不再需要事件时(如滚动到顶部/底部),再次调用
parent.requestDisallowInterceptTouchEvent(false),将控制权交还给父容器。
- 子 View 在
-
代码示例:
override fun dispatchTouchEvent(ev: MotionEvent): Boolean { if (ev.action == MotionEvent.ACTION_DOWN) { parent.requestDisallowInterceptTouchEvent(true) // 请求父容器不要拦截 } // ... // 当滚动到底部时,将控制权交还给父容器 if (!canScrollVertically(1)) { parent.requestDisallowInterceptTouchEvent(false) } return super.dispatchTouchEvent(ev) }
三、系统级解决方案:NestedScrolling机制
NestedScrolling是Android 5.0引入的官方机制,用于实现父子View之间的协同滑动,解决了传统事件分发机制的局限性。
-
核心接口:
NestedScrollingParent3:父容器需要实现此接口。NestedScrollingChild3:子 View 需要实现此接口(如RecyclerView)。
-
工作原理:
- 预滑动:子 View 在滑动前,会通过
dispatchNestedPreScroll()方法将滑动距离传递给父容器。父容器可以在此阶段“预消费”一部分滑动。 - 子View消费:子 View 处理剩余的滑动距离。
- 后滑动:子 View 滑动完成后,会通过
dispatchNestedScroll()方法将未消费的滑动距离传递给父容器。
- 预滑动:子 View 在滑动前,会通过