最近楼主在看任玉刚老师的《Android开发艺术探索》,看到了View的滑动冲突,感觉收获比较大。楼主之前也是做过View的滑动冲突,相对来说,对于View的事件分发机制比较熟悉,所以在View的滑动冲突这一块,做起来可能比较轻松的,但是实际上还是有很多的坑。
之所以写这个篇文章,是因为看到任玉刚老师介绍了比较系统的解决方法,对于之前的我,全靠自己的摸索,感触比较深刻。
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变量都会被重置。