Android 扩展-View的滑动冲突(一)

  最近楼主在看任玉刚老师的《Android开发艺术探索》,看到了View的滑动冲突,感觉收获比较大。楼主之前也是做过View的滑动冲突,相对来说,对于View的事件分发机制比较熟悉,所以在View的滑动冲突这一块,做起来可能比较轻松的,但是实际上还是有很多的坑。
  之所以写这个篇文章,是因为看到任玉刚老师介绍了比较系统的解决方法,对于之前的我,全靠自己的摸索,感触比较深刻。
  View的滑动冲突有很多的情况,我们这里不可能一一的介绍,只是介绍几种比较典型的情况。

1.问题引出

Android 扩展-View的滑动冲突(一)_第1张图片

  这个滑动冲突的情况,主要是:外部的View可以左右滑动,但是内部的View可以上下滑动。典型的是ViewParger+Fragment配合使用,同时Fragment的内容还有一个ListView或者RecyclerView,ViewParger可以左右滑动,ListView可以上下滑动,这种情况就会出现滑动冲突的问题。虽然ViewParger内部帮助我们处理了这个问题,但是我们这里就当ViewParger没有帮助我们处理这个问题。

2.解决方法

(1).分析思路

  要想找到正确的解决方法,我们必须正确的理解这种情况。这种情况是两个不同方向的滑动的冲突,如果我们能正确的判断出用户手指滑动方向,这个问题就相当于解决了一半。那怎么判断手指滑动的方向呢?
  滑动方向的判断方式有很多种:比如,我们通过x方向与y方向上夹角的大小来判断,也可以通过x方向滑动的距离和y方向上滑动的距离等等。这里楼主用x方向滑动的距离和y方向上滑动的距离来判断的。
我们这样来假设情况,假如,x方向上滑动的距离比y方向上滑动的距离大,那么我们认为的是当前的用户在水平上方向滑动,反之,则是在竖直方向上滑动。
  问题来了,方向是判断出来了,怎么解决我们的问题呢?在事件分发机制中,我们知道有三个方向,分别是dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent。这里简单的介绍一下这三个方法的作用。

A.dispatchTouchEvent方法

  这个方法通常来对事件分发,当一个事件传递到一个ViewGroup中时,这个方法里面会判断是否分发事件到它的子View里面。如果在ACTION_DOWN事件时,当前的ViewGroup拦截事件,那么在这个方法里面,将不再分发同一个事件序列(从ACTION_DOWN到ACTION_UP表示一个事件序列)到子View,而是统一的分发到这个ViewGroup中去;而如果当前的ViewGroup没有拦截这个事件,那么这个事件将分发到当前ViewGroup的子View中去。

B.onInterceptTouchEvent方法

  这个方法通常用来判断当前的ViewGroup或者是View是否拦截当前的事件,如果拦截的话,那么就返回true,反之返回false就行了。

C.onTouchEvent方法

  这个方法就是正式的处理事件的方法,如果要想处理一个事件的话,必须返回true。因为返回false的话,会被认为当前的View不处理事件,于是会将事件传递给当前View的父View。
  我们知道了,怎么拦截事件,就可以开始我们下面的表演了。

(2).外部拦截法

  经过上面我们知道了,我们可以通过onInterceptTouchEvent方法来拦截事件。外部拦截法的思想是:当一个事件传递过来时,父ViewGroup先判断是这个事件是否需要拦截,如果需要拦截的话,那么我们就在这个方法里面返回true就行了;如果不需要拦截的话,那么就返回false,父ViewGroup会通过dispatchToucEvent方法将事件分发到它的子View中去。
  外部拦截法比较符合Android的事件分发机制。
  代码模板:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        //判断是否拦截事件
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                //父ViewGroup需要拦截当前的事件
                if (isIntercept(x, y)) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
        }
        mLastInterceptX = x;
        mLastInterceptY = y;
        return intercepted;
    }

  上面就是外部拦截法的基本模板,针对不同的需求,我们只需要修改isIntercepted方法就行了。但是在这里还是需要注意几点:

A.在ACTION_DOWN事件里面,我们只能返回false。因为如果返回true的话,那么表示当前的ViewGroup方法需要拦截这个事件序列,所以之后的ACTION_MOVE和ACTION_UP事件不会传递到当前的ViewGroup的子View里面去。
B.在ACTION_MOVE事件里面,我们根据我们具体的需求来判断是否拦截。
C.在ACTION_UP事件里面,我们最好不要拦截,因为这个事件本身就没有多大的意义。

  关于ACTION_UP的讨论,这里有一个特殊情况,假设父ViewGroup拦截了ACTION_UP事件,同时子View设置了OnClickListener事件,那么UP事件被父ViewGroup拦截,会导致子View的onClick无法响应。但是父ViewGroup比较特殊,一旦开始拦截任何一个事件时,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也肯定会传递给父ViewGroup,即便父ViewGroup的onInterceptTouchEvent方法在ACTION_UP事件里面返回了false。

(1).内部拦截法

  内部拦截法是指父ViewGroup不拦截任何的事件,全部事件都传递给子View来处理。如果子View需要处理此事件的话,就直接将此事件消耗掉就行了,否则就交给父ViewGroup来处理,这种方法和Android中的事件分发机制不一样,需要配合requestDisallowInterceptTouchEvent方法才能正常的工作,使用起来较外部拦截法比较复杂。
  代码模板:

public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                //请求父ViewGroup不要拦截DOWN事件
                //个人觉得这一步是多余的,第一,如果父ViewGroup拦截DOWN事件,那么当前的事件序列根本不能传递给这里来,更不要说进入switch语句调用这段代码
                //第二,前面已经说了,父ViewGroup不拦截任何事件,那么这里请求父ViewGroup不拦截事件,岂不是多余的吗?
                //最后说一句,个人感觉,不知道正确与否
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                //如果父ViewGroup要拦截此事件的话,那么请求父ViewGroup来拦截事件
                //拦截是从下一个事件(MOVE或UP)开始,当前这个MOVE事件不会拦截的
                if (isParentIntercept(x, y)) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
        }
        mLastInterceptX = x;
        mLastInterceptY = y;
        return super.dispatchTouchEvent(ev);
    }

  上面的代码模板就是内部拦截法的基本实现,当面对不同的需求时,只需要改变isParentIntercept方法就行了。这里需要注意的是:
  A.通过parent的requestDisallowInterceptTouchEvent方法来申请父ViewGroup拦截我们的事件,其实是从下一个事件开始拦截的,只要当前的事件还没有到结束。
  B.一旦事件被父ViewGroup拦截的话,那么在当前的事件序列中,之后的事件不会再传递到当前的View中来,而是直接在父ViewGroup中消耗掉。
  C.父ViewGroup不能拦截ACTION_DOWN事件,因为一旦ACTION_DOWN事件被父ViewGroup拦截的话,内部拦截法根本不会发挥效果。因为ACTION_DOWN事件被拦截的话,那么当前的事件序列中所有的事件都不会传递到子View中去,所以我们在子View中设置的规则根本没有效果;其次,ACTION_DOWN事件不受requestDisallowInterceptTouchEvent方法的影响,在每次ACTION_DOWN事件产生时,FLAG_DISALLOW_INTERCEPT变量都会被重置。

你可能感兴趣的:(Android 扩展-View的滑动冲突(一))