滑动冲突之父与子的“轨道争夺战​”

10 阅读3分钟

滑动冲突就像两个地铁线路交叉时乘客的换乘难题:父 View(1 号线)想横向运送乘客(触摸事件),子 View(2 号线)想纵向运输,乘客一抬手就引发“轨道争夺战”。下面用故事 + 代码拆解解决方案:


​一、冲突现场:父与子的“轨道争夺战​

​场景 1:方向不同的线路(ViewPager + ListView)​
乘客在屏幕上斜向滑动时,1 号线(ViewPager)想横向运客,2 号线(ListView)想纵向运客,结果系统调度混乱——乘客卡在换乘站动弹不得。

​场景 2:同方向抢客(ScrollView + ListView)​
两条都是纵向线路,乘客在子 View(ListView)区域滑动时,父 View(ScrollView)和子 View 争抢乘客,导致乘客被拽得晕头转向。


​二、调度原理:Android 事件分发机制​

想象事件分发像地铁调度系统:

  1. ​父 ViewGroup(调度中心)​​:通过 onInterceptTouchEvent() 决定是否拦截乘客(返回 true 则截胡)

  2. ​子 View(列车)​​:通过 onTouchEvent() 决定是否运送乘客(返回 true 则接单)

  3. ​关键规则​​:一旦某趟列车接单 ACTION_DOWN,后续事件默认全交给他,除非调度中心强行截胡 。

⚠️ ​​冲突根源​​:子 View 抢到 ACTION_DOWN 后独占所有乘客,父 View 再想截胡也无力回天!


​三、解决方案 1:外部拦截法(调度中心强硬控场)​

父 View 化身霸道调度官,直接决定乘客去向:

java
Copy
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;
    }
}

​原理图解​​:

  1. 乘客 DOWN 事件 → 父 View 放行 → 子 View 接单

  2. 乘客 MOVE 事件 → 父 View 检测到横向滑动 → 拦截后续事件

  3. 后续事件直送父 View,子 View 收到 ACTION_CANCEL 后乖乖放手

    1

✅ ​​适用场景​​:方向不同的滑动冲突(如横向轮播图 + 纵向列表)


​四、解决方案 2:内部拦截法(子 View 主动让座)​

子 View 学会察言观色,主动通知父 View 接客:

java
Copy
public class PoliteListView extends ListView {
    private float mLastX, mLastY;

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 霸道宣言:“这个乘客我全包了!”(禁止父 View 截胡)
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                float dx = Math.abs(event.getX() - mLastX);
                float dy = Math.abs(event.getY() - mLastY);
                // 发现横向滑动:主动让座给父 View
                if (dx > dy) {
                    // 礼貌通知:“父 View,这个乘客给您吧!”
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
        }
        mLastX = event.getX();
        mLastY = event.getY();
        return super.dispatchTouchEvent(event);
    }
}

​父 View 配合修改​​(否则无法接单):

java
Copy
public class FatherScrollView extends ScrollView {
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // 仅当子 View 主动让座时才接单(DOWN 必须放行!)
        return event.getAction() != MotionEvent.ACTION_DOWN;
    }
}

✅ ​​适用场景​​:同方向滑动冲突(如外层刷新布局 + 内层列表)


​五、终极方案:NestedScrolling(协作式调度)​

Android 5.0 引入的“协作式调度系统”,父子 View 通过协议分阶段处理事件:

  1. ​子 View​​ 实现 NestedScrollingChild,滑动前先问父 View:“我要滑 10px,您要先滑吗?”

  2. ​父 View​​ 实现 NestedScrollingParent,回应:“我先滑 3px,剩下 7px 给你”

  3. 典型应用:SwipeRefreshLayout + RecyclerView 无缝协作

java
Copy
//View 滑动时询问父 View
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    if (父 View 想先滑动) {
        consumed[0] = dx; // 告诉子 View:“横向滑动我全包了!”
    }
}

​六、避免冲突的工程经验​

  1. ​优先使用官方已经处理好滑动冲突的控件​​:RecyclerView 自带嵌套滑动能力,比 ListView + ScrollView 更稳

  2. ​工具分析​​:用 Android Studio 的 ​​Layout Inspector​​ 检查视图层级,定位冲突点

🚀 ​​总结​​:

  • ​方向不同​​ → 外部拦截法(父 View 做主)
  • ​方向相同​​ → 内部拦截法(子 View 谦让)
  • ​复杂场景​​ → NestedScrolling 协作机制
    掌握事件分发机制,就像成为熟练的地铁调度员——让每个触摸事件都精准抵达目的地!