说在前面的话

事件分发是一个重点也是难点,所以,本篇幅有点长,如果耐心看完本篇,相信读者会有收获的。同时,读者也可以自己写例子测试,毕竟,纸上得来终觉浅,绝知此事要躬行。但是,对于水平高的读者,其实最好的方式是看源代码,因为一切原因都可以从源头找到答案

关于事件分发

关于事件分发,其实主要就是理解三个函数,这三个函数分别是dispatchTouchEvent(MotionEvent ev)onInterceptTouchEvent(MotionEvent ev)以及onTouchEvent(MotionEvent ev),这里直接上一张图:

伪代码-20161116

这张图将分发事件中的重要的三个函数之间的关系表达的比较清晰,这里再简单的解释一下上面的伪代码:一个点击事件产生后,会从外到内传递,传递到根ViewGroup后,会调用根ViewGroup的dispatchTouchEvent方法,然后如果自己的onInterceptTouchEvent方法返回true,表示拦截事件,那么这个事件就会自己处理,也就是调用自己的onTouchEvent方法,如果返回false,就表示自己不拦截这个事件,事件就会传递给子元素,也就是调用子元素的dispatchTouchEvent方法,如此传递下去,直到事件被处理。

知道了关系以后,我们还需要了解这三个函数,了解一个函数,其实无非就是理解它的作用,传入的参数以及返回值。
先说dispatchTouchEvent(MotionEvent ev):
这个函数的作用是分发事件,不管是ViewGroup还是View都有这个方法,它的返回值受本身的onInterceptTouchEvent(MotionEvent ev)child.dispatchTouchEvent(MotionEvent ev)共同影响(从上面那张图就可以看出来),返回的各个值的意义如下:
return true :表示该View内部消化掉了所有事件。

return false :事件在本层不再继续进行分发,这个false也就是本身的dispatchTouchEvent(MotionEvent ev)返回值,而这个返回值会回溯给上层控件的dispatchTouchEvent(MotionEvent ev),表示自己没有接受这个事件,不管上层控件是view还是viewGroup,都是交由上层控件的onTouchEvent(MotionEvent ev)方法进行消费(如果本层控件已经是Activity,那么事件将被系统消费或处理)。  

如果事件分发返回系统默认的 super.dispatchTouchEvent(ev),事件将分发给本层的事件拦截onInterceptTouchEvent(MotionEvent ev)方法进行处理,而不是super的onInterceptTouchEvent。因为return super.dispatchTouchEvent(ev)会去运行父viewGroup的dispatchTouchEvent(ev),然后运行onInterceptTouchEvent,那么这个onInterceptTouchEvent是谁的呢?根据方法是基于对象的,所以就会运行child的onInterceptTouchEvent(MotionEvent ev)也就是本层的事件拦截器,而不是super的onInterceptTouchEvent。详情可以参考规则中的第十条。

然后是onInterceptTouchEvent(MotionEvent ev):
这个函数的作用是拦截事件,只有ViewGroup有这个方法,返回的各个值的意义如下:
return true :表示将事件进行拦截,并将拦截到的事件交由本层控件 的 onTouchEvent 进行处理;
return false :则表示不对事件进行拦截,事件得以成功分发到子View。并由子View的dispatchTouchEvent进行处理。 
如果返回super.onInterceptTouchEvent(ev),默认false,即表示不拦截该事件,这样事件才能以分发下去。
最后是onTouchEvent(MotionEvent ev):
这个函数的作用是处理触摸事件,ViewGroupView都有这个方法,返回值的意义如下:
如果return true,表示onTouchEvent处理完事件后消费了此次事件。此时事件终结;

如果return fasle,则表示不响应事件,如果是ACTION_DOWN事件,那么该事件将会不断向上层View的onTouchEvent方法传递,直到某个View的onTouchEvent方法返回true,如果到了最顶层View还是返回false,那么事件就会交给Activity处理。且在同一个事件系列中,当前View无法再次接收到该事件序列,如果不是ACTION_DOWN事件,那么不会返回给父view的onTouchEvent处理,而是给Activity处理,并且该view可以继续接收该事件序列;  

如果return super.onTouchEvent(event);,默认是true,即表示处理事件。那这个和return true有什么区别呢?从代码就可以看出来,return super.onTouchEvent(event)会执行super.onTouchEvent(event)这个方法。比如,当你继承EditText后,重写onTouchEvent(MotionEvent event)方法,如果你将return super.onTouchEvent(event);换成return true,就会发现当你按返回取消输入框,再次点击自定义EditText时就会无法弹出输入框,解决办法可以是将return true修改成return super.onTouchEvent(event),或者是在之前调用一次super.onTouchEvent(event)方法,弹出输入框是在action为ACTION_UP的时候弹出的。

