导言
Android中的滑动冲突很常见,例如ScrollView/ListView,ViewPager/ViewPager,相信各位或多或少都了解Android事件分发机制,以及滑动冲突产生的原理。网上相关的文章也很多,并且都讲解的很详细。但那毕竟是别人的成果,我觉得有必要通过一篇文章来记录自己的理解。
大纲
我将从下面几个方面来理解事件分发和解决滑动冲突:
- 理解四个方法
- Android事件分发机制
- 解决滑动冲的思路
- 一个滑动冲突场景
- 总结
- 参考文章
1.理解四个方法
讲到Android事件分发机制和解决滑动冲突,就离不开这四个方法:
- dispatchTouchEvent(MotionEvent ev)
- onInterceptTouchEvent(MotionEvent ev)
- onTouchEvent(MotionEvent ev)
- requestDisallowInterceptTouchEvent(boolean disallowIntercept)
大概介绍一下前三个方法的关系:
/**
* 伪代码
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onInterceptTouchEvent(ev)) {
return onTouchEvent(ev);
}
return child.dispatchTouchEvent(ev);;
}
dispatchTouchEvent()
正如其方法名,该方法是用来传递事件的,传递的顺序是Activity -> ViewGroup -> View 只要事件传递到当前view,就一定会调用该方法,返回结果表示是否消费该事件:
- true : 消费该事件,不再继续传递
- false : 事件不再向下传递,并且把事件交给parent处理
- super : 事件继续 向下传递
onInterceptTouchEvent()
是否拦截事件(不再向下传递),该方法只存在于ViewGroup,一旦拦截,那么当前整个事件序列不会再调用该方法,后续事件都交给当前ViewGroup处理。返回结果表示是否拦截:
- true : 拦截,事件不再向下传递
- false/super : 不拦截,事件继续传递
onTouchEvent()
该方法用来处理事件,处理的顺序是View -> ViewGroup -> Activity ,返回结果表示是否处理事件:
- true : 处理事件,不再向下传递
- false : 不处理事件,同一个事件序列里面,该View无法收到后续事件
- super : 交给上层View处理
requestDisallowInterceptTouchEvent()
该方法是在子view中请求父view不要拦截事件,参数disallowIntercept的值表示:
- true : 请求所有父view不要拦截事件,即当前事件序列不走父view的onInterceptTouchEvent()方法,直接向下传递
- false : 请求所有父View拦截事件,即子view不需要处理该事件,直接交给父view处理
该方法的作用下面还会详细介绍。
2.Android事件分发机制
在介绍事件分发机制之前,先介绍一下事件序列(上文有提到过):
?
事件序列.png
注:一般情况下,事件列都是以DOWN事件开始,UP事件结束,中间有很多MOVE事件
接下来,用一张图看明白事件传递的过程中的方法调用:
?
事件分发图.png
假如事件传递不中断的话,方法调用的整个流程如下图:
?
事件方法调用顺序.png
如果仔细看上面两张图,大家基本就能明白事件的传递流程了,下面我用文字描述一下整个流程:
- 事件从Activity的dispatchTouchEvent开始传递,传递给ViewGroup
- 如果ViewGroup的onInterceptTouchEvent不拦截事件,则继续向下面(ViewGroup或者View)传递,如果拦截了,则事件直接交给ViewGroup的onTouchEvent处理
- 当事件传递到了View,View就会调用onTouchEvent处理事件,正常情况下,还会把事件交给ViewGroup的onTouchEvent处理
- ViewGroup处理事件之后,正常情况下,又会交给Activity的onTouchEvent处理。
这里再把几个需要注意的点提一下:
- 如果ViewGroup的onInterceptTouchEvent方法拦截了事件,事件序列的后续事件不会再调用次方法,也不会向下传递,都直接交给该ViewGroup处理
- 如果View没有对ACTION_DOWN事件进行消费,事件序列的后续事件都不会传递过来了
3.滑动冲突解决方案
面对滑动冲突,我们可以有2种解决思路:
- 外部拦截法:是指我们可以重写parent的onInterceptTouchEvent方法,判断当前的事件是否需要拦截,伪代码如下:
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean isIntercept = false;
switch(event.getAction) {
case MotionEvent.ACTION_DOWN:
isIntercept = false;
//todo 记录点击初始位置
break;
case MotionEvent.ACTION_MOVE:
if (子控件不需要处理滑动事件) {
isIntercept = true;
} else {
isIntercept = false;
}
break;
case MotionEvent.ACTION_UP:
isIntercept = false;
break;
}
super.onInterceptTouchEvent(event);
return isIntercept;
}
在这里,down事件必须返回false,不然事件无法传递到子view,后续事件序列都会交给parent处理。而up事件也需要返回false,因为up事件对parent来说没有什么意义,其次若子view处理事件,却没有收到up事件会让子view的onClick事件无法触发。
- 内部拦截法:是指我们可以重写child的dispatchTouchEvent方法,判断是否需要让parent拦截事件,伪代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch(event.getAction) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptToucehEvent(true);
//todo 记录点击初始位置
break;
case MotionEvent.ACTION_MOVE:
if (子控件需要处理滑动事件) {
getParent().requestDisallowInterceptToucehEvent(true);
} else {
getParent().requestDisallowInterceptToucehEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onDispatchTouchEvent(event);
}
如果parent.requestDisallowInterceptTouchEvent(true)传入参数为true ,则parent就不会执行onInterceptTouchEvent方法,直接把事件交给子view处理。 requestDisallowInterceptTouchEvent()方法的逻辑:parent的dispatchTouchEvent()每次down事件,都会把它置为false,即拦截事件,走parent的onInterceptTouchEvent()方法,而在onInterceptTouchEvent()方法中,有一个属性mIsBeingDragged,当dy(滚动距离)>mTouchSlop的时候置为true,down事件和dy<mTouchSlop时为false,最后onInterceptTouchEvent()方法返回mIsBeingDragged,说明即使parent拦截了事件,但滚动距离比较小的时候,事件仍可以传递给子view,子view可以在onTouchEvent方法中调用parent.requestDisallowInterceptTouchEvent()方法
一个滑动冲突的场景
这里举一个很简单的例子 场景:ViewPager嵌套ViewPager的滑动冲突 解决思路:内部拦截法,当子ViewPager的position处于0且dx>0,或者子ViewPager的position处于adapter.count-1且dx<0时,把事件交给父ViewPager,其他时候都是子ViewPager处理即可 代码实现:
package study.self.zf.scrollconflict.widget;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class ChildViewPager extends ViewPager {
private int mStartX;
private int mStartY;
public ChildViewPager(Context context) {
super(context);
}
public ChildViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mStartX = (int) ev.getX();
mStartY = (int) ev.getY();
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int dx = (int) getX() - mStartX;
int dy = (int) getY() - mStartY;
if (Math.abs(dx) > Math.abs(dy)) {
int position = getCurrentItem();
int allCount = getAdapter().getCount();
boolean isInterceptByParent = (position == 0 && dx > 0) || ((position == allCount -1) && dx < 0);
getParent().requestDisallowInterceptTouchEvent(!isInterceptByParent);
} else {
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
}
总结
从以上的讲解可以看出来,滑动冲突并不难,而且思路也很简单,无非就是从dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()、requestDisallowInterceptTouchEvent()方法入手,分析什么时候parent处理事件,什么时候子view处理事件即可。
|