好了这三个案例你是否都能从源码的角度对其分析,如果不能,看完此篇博客,你会对上述的案例非常清楚,好了我们开始事件分发机制的源码讲解
首先我们必须明白对于一个view的事件的分析首先要从dispatchTouchEvent这个方法入手,先上源码
-
-
-
-
-
-
-
- public boolean dispatchTouchEvent (MotionEvent event) {
- if (mOnTouchListener != null && ( mViewFlags & ENABLED_MASK) == ENABLED &&
- mOnTouchListener.onTouch( this, event)) {
- return true;
- }
- return onTouchEvent(event);
- }
在这里如果dispatchTouchEvent的返回值为true则表示,这个事件被view所消费,反之则不消费,从源码中看首先会进入一个if判断语句,判断的条件有三个
①mOnTouchListener!=null;
第一个条件mOnTouchListener这个监听器是什么?接着看源码
-
-
-
-
- public void setOnTouchListener(OnTouchListener l) {
- mOnTouchListener = l;
- }
明白了吧,这个监听就是我们对View设置的监听
②(mViewFlags & ENABLED_MASK ) == ENABLED
第二个条件( mViewFlags & ENABLED_MASK ) == ENABLED
条件是判断View是否Enabled,一般的View都是enabled,除非你手动去设置,也就说第二个条件是满足的。
③mOnTouchListener.onTouch(this,event)
第三个条件就是我们在activity设置setOnTouchListener后重写的onTouch()方法的返回值,也就是说这个ouTouch的返回值是我们自己设定的,假如我们给View设置了OnTouchListener,并且使ouTouch方法的返回值为true,从dispatchTouchEvent的源码中我们可以看出它就不会执行View的onTouchEvent(event)这个方法,并且此时dispatchTouchEvent的返回值为true,假如ouTouch方法的返回值为false此时View的dispatchTouchEvent的if语句的条件就为false那么就会执行View的onTouchEvent(event)这个方法,并且dispatchTouchEvent方法的返回值就是View的onTouchEvent(event)方法的返回值,到这里大家对onTouch和onTouchEvent这个两个方法的执行顺序清楚了吧。现在可以回过头来看看案例三打印的日志的顺序,可以自己分析出来了吧。
从dispatchTouchEvent方法中我们可以得出:
在进行事件分发时的执行顺序是dispatchTouchEvent--->OnTouchListener的onTouch方法--->onTouchEvent方法
到这里你应该能从源码的角度对案例三中的打印顺序进行解释,在脑子里回顾一下。。。。
接下来我们就来看onTouchEvent方法的源码
下面我们来拆分一下上面的源码首先执行一个if判断语句
- if ((viewFlags & ENABLED_MASK) == DISABLED) {
-
-
- return (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE ));
- }
在这里要特别注意的是此方法中if(((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))它的范围这里把中间的代码省略如下:
- public boolean onTouchEvent(MotionEvent event) {
- 。。。。。。。。。。。
- 此处有省略
- if (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
- switch (event.getAction()) {
- 。。。。。。。。。。。
- 此处有省略
- }
- return true;
- }
- return false;
- }
从上面的简化代码中我们可以看出只要是进入了if判断语句则onTouchEvent一定会返回true即消费事件,并且进入此if语句的条件为
此View是可以点击的或者是可以长按的。
下面我们来拆分一下上面的源码首先执行一个if判断语句
- if ((viewFlags & ENABLED_MASK) == DISABLED) {
-
-
- return (((viewFlags & CLICKABLE) == CLICKABLE ||
- (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE ));
- }
在上面的注释中已经对其进行了说明,这里单独拿出来再强调一下-----如果一个View是disabled, 并且该View是Clickable或者longClickable, onTouchEvent()就不执行下面的代码逻辑直接返回true, 表示该View就一直消费Touch事件,这一点从上面的代码可以看出,如果一个enabled的View,并且是clickable或者longClickable的,onTouchEvent()会执行下面的代码逻辑并返回true,这一点从上面的省略代码片段可以得出。综上,一个clickable或者longclickable的View是一直消费Touch事件的,而一般的View既不是clickable也不是longclickable的(即不会消费Touch事件,只会执行ACTION_DOWN而不会执行ACTION_MOVE和ACTION_UP) Button是clickable的,可以消费Touch事件,但是我们可以通过setClickable()和setLongClickable()来设置View是否为clickable和longClickable。
接着我们来分析一下onTouchEvent中的事件上面的代码中有比较详细的注释,我在这里再分析一下
ACTION_DOWN:
- case MotionEvent.ACTION_DOWN:
- if (mPendingCheckForTap == null) {
- mPendingCheckForTap = new CheckForTap();
- }
- mPrivateFlags |= PREPRESSED;
- mHasPerformedLongPress = false;
- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
- break;
在这个方法中首先给mPrivateFlags设置一个PREPRESSED的标识,然后设置为mHasPerformedLongPress设置一个初始值false
,接着会执行一个延迟- postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
在这里ViewConfiguration.getTapTimeout()的值为115毫秒(注意以上源码包括时间常量都是2.2源码中,其他源码可能会稍有不同)这个延迟有什么作用呢?
在给定的TapTimeout时间之内,用户的触摸没有移动,就当作用户是想点击,而不是滑动.具体的做法是,将 CheckForTap的实例mPendingCheckForTap添加时消息队例中,延迟执行。如果在这tagTimeout之间用户触摸移动了,则删除此消息.否则执行按下状态.然后检查长按。
经过115毫秒的延迟后会执行CheckForTap方法,这个方法是干什么的呢?来看下源码
-
-
-
- private final class CheckForTap implements Runnable {
- public void run() {
- mPrivateFlags &= ~PREPRESSED;
- mPrivateFlags |= PRESSED;
- refreshDrawableState();
-
-
-
- if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
- postCheckForLongClick(ViewConfiguration.getTapTimeout());
- }
- }
- }
这个方法可以看到如果View是LONG_CLICKABLE的就是执行postCheckForLongClick(ViewConfiguration.getTapTimeout())这个方法来检测长按事件,但是一般的View不是LONG_CLICKABLE的,可能有的人会有疑问,如果View不是LONG_CLICKABLE的怎么执行长按事件啊?此时我们需要调用setOnLongClickListener实现OnLongClickListener接口
源码如下:
- public void setOnLongClickListener (OnLongClickListener l) {
- if (!isLongClickable()) {
- setLongClickable( true);
- }
- mOnLongClickListener = l;
- }
从源码中我们可以看到设置了OnLongClickListener后如果这个View不是LONG_CLICKABLE的,那么就把它设置成LONG_CLICKABLE的。这样我们回到CheckForTap方法在View是LONG_CLICKABLE的情况下就会调用postCheckForLongClick方法,这个方法的源码如下
- private void postCheckForLongClick(int delayOffset) {
-
-
-
- mHasPerformedLongPress = false;
- if (mPendingCheckForLongPress == null) {
- mPendingCheckForLongPress = new CheckForLongPress();
- }
- mPendingCheckForLongPress.rememberWindowAttachCount();
-
-
-
-
- postDelayed(mPendingCheckForLongPress,ViewConfiguration.getLongPressTimeout() - delayOffset);
- }
在上面的方法会有一个延迟经过500-115毫秒后会执行CheckForLongPress方法。
- class CheckForLongPress implements Runnable {
-
-
- private int mOriginalWindowAttachCount;
-
-
-
-
-
- public void run() {
- if (isPressed() && (mParent != null)
- && mOriginalWindowAttachCount == mWindowAttachCount) {
- if (performLongClick()) {
-
-
-
- mHasPerformedLongPress = true;
- }
- }
- }
-
-
- public void rememberWindowAttachCount() {
- mOriginalWindowAttachCount = mWindowAttachCount;
- }
- }
从CheckForLongPress的run方法中可以看到如果performLongClick()的返回值为true mHasPerformedLongPress才为true,那么我们看看performLongClick它的做了哪些动作呢?我们来看看源码
-
-
-
-
-
-
-
- public boolean performLongClick() {
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
-
- boolean handled = false;
- /**
- * 到了重点可以看到在这里会执行我们为View设置的长按事件的回调,这里的mOnLongClickListener就是我们自己给View设置的长按的监听,
- * 从这里也可以得出一个结论即长按事件是在ACTION_DOWN中执行的
- */
- if (mOnLongClickListener != null) {
- handled = mOnLongClickListener.onLongClick(View.this);
- }
- if (!handled) {
- handled = showContextMenu();
- }
- if (handled) {
- performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- }
- return handled;
- }
终于来了个重点我们看到在其中有个判断
- if (mOnLongClickListener != null) {
- handled = mOnLongClickListener.onLongClick(View.this);
- }
也就是说如果你设置了长按的监听,那么mOnLongClickListener!=null此时就会执行我们重写的onLongClick()方法,这里我们也得出一个结论:
即长按事件是在ACTION_DOWN中执行的。
到这里我们可以总结一下:首先当执行ACTION_DOWN事件后会设置一个PREPRESSED标识,如果这次点击持续115毫秒后就会发送一个检测长按的延迟任务,这个任务的延迟时间是500-115毫秒,这个115毫秒就是检测PREPRESSED所经历的时间,所以这样算一下就可以知道当按钮从按下的那一刻起经历了500毫秒就会触发长按事件(注意这个Android 2.2中的源码,其它的系统的时间会稍有差异)
通过以上的分析我们还可以得出如下结论:
1、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture;
2、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false;
一般的View默认是不消费touch事件的,我们要想执行点击事件必须要调用setOnClickListener()来设置OnClickListener接口,我们看看这个方法的源码就知道了
- public void setOnClickListener (OnClickListener l) {
- if (!isClickable()) {
- setClickable( true);
- }
- mOnClickListener = l;
- }
当我们设置了onClickListener时如果isClickable()=false,就执行 setClickable(true)。
到这里我们就可以分析案例一和二:
在案例一中当Button和ImageView都返回false时dispatchTouchEvent 的if语句不成立就会执行onTouchEvent方法,而在onTouchEvent中有 if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE ))这个if判断语句因为ImageView的CLICKABLE=fasle,Button的CLICKABLE=true。所以Button可以进入这个if语句而ImageView不行,从源码中可以看到(结合我们简化的代码看)只要进入这个if语句那么onTouchEvent一定返回true从而dispatchTouchEvent 返回true而消耗事件,如果没有进入这个if语句那么onTouchEvent一定返回false从而dispatchTouchEvent一定返回fasle不消耗事件。而当ImageView和Button都返回true时此时dispatchTouchEvent的第一个语句成立并且返回值为true表示消费所有事件。所以此时它俩的打印内容是一样的。
ACTION_MOVE:当执行move事件时首先会拿到当前触摸的X,Y坐标然后判断当前触摸的点有没有移出当前的View,如果移出了当前的View可分为两种情况:
①ACTION_DOWN事件触发不到115毫秒时:首先执行removeTapCallback方法这个方法是移除在ACTION_DOWN方法中设置的PREPRESSED检测
这也和我们在ACTION_DOWN中的CheckForTap延迟115毫秒后执行进行的说明相吻合。
②ACTION_DOWN事件触发时间大于115毫秒时:此时已经触发了长按的事件,当前mPrivateFlags一定为PRESSED且发送了长按的检测,此时就会移除removeLongPressCallback()然后把mPrivateFlags中PRESSED标识去除
ACTION_UP:
ACTION_UP在上面的注释已经很清楚在这里我想对我们需要重重重点了解的内容分析一下。
在ACTION_UP执行的过程中有这么一个判断
- if (!mHasPerformedLongPress) {
-
- removeLongPressCallback();
-
-
- if (!focusTaken) {
-
-
-
- if (mPerformClick == null) {
- mPerformClick = new PerformClick();
- }
- if (!post(mPerformClick)) {
- performClick();
- }
- }
- }
我们首先不考虑判断条件看看这个if语句里进行了什么操作如果mPerformClick!=null的话就会执行performClick()方法我们看看此方法干了什么
-
-
-
-
-
-
- public boolean performClick() {
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
-
- if (mOnClickListener != null) {
- playSoundEffect(SoundEffectConstants.CLICK);
- mOnClickListener.onClick(this);
- return true;
- }
-
- return false;
- }
- btnClick.setOnClickListener( new OnClickListener() {
-
- @Override
- public void onClick(View v) {
- Toast. makeText(MainActivity.this,"点击事件",0) .show();
- }
- });
-
- btnClick.setOnLongClickListener( new OnLongClickListener() {
-
- @Override
- public boolean onLongClick(View v) {
- Toast. makeText(MainActivity.this,"长按事件",0).sh ow();
- return false;
- }
- });
运行后我们长按按钮发现会触发长按事件弹出“长按事件”,松开按钮后会触发点击事件弹出“点击事件”,
- if (performLongClick()) {
- mHasPerformedLongPress = true;
- }
当mHasPerformedLongPress为false的时候就是在ACTION_UP中执行performClick()方法从而执行点击事件
当setOnLongClickListener的返回值为true时,当我们快速点击按钮时会触发点击事件弹出“点击事件”,但是当我们触发了长按事件后就不会触发点击事件,此时长按事件屏蔽了点击事件。
分析:
当setOnLongClickListener返回值为true时,performLongClick方法的返回值也为true此时会从新给mHasPerformedLongPress赋值为true,当mHasPerformedLongPress为true的时候不会进入ACTION_UP中执行performClick()方法从而不会执行点击事件。
不知道通过上面的分析大家有没有理解,如果没有理解的话就多读几遍吧。
总结:
1.整个View的事件分发的流程是
View.dispatchEvent->View.setOnTouchListener->View.onTouchEvent
在dispatchTouchEvent中会进行OnTouchListener的判断,如果OnTouchListener不为null且返回true,则表示事件被消费,onTouchEvent不会被执行;否则执行onTouchEvent。
2.一个clickable或者longClickable的View会永远消费Touch事件,不管他是enabled还是disabled的
3.View的长按事件是在ACTION_DOWN中执行,要想执行长按事件该View必须是longClickable的,并且不能产生ACTION_MOVE
4.View的点击事件是在ACTION_UP中执行,想要执行点击事件的前提是消费了ACTION_DOWN和ACTION_MOVE,并且没有设置OnLongClickListener的情况下,如设置了OnLongClickListener的情况,则必须使onLongClick()返回false
5.如果View设置了onTouchListener了,并且onTouch()方法返回true,则不执行View的onTouchEvent()方法,也表示View消费了Touch事件,返回false则继续执行onTouchEvent()