简介

滑动冲突,其实就是界面中存在内外两层同时可以滑动,又互相影响的情况。我们在实际中也能常见到此类已情况,如微信中,下方有tab,而每个tab中又存在ListView,那就是既能左右滑动,又能上下滑动,而且互不影。你肯定会说,这有何难,用一个ViewPager便能轻松解决了。没错,ViewPager是可能解决此类已类情况,因为ViewPager内部已经处理了滑动冲突,我们直接用就是了。但是实际开发中,往往不只是像ViewPager这种上下左右简单的滑动冲动,或许像上下上下情况,也或许像左右左右情况,又或许存在因滑动速度或角度来决定,等。所以认识如何解决滑动冲突还是很有必要的。而且解决办法无不在乎两种固定的套路:外部拦截 和 内部拦截。

外部拦截

外部拦截法是指点击事件都先经过父容器,由父容器决定是否消化此事件,需要消化就拦截,否则不拦截传递给子容器去。还记得上篇文章《Android中View的事件分发机制》中介绍过三个重要方法以及它们的关系吗?回顾一下那段伪代码:

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);
    } else {
        consume = chile.dispatchTouchEvent(ev);
    }
    return consume;
}

所以,按照这个思路,重写父容器的onInterceptTouchEvent方法便好。代码如下:

private int mLastX;
private int mLastY;

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            // 返回false,不拦截,传给子View
            intercepted = false;
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int moveX = x - mLastX;
            int moveY = y - mLastY;
            if (XXXXX) {                    // 这里可根据实际情况,如 moveX 比 moveY 大,或角度等来执行条件判断
                // 拦截事件
                intercepted = true;
            } else {
                // 否则,不拦截,传给子View
                intercepted = false;
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            intercepted = false;
            break;
        }
        default:
            break;
    }
    mLastX = x;
    mLastY = y;
    return intercepted;
}

说明:

1、ACTION_DOWN事件,父容器必须返回false,即不拦截事件,因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这时事件就没法传递给子无素了(《Android中View的事件分发机制》中结论的第3点);
2、ACTION_MOVE事件,可以根据需要来决定是否拦截,也就是上面代码中的XXXXX,如果父容器需要拦截就返回true,否则返回false;

3、ACTION_UP事件,这里必须返回false,因为它本身没有太大意义和如果父容器在ACTION_UP时返回true,就会导致子元素无法接收到ACTION_UP事件,这时子元素中的onClick事件就无法触发。但是父容器比较特殊,一旦开始拦截任何一件事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一件事件也必定可以传递给父容器,所以便父容器在ACTION_UP中返回false。

内部拦截

内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素,由如子元素决定是否要消耗此事件,若不消耗就通过requestDisallowInterceptTouchEvent方法干预父容器进行处理,这种方法和Android中的事件分发机制不一致(《Android中View的事件分发机制》中结论的第11点)。 

重写子元素的dispatchTouchEvent方法:

private int mLastX;
private int mLastY;
private View mParentView;			// 父容器对象,记得要赋值

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    int x = (int) event.getX();
    int y = (int) event.getY();

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {

	// 告诉父容器,不允许拦截事件
            mParentView.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int moveX = x - mLastX;
            int moveY = y - mLastY; 
            if (XXXXX) {                    // 这里可根据实际情况,如 moveX 比 moveY 大,或角度等来执行条件判断

	// 告诉父容器,可以拦截事件
                mParentView.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }

    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(event);
}

还是要重写父元素的onInterceptTouchEvent方法:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    int action = event.getAction();
    if (action == MotionEvent.ACTION_DOWN) {

// ACTION_DOWN不拦截
        return false; 
    } else {

// 默认情况下,ACTION_MOVE和ACTION_UP都拦截
        return true;
    }
}

说明:

1、 在子元素中,使用parentView.requestDisallowInterceptTouchEvent指向父元素是否要拦截和在自己onTouchEvent里返回false的区别:虽然onTouchEvent在ACTION_MOVE中返回false也不消耗事件,但是父元素的onTouchEven并不会被调用 ,最终这些消失的点击事件传递给Activity处理(《Android中View的事件分发机制》中结论的第5点)。

2、 父元素要默认不拦截ACTION_DOWN事件,这样事件才能分发到子元素去,但默认要设置拦截 ACTION_MOVE 和 ACTION_UP 事件(未必生效,因为子元素在ACTION_DOWN中设置了mParentView.requestDisallowInterceptTouchEvent(true)),这样当子元素在ACTION_MOVE中调用mParentView.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。父容器不能拦截ACTOIN_DOWN事件,原因上面提过(《Android中View的事件分发机制》中结论的第3点)。

总结

以上两种方法解决的事情都是一样,没有太大的区别,也是公认的套路方法,大家在实现开发中可根据情况自由选择。不过一般地内部拦截没有外部拦截法简单易用,所以推荐采用外部拦截法来解决常见的滑动冲突。