Android滑动冲突详解(场景+解决)

滑动冲突

滑动冲突介绍

🧠 一、滑动冲突的本质

滑动冲突其实就是事件冲突。

由于 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的侵入性太强,还很麻饭,它的思路是

  1. 父容器(自定义 ScrollView)重写 onInterceptTouchEvent()
  2. 在其中找到子RecyclerView ,判断 RecyclerView 是否滑到顶部或底部。
  3. 如果已经滑到底部或顶部,才拦截事件让 ScrollView 滑动。

我们需要:

  1. 子控件(RecyclerView)在滑动中判断是否已经到底部或顶部;
  2. 如果还没到底部 → 拦截父 View ,继续由 RecyclerView 处理滑动;
  3. 如果到底部 → 允许父 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>
  1. 设置 RecyclerView 高度为 wrap_content,这会让 RecyclerView 随内容撑开,滚动交给外层控制

  2. 禁用 RecyclerView 的嵌套滑动功能

    recyclerView.isNestedScrollingEnabled = false

注意事项

问题 解决方法
RecyclerView 不显示所有 item RecyclerView.layout_height="wrap_content",并确保 adapter 数据加载完成后再渲染
滚动不流畅 / 卡顿 确保 RecyclerView item 高度稳定;不要嵌套过深;使用 setHasFixedSize(true) 优化
滑动冲突仍存在 可考虑使用 NestedScrollView + ConstraintLayout 组合避免多层嵌套
相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone1 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen1 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone1 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui