因为不懂Android事件分发机制,被女朋友嘲笑了

2,503 阅读5分钟

Android事件分发

Android的事件分发机制是一个非常重要的知识点,是一个核心,又是一个难点,是Android开发人员必须要了解的概念,学会他,我们就可以解决滑动冲突等问题,比如在View嵌套的时候,外部滑动与内部滑动的方向一致,该如何处理,这就需要了解事件分发机制才能解决,事件分发通常与View、ViewGroup和Activity相关联,形成了一个复杂的机制。

我们知道当一个或多个手指触摸屏幕时,通常有四种类型的事件:

  1. ACTION_DOWN:按下屏幕时发生,表示触摸事件开始,他是第一个发生的事件。
  2. ACTION_UP:手指抬起,表示触摸事件结束。
  3. ACTION_MOVE:手指按下屏幕,并且手指移动的距离超过了某个阈值
  4. 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中,他们是:

  1. Activity
public boolean dispatchTouchEvent(MotionEvent ev);

public boolean onTouchEvent(MotionEvent ev);
  1. ViewGroup:
public boolean dispatchTouchEvent(MotionEvent ev)public boolean onInterceptTouchEvent(MotionEvent ev);

public boolean onTouchEvent(MotionEvent ev);
  1. 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消耗了此事件。

常见解决方案

  1. 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);
}

  1. 收起软键盘

现在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);
 }