滑动冲突
滑动冲突介绍
🧠 一、滑动冲突的本质
滑动冲突其实就是事件冲突。
由于 Android 的事件是自顶向下传递的(dispatchTouchEvent()),当一个手势动作(如手指上下滑动)可以被父 View 和子 View 同时处理时,系统无法自动决定谁应该响应。
🧩 二、常见滑动冲突场景
1. 垂直方向冲突(最常见)
| 父控件 | 子控件 | 问题 |
|---|---|---|
| ScrollView | RecyclerView / ListView / ScrollView | 上滑或下滑时,父子都想滑动,产生冲突 |
2. 水平方向冲突
| 父控件 | 子控件 | 问题 |
|---|---|---|
| ViewPager | 横向滑动 RecyclerView / ImageView | 两者都想响应左右滑动,出现卡顿、误触或页面切换失败等问题 |
3. 混合方向冲突(嵌套滑动+手势识别)
比如:
- ViewPager + RecyclerView + 图片缩放控件(支持缩放和滑动)
- ScrollView + EditText(软键盘弹出也可能引起)
✅ 1. 外部拦截法
外部拦截法,指的是从外部容器入手,去决定是否要去拦截事件,若拦截掉,子View就没法消费了。
重写父 ViewGroup 的
onInterceptTouchEvent()方法,动态判断是否拦截事件。
具体做法:
ACTION_DOWN:不拦截ACTION_MOVE:根据滑动方向判断是否拦截
适合场景:
- 适合不同方向场景
- 不适用复杂场景
场景:不同方向(ViewPager + ListView)
乘客在屏幕上斜向滑动时,1 号线(ViewPager)想横向运客,2 号线(ListView)想纵向运客,结果系统调度混乱------乘客卡在换乘站动弹不得。
java
public class BossyViewPager extends ViewPager {
private float mLastX, mLastY;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = event.getX();
mLastY = event.getY();
intercept = false; // 必须放行 DOWN 事件!否则子 View 罢工[1,6](@ref)
break;
case MotionEvent.ACTION_MOVE:
float dx = Math.abs(event.getX() - mLastX);
float dy = Math.abs(event.getY() - mLastY);
// 横向滑动优先:父 View 截胡
if (dx > dy) {
intercept = true; // 宣布:"这个乘客归我了!"
}
break;
case MotionEvent.ACTION_UP:
intercept = false; // 放行 UP 事件,否则子 View 的点击事件失效[6](@ref)
break;
}
return intercept;
}
}
✅ 2. 内部拦截法
由子 View 决定是否允许父亲拦截,在子 View 中调用
requestDisallowInterceptTouchEvent(true),告诉父 View 不要拦截事件。
典型场景:
- 适合同方向场景
ViewPager嵌套RecyclerView、图片查看器- RecyclerView 想横滑但父亲是垂直滑的 View
场景:同方向:子滚完 → 父继续滚
crollView 嵌套 RecyclerView,当 RecyclerView 滑动到顶部或底部时 ,继续滑动才交给外层 ScrollView 滑动
✅ 实现思路:内部拦截法 + 边界判断
不推荐用外部拦截法,这种方式对子View的侵入性太强,还很麻饭,它的思路是
- 父容器(自定义 ScrollView)重写
onInterceptTouchEvent()。- 在其中找到子RecyclerView ,判断 RecyclerView 是否滑到顶部或底部。
- 如果已经滑到底部或顶部,才拦截事件让
ScrollView滑动。
我们需要:
- 子控件(
RecyclerView)在滑动中判断是否已经到底部或顶部; - 如果还没到底部 → 拦截父 View ,继续由
RecyclerView处理滑动; - 如果到底部 → 允许父 View 拦截 ,交给
ScrollView滑动。
关键
java
getParent().requestDisallowInterceptTouchEvent(true); // 请求父不要拦截,不让父滑
getParent().requestDisallowInterceptTouchEvent(false); // 请求父拦截,允许父滑
重写子RecyclerView的onTouchEvent方法
java
public class NestedRecyclerView extends RecyclerView {
@Override
public boolean onTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = e.getY();
// 默认不允许父拦截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float currentY = e.getY();
float dy = currentY - lastY;
boolean isScrollingDown = dy > 0; // 手指下滑,页面上滑
boolean isScrollingUp = dy < 0; // 手指上滑,页面下滑
// 判断是否滑到顶部或底部
if ((isScrollingDown && !canScrollVertically(-1)) || // 到顶部
(isScrollingUp && !canScrollVertically(1))) { // 到底部
// 到边界了,让父 View 接管事件
getParent().requestDisallowInterceptTouchEvent(false);
} else {
// 中间区域,由自己处理
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}
return super.onTouchEvent(e);
}
外部拦截 VS 内部拦截
| 特性 | 外部拦截法 | 内部拦截法 |
|---|---|---|
| 实现位置 | 父容器(如 ScrollView) 的 onInterceptTouchEvent() |
子控件(如 RecyclerView) 的 onTouchEvent() 中 |
| 是否依赖子控件主动配合 | ❌ 否 | ✅ 是 |
| 控制权 | 父控件主导事件是否下发 | 子控件主动决定是否让父控件拦截 |
| 推荐程度(实战) | ⚠️ 不推荐用于复杂滑动场景 | ✅ 实战中主流做法 |
| 是否适合 RecyclerView | ❌ 容易被 requestDisallowInterceptTouchEvent(true) 阻断 | ✅ 可精细控制 |
✅ 3. 使用NestedScrollView
使用 Android 提供的嵌套滑动机制来优雅解决冲突:
| 方法 | 说明 |
|---|---|
NestedScrollView |
替代普通 ScrollView,支持子 View 滑动协同 |
NestedScrollingChild / Parent |
实现接口支持协调滑动 |
CoordinatorLayout + Behavior |
更强大的嵌套滑动协调机制 |
场景描述:
NestedScrollView垂直方向滑动;- 内部嵌套一个
RecyclerView; - 当 RecyclerView 滚动到底部后,继续滑动手势 →
NestedScrollView开始滑动。
xml
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!--xxx 其他内容 -->
<!-- RecyclerView 嵌套在里面 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
-
设置 RecyclerView 高度为
wrap_content,这会让 RecyclerView 随内容撑开,滚动交给外层控制 -
禁用 RecyclerView 的嵌套滑动功能
recyclerView.isNestedScrollingEnabled = false
注意事项
| 问题 | 解决方法 |
|---|---|
| RecyclerView 不显示所有 item | RecyclerView.layout_height="wrap_content",并确保 adapter 数据加载完成后再渲染 |
| 滚动不流畅 / 卡顿 | 确保 RecyclerView item 高度稳定;不要嵌套过深;使用 setHasFixedSize(true) 优化 |
| 滑动冲突仍存在 | 可考虑使用 NestedScrollView + ConstraintLayout 组合避免多层嵌套 |