重要的知识点(大家拿本子记一下,高考必考啊)

1.一个viewGroup一旦决定拦截事件(这里分两种情况,一个是拦截了ACTION_DOWN事件,还有一个是没有子View满足分发事件的条件或者子view在ACTION_DOWN时返回了false),那么后面的事件序列都会交给它处理,并且不会再调用onInterceptTouchEvent(ev)方法, 当ACTION_DOWN事件成功传入子view的时候, 那么父ViewGroup在别的事件分发的时候,比如ACTION_MOVE,每次都会调用onInterceptTouchEvent来判断是否拦截当前事件。 也就是说,父ViewGroup的onInterceptTouchEvent不会再次调用的时机只是自己来处理这个事件,也就是自己的onTouchEvent被调用,只有这个时候才不会再次调用onInterceptTouchEvent,当事件传入子view来处理事件的时候,父ViewGroup都会每次都调用onInterceptTouchEvent来决定是否拦截当前事件。

2.dispatchTouchEvent无论返回true还是false,事件都不再进行分发,只有当其返回super.dispatchTouchEvent(ev),才表明其具有向下层分发的愿望,但是是否能够分发成功,则需要经过事件拦截onInterceptTouchEvent的审核。事件是否向上传递处理是由onTouchEvent的返回值决定的。

3.正常情况下,一个事件序列只能被一个view拦截且消耗,因为,一旦决定拦截事件,那么这个事件只能被这个view消耗,并且它的onInterceptTouchEvent(ev)方法也不会再次调用(这里的拦截和规则一中的拦截是一样的。这里的再次调用是指当确定拦截事件后,除了在ACTION_DOWN时调用onInterceptTouchEvent(ev),后面都不调用,其实跟规则一中说的一样),如果你想这个事件序列被多个view拦截消耗,那么你可以在拦截事件的那个view中的onTouchEvent()方法中调用你想让其拦截事件的那个view的onTouchEvent()方法来实现。

4.view一旦用onTouchEvent()开始处理事件,如果没有处理ATION_DOWN事件,那么同一个事件序列中的事件也不会交给他处理,会回溯给他的父控件,如果你处理了ACTION_DOWN但是没有处理ACTION_MOVE或者ACTION_UP,那么这个事件还是被你消耗,不会调用父控件的onTouchEvent方法,最后会是Activity处理,后面的事件还是继续交给你处理。其实,这就类似现实,如果别人第一次叫你做事,你没做好,那么后面就都不会放心叫你做了,如果你第一次做好了,后面没做好,别人还是会给你做的,所以,第一次很重要。

5.view的onTouchEvent默认都是消费事件的(返回true),除非是不可点击的,也就是longClickable和clickable都为false,只有这个属性会影响view的onTouchEvent的返回值,别的属性不会,比如,Enabled属性,就算是Enabled属性为false,也就是disable状态,view的onTouchEvent默认返回的还是true。

6.事件传递是由外向内的,即事件总是传递给父元素,再由父元素分发给子控件,通过requestDisallowInterceptTouchEvent();方法可以在子元素中干预父元素的事件分发过程,但是不能干预ACTION_DOWN事件,因为当时ACTION_DOWN事件的时候,父元素会重置FLAG_DISALLOW_INTERCEPT标志位。

7.使用内部拦截法的时候,为了弄清楚顺序,我就直接调试,结果,运行到父元素的dispatchTouchEvent后,不会去调用父元素的onInterceptTouchEvent方法,直接就到了子元素的dispatchTouchEvent,依然会运行到子view的onTouchEvent,等到ACTION_UP的时候才会又跑到父元素中的dispatchTouchEvent和onInterceptTouchEvent去判断是否拦截ACTION_UP事件。我倒腾了一天,才发现是需要移动,也就是让move多次调用才行,因为事件是由外向内的,当第一次ACTION_MOVE事件到的时候,先运行父ViewGroup的dispatchTouchEvent方法,此时FLAG_DISALLOW_INTERCEPT依然是设置成true,所以,不会运行父ViewGroup的onInterceptTouchEvent方法,直接就会运行子view的dispatchTouchEvent方法,然后FLAG_DISALLOW_INTERCEPT被设置成false,于是当第二次的ACTION_MOVE到来的的时候,才会去运行父viewGroup的onInterceptTouchEvent方法,然后子view收到ACTION_CANCEL事件,等到第三个ACTION_MOVE的时候父viewGroup才开始拦截事件。但是因为我之前是调试,所以都只有一次move事件,结果就不一样了。也是醉了。并且使用内部拦截法的时候,ACTION_UP事件也会被父view拦截,不会传递到子view中,也就意味着子view的onClick事件不会响应,这一点要记住。

