Android-事件的触发机制
一些有的没的
View是Android中所有控件的基类,同时也是界面层所有控件的一种抽象,代表了一个控件,如Button,TextView。
ViewGroup也是继承view的,翻译为控件组,代表许多View的集合,如LinearLayout,ListView。
MotionEvent事件为手机屏幕触摸事件封装类的对象,其中封装了该事件的所有信息,如触摸位置、类型、时间等参数,其中最重要的参数有三个:
① ACTION_DOWN:表示所有事件的开始
② ACTION_MOVE:滑动事件
③ ACTION_UP:表示事件的结束
View和ViewGroup的主要事件函数:
① 对应view类控件
dispatchTouchEvent(MotionEvent ev) 分发TouchEvent
onTouchEvent(MotionEvent ev) 处理TouchEvent
② 对应viewgroup类控件
dispatchTouchEvent(MotionEvent ev) 分发TouchEvent
onTouchEvent(MotionEvent ev) 处理TouchEvent
onInterceptTouchEvent(MotionEvent en) 拦截TouchEvent
方法一:
public boolean dispatchTouchEvent(MotionEvent ev){
returnsuper.dispatchTouchEvent(ev);
}
主要负责对onTouchEvent事件进行分类,本身不处理onTouchEvent事件,而分类的方式由其返回值决定:
返回true:将事件交给当前view本身的onTouchEvent处理;
返回false:将事件交给当前view本身的onInterceptTouchEvent处理,再由其决定事件传递的方向。
方法二:
publicboolean onInterceptTouchEvent(MotionEvent ev){
return super.onInterceptTouchEvent(ev);
}
主要用于负责处理事件并改变事件的传递方向,其传递方向也由返回值决定:
返回true:事件会传递给当前控件的onTouchEvent(),而不再传递给子控件,就是所谓的Intercept(截断),并且一旦截断,此方法就不会再被调用。
返回false:事件会传递给子控件的dispatchTouchEvent方法,再由子控件分发。
方法三:
public Boolean onTouchEvent(MotionEvent event){
returnsuper.onTouchEvent(event);
}
当已经完整的处理了该事件且不希望其他回调方法再次处理时返回true,否则返回false。
View的事件分发
首先给一个Button注册点击和触摸事件:
输出日志:
可以看到事件的传递顺序是先经过onTouch再传递到onClick,如果将onTouch方法的返回值改为true再执行:
可以看到onClick方法不再执行了!下面从源码中看原理。
首先需要知道的是,只要触摸到了任何一个控件,就一定会调用该控件的dispatchTouchEvent方法,看一下view中dispatchTouchEvent方法的源码:
第一个条件mOnTouchListener是在setOnTouchListener方法里赋值的,只要我们注册了onTouch事件,这个变量就一定被赋值了;第二个条件判断当前点击的控件是否是enable的,按钮默认都是enable;第三个条件是去回调onTouch方法,如果onTouch方法返回true,整个方法就返回true,不会再向下执行(这里所说的不会向下执行就是不会再执行onTouchEvent方法,从日志中看出其不会再执行onClick方法,也可以说明,onClick方法的调用肯定在onTouchEven方法中!),如果onTouch方法返回false,整个方法会去执行onTouchEven(event)方法。
onTouch和onTouchEvent方法的区别:
这两个方法法都是在dispatchTouchEvent中调用的,onTouch优先于onTouchEven执行,如果在onTouch方法中通过返回true将事件消费掉,onTouchEven将不会再执行。
从dispatchTouchEvent的执行条件可以看到,onTouch能够得到执行需要两个前提条件:mOnTouchListener的值不能为空;当前点击的控件必须是enable的。
从onTouchEven的源码中可以得到的结论:如果该控件是可以点击的,就会返回true,也即,只要触摸的控件是可以点击的,dispatchTouchEvent方法的返回值就是true。
Touch事件的层级传递:
如果给一个控件注册了Touch事件,每次点击它的时候就会触发一系列的ACTION_DOWN、ACTION_MOVE、ACTION_UP事件,在执行ACTION_DOWN的时候返回了false,后面一系列的其他action就不会再得到执行了,也即,当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回了true,才会触发后一个action。
ViewGroup的事件分发
首先自定一个布局MyLayout并继承LinearLayout,并在布局中定义两个button,并注册MyLayout的触摸事件以及两个button的点击事件。
运行程序,并分别点击button1,button2和MyLayout的空白区域后,输出日志如下:
可以发现,当点击按钮的时候,MyLayout注册的onTouch方法并不会执行,只有点击空白区域的时候才会执行该方法,可以理解为button的onClick方法把事件先消费掉了,因此事件不会再继续传递。从源码中看原理:
MyLayout继承的LinearLayout属于viewgroup,其中有一个onInterceptTouchEvent方法,其源码为:
在MyLayout中重写此方法,返回值改为true后,再点击button1、button2以及MyLayout的空白区,输出日志如下:
无论点击哪里,都只执行MyLayout的onTouch方法,看一下viewgroup的dispatchTouchEvent方法的源码:
代码13行有一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中,disallowIntercept是指是否禁用掉事件拦截的功能,默认为false;当第一个值为false时就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值就是对onInterceptTouchEvent方法的返回值取反,默认值为false,取反后为true就进入到条件判断的内部了;当重写了onInterceptTouchEvent方法后返回值为true,导致所有按钮的点击事件都屏蔽了,也就说明按钮点击事件的处理就是在此条件判断内部进行的。
情况①:当不更改onInterceptTouchEvent的返回值时,会进入到13行的条件判断,在条件语句内部19行有一个for循环,遍历了当前viewgroup下的所有字view,之后在第24行判断当前的view是不是正在点击的view,如果是的话就进入到下一个条件判断内,在第29行调用了当前view的dispatchTouchEvent方法,下面执行的活动和view的事件分发是一致的了。调用子view的dispatchTouchEvent方法后是有返回值的,当一个控件是可点击的,那么点击该控件时,dispatchTouchEvent的返回值必定是true,也就会在31行代码处给viewgroup的dispatchTouchEvent方法直接返回true,这样后面的代码就不会执行了。
做个测试,重写button1的onTouch方法,点击button1、button2和MyLayout的空白区域后,调用过程应该为:button1的onTouch方法,button1的onClick方法,button2的onClick方法,MyLayout的onTouch方法,结果如下:
情况②:点击空白区域。在第44行代码,如果target等于null,就会进入到之后的条件判断内部,就会在第50行调用super.dispatchTouchEvent方法,也就是会进入View中的dispatchTouchEvent方法,也就只会运行MyLayout的onTouch方法。(在介绍View的事件分发时说过,执行View的dispatchTouchEvent方法需要判断三个条件:1. mOnTouchListener是否注册了点击事件;2.控件是否可点击;3.回调onTouch方法返回值。如果三个条件都为true,则执行条件语句返回true,如果其中有一个不为true,则执行onTouchEvent方法)
情况③:更改onInterceptTouchEvent方法的返回值为true,再分别点击button和MyLayout的空白区域,则不管点击哪里,都只会执行MyLayout的onTouch方法,输出日志为:
笔记写到这其实应该很清楚才对,可我已经成功把自己绕晕了,所以,我决定不看源码了,直接写代码!
自定义了一个TestButton继承Button,一个GroupView继承LinearLayout,并重写他们的dispatchTouchEvent方法、onTouchEvent方法和onInterceptTouchEvent方法。
首先模拟TestButton处理该事件,也就是令TestButton的onTouchEvent方法的返回值为true,并点击TestButton:
点击空白区域:
模拟GroupView处理该事件,令ViewGroup的onTouchEvent方法的返回值为true,点击TestButton:
点击空白区域:
恩。。。。。运行完之后我发现绕的圈圈在哪里了,就是,在模拟GroupView处理事件时,点击按钮,为啥还会执行TestButton的onTouchEvent方法???(只执行到ACTION_DOWN这个动作)
我懂了!!!
刚才模拟GroupView处理事件时,是将GroupView的onTouchEvent的返回值改为了true,并不是将GroupView的onInterceptTouchEvent的返回值改为了true,它还会向view传递,并调用view的dispatchTouchEvent方法和onTouchEvent方法,但执行到ACTION_DOWN动作后,onTouchEvent方法的返回值为false,就不会再执行下面一系列的action了。
好啦,绕晕后梳理一哈
Activity对点击事件的分发
点击事件发生的时候,最先传递给了Activity,由Activity的dispatchTouchEvent来进行事件的分发。具体工作由Activity内部的Window来完成,Window将事件传递给décor view(一般是当前界面的底层容器,即setContentView所设置的View的父容器,通过Activity.getWindow.getDecorView()可以获得。)
看下Activity的dispatchTouchEvent方法:
一般事件都是从ACTION_DOWN开始的,所以进入if语句后执行onUserInteraction()方法:
没错这是一个空方法,主要用于屏保。接下来看一下方法superDispatchTouchEvent():
再看mDecor.superDispatchTouchEvent()方法:
明白啦,就是调用了ViewGroup的dispatchTouchEvent方法,也就是将事件分发给了ViewGroup!也就是说,当ViewGroup的dispatchTouchEvent方法返回true后,Activity的dispatchTouchEvent方法也返回true,表示被消费掉了(具体被谁消费掉的不管,反正不是Activity),如果返回false,则执行Activity的onTouchEvent方法,表示事件被Activity消费掉了。
ViewGroup对点击事件的分发
ViewGroup的dispatchTouchEvent方法很长,但倔强的我还是把它贴了出啦(哈哈哈,神经病!)
图中框出来的黄色框框,有一个if条件判断,当进入条件判断之后,就会进入一个红色框框的for循环,对子view进行遍历,判断是否是当前点击的view,看蓝色框框的if判断,似曾相识有木有!和Activity的dispatchTouchEvent里面的判断一样有木有,调用子view的dispatchTouchEvent方法,也即将点击事件分发给子view。厉害了,下面分析一下ViewGroup甩锅的两个条件(有一个为true即执行):
① disallowIntercept:表示是否禁用事件拦截的功能,默认值为false,可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改;
② onInterceptTouchEvent(ev)方法返回值的取反,默认为false:
�那么问题还有一个,如果没有进入到这个条件判断呢(比如拦截,比如没有遍历到正确的子View,再进一步讲,子View的dispatchTouchEvent方法返回的是false,又会执行什么操作呢,看源码谢谢!)
先看有拦截时,也就是当onInterceptTouchEvent方法返回值为true时,黄色框,return true,意思是ViewGroup要自己消费这个事件,这里又有一个坑,请跳:(如果当前的ViewGroup是可以点击的,那么当前ViewGroup的onTouchEvent方法会返回true,如果不可点击,onTouchEvent方法返回false,当返回false时,事件就会推给上一级处理,也就是会交给ViewGroup的上一级Activity处理)
当前ViewGroup可点击输出日志:
当前ViewGroup不可点击输出日志:
区别在:在执行ACTION_DOWN动作的时候,执行完ViewGroup的onTouchEvent方法后又执行了Activity的onTouchEvent方法,并且在执行ACTION_UP和ACTION_MOVE动作时,都是Activity做的处理,ViewGroup已不再进行处理。
再看另一种情况,没有遍历到目标子view,也就是点击了空白区域。绿色框框对当前的点击进行了判断,判断是不是点击了空白区域,如果点击的区域是空白区,便return super.dispatchTouchEvent()(这里有个坑,我很勇敢的跳了进去),就问!ViewGroup的父类是谁!是View啊!!!不是Activity啊!!!这里操作的就是View的dispatchTouchEvent方法啊,那么就看下一小节,View对点击事件的分发。�
View对点击事件的分发
看View的dispatchTouchEvent方法的源码:
首先三个判断:
① mOnTouchListener != null
此变量在setOnTouchListener()方法里赋值,也即只要给控件注册了Touch事件,此变量一定不为空;
② (mViewFlags&ENABLE_MASK) == ENABLE
判断当前点击事件是否可点击,很多View默认条件是enable的,所以默认值为true;
③ mOnTouchListener.onTouch(this,event)
回调控件的Touch事件的onTouch方法,默认返回值为false。
当这三个条件都为true时,则View的dispatchTouchEvent方法返回值为true,直接由当前view消费此事件,若有一个条件为false,则View的dispatchTouchEvent方法返回值为onTouchEvent()方法。那我们就来看一下onTouchEvent方法吧好吧。
看上面的红色框框,是对控件是否可点击进行判断,如果是可以点击的,就一定会return true,该控件自己处理这个事件,就会调用上面的黄色框框的方法,performClick:
就在这个函数里面调用了onClick有没有,也就解释了,当ACTION_UP动作结束之后才会有onClick方法的日志输出:
如果这个控件是不可点击的,就一定会return false,则相当于view的dispatchTouchEvent方法返回了false,这里也就对应上面介绍ViewGroup的dispatchTouchEvent方法子view的dispatchTouchEvent方法返回false的情况,这种情况下从ViewGroup的dispatchTouchEvent方法中可以看出,其走的是target = mMotionTarget,而此时mMotionTarget = null,也就是target = null,也就会调用:
�ViewGroup的super.dispatchTouchEvent方法就是view的dispatchTouchEvent方法,控件不可点击,也就直接调用当前viewGroup的onTouchEvent方法了,如果ViewGroup的onTouchEvent也返回false,则会继续调用Activity的onTouchEvent方法。
总结,这个图还是蛮好的~~~
事件由Activity的dispatchTouchEvent开始,将事件传递给当前的Activity的下一层ViewGroup;事件分发给ViewGroup时,调用其dispatchTouchEvent方法接着分发,首先会调用ViewGroup的onInterceptTouchEvent方法进行拦截,如果返回false(不拦截),就开始遍历ViewGroup的子view,将事件分发给子view,若返回值为true(拦截),事件由ViewGroup自己处理,将会调用ViewGroup自己的onTouchEvent方法,若当前ViewGroup是可点击的,则onTouchEvent返回true,自己消费事件,若当前ViewGroup不可点击,则onTouchEvent返回false,交由上一级处理;当事件分发到View时,首先还是调用view的dispatchTouchEvent方法,若返回true,则当前view消耗掉此事件,若返回true,则会调用当前view的onTouchEvent方法,同样,当前view是可以点击的,则返回true,对事件进行处理,返回false,则交由上一级处理。