阅读本文前,需要对事件分发机制所有了解,建议先阅读 Android 事件分发机制源码流程分析,但是解决这个问题之前,首先我们要知道,滑动冲突是怎么来的,为什么会存在滑动冲突呢 ?

其实在 Android 中,只要是在界面中内外两层 View 可以同时滑动的时候,这个时候,就会产生滑动冲突了,或者可以理解在同一方向一个 View 上只能响应一组事件序列。当然了,部分情况下,会看到同时滑动的情况,可能是通过特殊的方式,将自己的事件序列传递给其他 View 上面了。

滑动冲突的场景

 常见的滑动冲突大致可以分为 3 种:

  • 场景一:外部滑动方向和内部的滑动方向不一致
  • 场景二:外部滑动方向和内部滑动方向一致
  • 场景三:场景一和场景二的嵌套

几种场景的的情况图如下:

  1. 场景一的情况一般是 ViewPager + Fragment 配合所组成的滑动效果,这种情况下,ViewPager 可以左右滑动,但是 Fragment 里面使用的可能是一个 ListView。本来这种场景下是会存在滑动冲突的情况的,但是 ViewPager 源码中自动的帮我们做了处理,所以不会有这个情况,如果将 ViewPager 换成是 Scrollview 那就必须手动处理滑动冲突了。否则造成的后果就是内外两层只有一层能够滑动这是因为两者的事件在分发的时候有歧义,所以会发生冲突,还有一个类似的场景,那就是外部的 View 是剩下滑动的,内部的 View 是左右滑动的,这种情况属于同一类型的冲突
  2. 场景二的情况稍微复杂一点,当内外两层都可以上下滑动的时候,显然是存在逻辑问题的,因为当手指滑动的时候,系统无法知道用户到底是想让那一层的 View 滑动。所以当手指滑动的时候,就会出现问题,要么只有一层能够滑动,要么内外两层滑动的很卡顿。在实际的开发中,这种主要的场景是指内外两层可以同时上下滑动,或者是内外两层可以同时左右滑动
  3. 场景三其实是场景一和场景二两种情况的嵌套。实际开发中遇到的情况就是外层有一个侧滑菜单, 内层就是 ViewPager + Fragment 中的 ListView 了。虽然场景三的滑动冲突看起来更加复杂一些,但是他是几个滑动冲突的叠加,只需要分别处理内层、中层和外层之间的滑动冲突即可,而具体的处理方式其实和场景一、场景二是相同的

滑动冲突的处理规则

  • 场景一: 对于场景一,他的处理规则是,当用户左右滑动时,需要让外部的 View 拦截事件,当用户上下滑动时,需要让内部的 View 拦截事件。具体来说,就是根据水平滑动还是竖直滑动来判断到底是谁来拦截事件
  • 场景二:对于场景二来说,比较特殊,它无法根据滑动的角度、距离差或者是速度差来做判断,这个时候一般情况下会从业务上入手,比如当处于某种状态时,需要外部的 View 响应用户滑动事件,而当处于另外一种状态时,则需要让内部的 View 响应滑动事件
  • 场景三:基于场景三的情况和场景二基本也是一样的,同样是从业务上入手

滑动冲突的解决方式

其实滑动冲突看起来很复杂,其实有套路解决方案的,在这里,我们就根据场景一的情况得出通用的解决方法,然后场景二和场景三也是只需要修改相关的滑动规则逻辑即可。

外部拦截法

所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件,就不拦截,这样就可以处理滑动冲突的问题。外部拦截法需要重写父容器的 onInterceptTouchEvent 方法,在内部做相应的拦截即可,这种方法的伪代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
    boolean intercepted = false;

    int x = (int) e.getX();
    int y = (int) e.getY();

    switch (e.getAction()) {
        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (父容器需要点击事件) {
                intercepted = true;
            } else {
                intercepted = false;
            } 
            break;
        case MotionEvent.ACTION_UP:
            intercepted = false;
            break;
    }
    
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
}
  1. 上述代码是外部拦截法的典型逻辑,针对不同的滑动冲突,只需要修改父容器需要当前点击事件这个条件即可,其他均不需做修改并且也不能修改。这 里对上述代码再描述一下,在 onInterceptTouchEvent 方法中,首先是 ACTION_DOWN 这个事件,父容器必须返回 false,即不拦截 ACTION_DOWN 事件,这是 因为一旦父容器拦截了 ACTION_DOWN,那么后续的 ACTION_MOVE 和ACTION_UP 事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了; 其次是 ACTION_MOVE 事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回 true,否则返回 false;最后是 ACTION_UP 事件,这里必须 要返回 false,因为 ACTION_UP 事件本身没有太多意义

  2. 考虑一种情况,假设事件交由子元素处理,如果父容器在 ACTION_UP 时返回了 true,就会导致子元素无法接收到 ACTION_UP 事件,这个时候子元素中 的 onClick 事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而 ACTION_UP 作为最后一个事件也 必定可以传递给父容器,即便父容器的 onInterceptTouchEvent 方法在 ACTION_UP 时返回了 false

内部拦截法

内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,这种方法和 Android 中的事件分发机制不一致,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作,使用起来较外部拦截法稍显复杂。它的伪代码如下,我们需要重写子元素的 dispatchTouchEvent 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            parent.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x -mLastX;
            int deltaY = y -mLastY;
            if (父容器需要此类点击事件)) {
                parent.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}
  1. 上述代码是内部拦截法的典型代码,当面对不同的滑动策略时只需要修改里面的条件即可,其他不需要做改动而且也不能有改动。除了子元素需要做处理以外,父元素也要默认拦截除了 ACTION_DOWN 以外的其他事件,这样当子元素调用 parent.requestDisal-lowInterceptTouchEvent(false) 方法时,父元素才能继续拦截所需的事件

  2. 为什么父容器不能拦截 ACTION_DOWN 事件呢?那是因为 ACTION_DOWN 事件并不受 FLAG_DISALLOW_INTERCEPT 这个标记位的控制,所以一旦父 容器拦截 ACTION_DOWN 事件,那么所有的事件都无法传递到子元素中去,这样内部拦截就无法起作用了。父元素所做的修改如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    
    public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
    }

总结

好了,滑动冲突的两种解决方案就在这里了,只需要按照这样的规则来即可,至于文中提到的 3 个冲突场景可以参考以下链接 Demo,其实文中提到的冲突解决方案,都是参考了 Android 开发艺术探索这本书而来。

Android View的事件体系