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

滑动冲突

滑动冲突介绍

🧠 一、滑动冲突的本质

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

由于 Android 的事件是自顶向下传递的(dispatchTouchEvent()),当一个手势动作(如手指上下滑动)可以被父 View 和子 View 同时处理时,系统无法自动决定谁应该响应。


🧩 二、常见滑动冲突场景

1. 垂直方向冲突(最常见)

父控件子控件问题
ScrollViewRecyclerView / 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)想纵向运客,结果系统调度混乱——乘客卡在换乘站动弹不得。

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 滑动。

关键

getParent().requestDisallowInterceptTouchEvent(true);  // 请求父不要拦截,不让父滑
getParent().requestDisallowInterceptTouchEvent(false); // 请求父拦截,允许父滑

重写子RecyclerView的onTouchEvent方法

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 开始滑动。
<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 不显示所有 itemRecyclerView.layout_height="wrap_content",并确保 adapter 数据加载完成后再渲染
滚动不流畅 / 卡顿确保 RecyclerView item 高度稳定;不要嵌套过深;使用 setHasFixedSize(true) 优化
滑动冲突仍存在可考虑使用 NestedScrollView + ConstraintLayout 组合避免多层嵌套
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值