Android事件分发
Android的事件分发机制是一个非常重要的知识点,是一个核心,又是一个难点,是Android开发人员必须要了解的概念,学会他,我们就可以解决滑动冲突等问题,比如在View嵌套的时候,外部滑动与内部滑动的方向一致,该如何处理,这就需要了解事件分发机制才能解决,事件分发通常与View、ViewGroup和Activity相关联,形成了一个复杂的机制。
我们知道当一个或多个手指触摸屏幕时,通常有四种类型的事件:
- ACTION_DOWN:按下屏幕时发生,表示触摸事件开始,他是第一个发生的事件。
- ACTION_UP:手指抬起,表示触摸事件结束。
- ACTION_MOVE:手指按下屏幕,并且手指移动的距离超过了某个阈值
- ACTION_CANCEL:事件已取消(不是由用户的行为引起的)
这些信息都被包含在MotionEvent中,ACTION_DOWN和ACTION_UP只有1个,而ACTION_MOVE可能存在多个,所谓的事件分发就是对MotionEvent事件分发的过程,就是找到一个能处理这个MotionEvent的View,过程由Activity开始、传到ViewGroup、最终再传到 View,但是响应的过程是从下到上,从子到父,这点很好理解,当我们点击一个按钮时,这个按钮可能被包含在ViewGroup中,事件先会从Activity中开始分发进行,如果Activity要自己处理,那么这个按钮就得不到事件信息,如果Activity是进行分发,那么接下来包裹这个按钮的ViewGroup就会得到处理,同样如果这个ViewGroup要进行拦截,那么这个按钮也得不到响应,不拦截的话最终事件会被这个按钮消费。
事件分发由三个很重要的方法控制,他们被定义在Activity、View、ViewGroup中,他们是:
- Activity
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
- ViewGroup:
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
- View:
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
可以看到Activity中和View中都没有onInterceptTouchEvent,Android的事件分发就需要了解这三个方法,下面一一分析。
dispatchTouchEvent
他是事件分发处理函数,如果事件传递到了当前View,那么这个方法会被调用,返回结果表示是否消耗当前事件,false表示事件允许继续分发,返回true则表示该事件不在继续分发,有可能是当前View的onTouchEvent或者是子View的dispatchTouchEvent消费了。
不用我说,当发生点击操作时,会先从Activity的dispatchTouchEvent方法开始,然后依次传递给子视图,Activity的dispatchTouchEvent方法非常简单,首先判断是不是按下,如果是则调用一下onUserInteraction(虽然这个方法什么也没做),然后superDispatchTouchEvent方法经过层层调用,会传递到View或ViewGroup的dispatchTouchEvent中。
如果superDispatchTouchEvent返回true,则事件结束,表示有View已经消费了,false的话会传递给自身的onTouchEvent方法进行消费,表示所有的View的onTouchEvent的返回了false,没有人去处理这个事件,只能交给自己处理。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
对上面加粗地方解释一下:如果还想深入了解,就需要看DecorView,但过程也不是很多。
在这里想象一下,在Activity中有个ViewGoup,他的dispatchTouchEvent方法true,当单机这个ViewGoup的时候,那么这里就直接结束了,如果返回false,那么会走Activity的onTouchEvent方法。
如果返回super.dispatchTouchEvent(ev),那么这里就麻烦了,会走onInterceptTouchEvent方法,如果onInterceptTouchEvent方法返回true,那么就代表他要拦截当前事件,他的onTouchEvent就会被调用。如果返回false,那么表示不拦截当前事件,会继续传递给它的子元素,子元素的dispatchTouchEvent方法就会被调用,然后反复这个过程直到事件被最终处理。
另外如果给这个View设置了OnTouchListener,那么OnTouchListener.onTouch方法就会被调用,这个事件如何处理就要看onTouch的返回值,如果返回true则onTouchEvent方法不会被调用。在onTouchEvent中,如果设置了OnClickListener,那么他的onClick就会被调用。
onInterceptTouchEvent
这个方法是在dispatchTouchEvent中调用的,用来判断是否拦截当前事件,如果当前View拦截了事件,那么在后续同一个事件序列中,这个方法不会被再次调用,默认返回false,返回true表示拦截。
所以,在上一张图
onTouchEvent
也在dispatchTouchEvent中调用,用来处理点击事件,如果返回true,则表示当前View消耗了此事件。
常见解决方案
- ScrollView嵌套ScrollView
假设现在有两个ScrollView,每个ScrollView都需要上下滑动,如果不解决,那就是这个样子。
只需自定义一个ScrollView,在onInterceptTouchEvent下这样写,即可解决。requestDisallowInterceptTouchEvent用于请求父view不要拦截Touch事件,也就是让父View不要管onInterceptTouchEvent方法,直接执行向子View分发事件的逻辑。同样ListView嵌套ListView、ScrollView嵌套ListView也可以使用此办法,
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.onInterceptTouchEvent(ev);
}
- 收起软键盘
现在Activity中存有一个EditText和一个Button,现在要你单机按钮或者空白处的时候收起软键盘,你会怎么做?
首先明确的是,如果我们不做一些手段,点击EditText使软键盘弹出后在点任何其他方,软键盘是不会收回的,了解了事件分发后,就可以利用Activity中的dispatchTouchEvent处理,在其中判断如果事件是ACTION_DOWN时,获取当前具有焦点的View,然后隐藏软键盘即可。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction()==MotionEvent.ACTION_DOWN){
View v = getCurrentFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null && v!=null) {
imm.hideSoftInputFromWindow(v.getWindowToken(), 0);
}
}
return super.dispatchTouchEvent(ev);
}
– END –