8.内部拦截法和外部拦截法的区别:内部拦截法需要到该事件的第三个的时候才有用,也就是该事件的第一个依然被子view得到,外部拦截法则是到第二个就有用了,子view不会得到该事件的任何一个,比如,拦截ACTION_MOVE的时候,使用内部拦截法在拦截第三个ACTION_MOVE的时候才拦截了,因为第一个ACTION_MOVE会被子view得到,而使用外部拦截法则是第二个ACTION_MOVE的时候就拦截了,因为子view不会得到ACTION_MOVE中的任何一个。详情可以见9,10。所以,使用外部拦截法要好点。

9.当viewGroup没有拦截ACTION_DOWN而拦截了ACTION_MOVE或者ACTION_UP的时候,那么,第一个被拦截的动作不会在viewGroup中的onTouchEvent中触发,也不会在子view的onTouchEvent中触发,而是子view会受到ACTION_CANCEL事件。该事件序列后面的事件都会被拦截,并且下一个同类型的事件传来时,不会再调用viewGroup的onInterceptTouchEvent方法,直接就调用viewGroup的onTouchEvent方法,这里解释一下,什么是第一个被拦截的动作,比如,多个move的时候,第一个move就不会被父view或者子view执行,感觉是这个事件变成了ACTION_CANCEL事件传递到了子view。也就是说,一旦在这种情况下,ACTION_UP事件永远不会被子view接收。也就意味着,不管是使用外部拦截法还是内部拦截法,只要拦截了,那么子view就收不到ACTION_UP事件。还有就是ViewGroup就不要拦截ACTION_UP了,因为这样大家都得不到ACTION_UP事件,何必呢?

10.为了讲述方便,当从一个ViewGroup分发事件到子ViewGroup时,在子ViewGroup的dispatchTouchEvent方法中调用父类的dispatchTouchEvent,发现不会继续调用父类的onInterceprTouchEvent,而是直接调用子ViewGroup的onInterceptTouchEvent,为什么在这里调用父类的dispatchTouchEvent不会跟着调用父类的onInterceptTouchEvent?我调试和看源码发现当运行到findChildWithAccessibilityFocus()方法时,view会变成接受到事件的view,然后就不知道了。水平还是看不懂源代码。其实这是因为我对java的理解有错误,基于方法都是基于对象的,所以在子viewGroup中调用父类的dispatchTouchEvent,也就是super.dispatchTouchEvent()时,这时会运行到父viewGroup的dispatchTouchEvent里,会调用onInterceptTouchEvent方法,这时的onInterceptTouchEvent其实就已经是子viewGroup的onInterceptTouchEvent方法,而不是父ViewGroup的dispatchTouchEvent方法,因为方法是基于对象的。

11.onClick发生的前提就是可点击,并且收到了ACTION_DOWN和ACTION_UP事件。这里解释一下,这里的收到了用词不是那么准确,应该是能接收到事件,并且return super.onTouchEvent()了,记住是return super.onTouchEvent(),如果是return true都不行,因为return true没有执行view的onTouchEvent方法,而点击事件是在ACTION_UP中设置的。即在ACTION_UP的时候源码中调用了performClick()方法。这里贴一部分源代码

TextView的源代码:

final boolean superResult = super.onTouchEvent(event);

View的源代码:

case MotionEvent.ACTION_UP:
              boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
              if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                  // take focus if we don't have it already and we should in
                  // touch mode.
                  boolean focusTaken = false;
                  if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                      focusTaken = requestFocus();
                  }

                  if (prepressed) {
                      // The button is being released before we actually
                      // showed it as pressed.  Make it show the pressed
                      // state now (before scheduling the click) to ensure
                      // the user sees it.
                      setPressed(true, x, y);
                 }

                  if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                      // This is a tap, so remove the longpress check
                      removeLongPressCallback();

                      // Only perform take click actions if we were in the pressed state
                      if (!focusTaken) {
                          // Use a Runnable and post this rather than calling
                          // performClick directly. This lets other visual state
                          // of the view update before click actions start.
                          if (mPerformClick == null) {
                              mPerformClick = new PerformClick();
                          }
                          if (!post(mPerformClick)) {
                              performClick();
                          }
                      }
                  }