事件分发是一个重点也是难点,所以,本篇幅有点长,如果耐心看完本篇,相信读者会有收获的。同时,读者也可以自己写例子测试,毕竟, 纸上得来终觉浅,绝知此事要躬行 。但是,对于水平高的读者,其实 最好的方式是看源代码,因为一切原因都可以从源头找到答案 。
关于事件分发
关于事件分发,其实主要就是理解三个函数,这三个函数分别是 dispatchTouchEvent(MotionEvent ev)
, onInterceptTouchEvent(MotionEvent ev)
以及 onTouchEvent(MotionEvent ev)
,这里直接上一张图,这张图将分发事件中的重要的三个函数之间的关系表达的比较清晰,知道了关系以后,我们还需要了解这三个函数,了解一个函数其实无非就是理解它的作用,传入的参数以及返回值。
先说 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)
:
这个函数的作用是处理触摸事件, ViewGroup
和 View
都有这个方法,返回值的意义如下:
如果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(); } } }