事件分发与消费机制

参考郭霖博客:
http://blog.csdn.net/guolin_blog/article/details/9097463
http://blog.csdn.net/guolin_blog/article/details/9153747

标签(空格分隔): Android


onTouch与onClick的关系,调用时机###

button.setOnClickListener(new OnClickListener() {  
    @Override  
    public void onClick(View v) {  
        Log.d("TAG", "onClick execute");  
    }  
});  
button.setOnTouchListener(new OnTouchListener() {  
    @Override  
    public boolean onTouch(View v, MotionEvent event) {  
        Log.d("TAG", "onTouch execute, action " + event.getAction());  
        return false;  
    }  
}); 

onTouch是优先于onClick执行的,并且onTouch执行了两次,一次是ACTION_DOWN,一次是ACTION_UP(你还可能会有多次ACTION_MOVE的执行,如果你手抖了一下)。因此事件传递的顺序是先经过onTouch,再传递到onClick。

【要注意的是onTouch与onTouchEvent都可以监控ACTION_DOWN、ACTION_MOVE、ACTION_UP等手势,而且是持续监听,即每个ACTION都会进入这两个方法进行监听】

结论:onTouch方法是有返回值的,这里我们返回的是false,那么onClick()还会执行,如果我们尝试把onTouch方法里的返回值改成true,那么onClick()就不会执行

应用场景:为什么给ListView引入了一个滑动菜单的功能,ListView就不能滚动了?
滑动菜单的功能是通过给ListView注册了一个touch事件来实现的。如果你在onTouch方法里处理完了滑动逻辑后返回true,那么ListView本身的滚动事件就被屏蔽了,自然也就无法滑动(原理同前面例子中按钮不能点击),因此解决办法就是在onTouch方法里返回false。

滚动事件也像点击事件一样,跟onTouch的返回值有关???


【任何控件本身是没有dispatchTouchEvent方法的,是从view类继承的】
首先你需要知道一点,只要你触摸到了任何一个控件,首先会去调用该控件所在布局的dispatchTouchEvent方法【该dispatchTouchEvent方法是继承于ViewGroup】,然后在布局的dispatchTouchEvent方法中找到被点击的相应控件,再去调用该控件的dispatchTouchEvent方法。所有view控件的dispatchTouchEvent方法都是继承于view,示意图如下:
单个子View时


此处输入图片的描述
此处输入图片的描述

布局嵌套时


此处输入图片的描述
此处输入图片的描述
子View的dispatchTouchEvent方法的源码:
public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}  

//条件一:mOnTouchListener正是在setOnTouchListener方法里赋值的,也就是说只要我们给控件注册了touch事件,mOnTouchListener就一定被赋值了。

//条件二:(mViewFlags & ENABLED_MASK) == ENABLED是判断当前点击的控件是否是enable的,按钮默认都是enable的

//条件三:mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册touch事件时的onTouch方法。

让这三个条件全部成立,从而dispatchTouchEvent方法直接返回true
而且onClick的调用肯定是在onTouchEvent(event)方法中的,所以当在onTouch方法里返回了true【而且既然可以调用onTouch方法,那么就一定满足该控件注册了touch事件,而且是可以点击的】,就会让dispatchTouchEvent方法直接返回true,所以不会走return onTouchEvent(event),当然就不会调用到onClick方法了

1. onTouch和onTouchEvent有什么区别,又该如何使用?
从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

因为onTouchEvent()方法的代码有点长,所以就不列出来了,在onTouchEvent()可以判断ACTION_DOWN、ACTION_MOVE、ACTION_UP等手势。还有里面的performClick()会调用到onClick()

public boolean performClick() {  
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
    if (mOnClickListener != null) {  
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}  
//mOnClickListener不是null,就会去调用它的onClick方法

而mOnClickListener是在这里赋值的

public void setOnClickListener(OnClickListener l) {  
    if (!isClickable()) {  
        setClickable(true);  
    }  
    mOnClickListener = l;  
}  

touch事件的ACTION层级传递###

我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action。

这里要补充一句,即使在onTouch事件里面返回了false。所以就一定会进入到onTouchEvent方法中其中有一个if用于判断是否可以点击,如果可以点击则返回一个true。是不是有一种被欺骗的感觉?明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得ACTION_UP可以得到执行。
所以将按钮替换成ImageView,然后给它也注册一个touch事件,并返回false。在ACTION_DOWN执行完后,后面的一系列action都不会得到执行了。因为ImageView和按钮不同,它是默认不可点击的,所以不能进入onTouchEvent方法中其中的那个用于判断是否可以点击的if,直接在onTouchEvent中返回false,所以最终的返回还是false,所以就导致后面其它的action都无法执行了。

应用场景:为什么图片轮播器里的图片使用Button而不用ImageView?
主要就是因为Button是可点击的,而ImageView是不可点击的。如果想要使用ImageView,可以有两种改法。第一,在ImageView的onTouch方法里返回true,这样可以保证ACTION_DOWN之后的其它action都能得到执行,才能实现图片滚动的效果。第二,在布局文件里面给ImageView增加一个android:clickable="true"的属性,这样ImageView变成可点击的之后,即使在onTouch里返回了false,ACTION_DOWN之后的其它action也是可以得到执行的。

布局嵌套时的事件分发##

分发顺序:
Acivity->Window->DecorView->ViewGroup->View

ViewGroup中有一个onInterceptTouchEvent方法

ViewGroup中有一个dispatchTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent ev) {  
    final int action = ev.getAction();  
    final float xf = ev.getX();  
    final float yf = ev.getY();  
    final float scrolledXFloat = xf + mScrollX;  
    final float scrolledYFloat = yf + mScrollY;  
    final Rect frame = mTempRect;  
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            mMotionTarget = null;  
        }  
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
            ev.setAction(MotionEvent.ACTION_DOWN);  
            final int scrolledXInt = (int) scrolledXFloat;  
            final int scrolledYInt = (int) scrolledYFloat;  
            final View[] children = mChildren;  
            final int count = mChildrenCount;  
            for (int i = count - 1; i >= 0; i--) {  
                final View child = children[i];  
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                        || child.getAnimation() != null) {  
                    child.getHitRect(frame);  
                    if (frame.contains(scrolledXInt, scrolledYInt)) {  
                        final float xc = scrolledXFloat - child.mLeft;  
                        final float yc = scrolledYFloat - child.mTop;  
                        ev.setLocation(xc, yc);  
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
                        if (child.dispatchTouchEvent(ev))  {  
                            mMotionTarget = child;  
                            return true;  
                        }  
                    }  
                }  
            }  
        }  
    }  
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
            (action == MotionEvent.ACTION_CANCEL);  
    if (isUpOrCancel) {  
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
    }  
    final View target = mMotionTarget;  
    if (target == null) {  
        ev.setLocation(xf, yf);  
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
            ev.setAction(MotionEvent.ACTION_CANCEL);  
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        }  
        return super.dispatchTouchEvent(ev);  
    }  
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
        final float xc = scrolledXFloat - (float) target.mLeft;  
        final float yc = scrolledYFloat - (float) target.mTop;  
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        ev.setLocation(xc, yc);  
        if (!target.dispatchTouchEvent(ev)) {  
        }  
        mMotionTarget = null;  
        return true;  
    }  
    if (isUpOrCancel) {  
        mMotionTarget = null;  
    }  
    final float xc = scrolledXFloat - (float) target.mLeft;  
    final float yc = scrolledYFloat - (float) target.mTop;  
    ev.setLocation(xc, yc);  
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
        ev.setAction(MotionEvent.ACTION_CANCEL);  
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
        mMotionTarget = null;  
    }  
    return target.dispatchTouchEvent(ev);  
}  
//在ViewGroup中的dispatchTouchEvent方法,第13行可以看到一个条件判断,如果disallowIntercept和!onInterceptTouchEvent(ev)两者有一个为true,就会进入到这个条件判断中。
//disallowIntercept是指是否禁用掉事件拦截的功能,默认是false,也可以通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改。那么当第一个值为false的时候就会完全依赖第二个值来决定是否可以进入到条件判断的内部,第二个值是什么呢?竟然就是对onInterceptTouchEvent方法的返回值取反!也就是说如果我们在onInterceptTouchEvent方法中返回false,就会让第二个值为true,从而进入到条件判断的内部,如果我们在onInterceptTouchEvent方法中返回true,就会让第二个值为false,从而跳出了这个条件判断。
//而子view的时事件分发就是在这个条件判断中,这个条件判断的内部是怎么实现的?在第19行通过一个for循环,遍历了当前ViewGroup下的所有子View,然后在第24行判断当前遍历的View是不是正在点击的View,如果是的话就会进入到该条件判断的内部,然后在第29行调用了该View的dispatchTouchEvent,之后的流程就跟不嵌套时的子view事件分发一样。
//所以说当跳出了这个条件判断后,即在onInterceptTouchEvent方法中返回true,就会让第二个值!onInterceptTouchEvent为false子View的事件分发将得不到执行,所以子View就会被屏蔽。
//而当子View没有被屏蔽时,即子View的dispatchTouchEvent得到执行,而子View是可点击的,子View的dispatchTouchEvent一定返回True,就会导致ViewGroup的dispatchTouchEvent第29行的条件判断成立,于是在第31行给ViewGroup的dispatchTouchEvent方法直接返回了true。这样就导致后面的代码无法执行到了,所以导致后面的代码中ViewGroup的touch事件就没有办法执行了
//

而当我们点击的只是ViewGroup的空白区域而不是子View时,首先也是会进入ViewGroup的dispatchTouchEvent,就算ViewGroup的onInterceptTouchEvent返回的是true,令ViewGroup的dispatchTouchEvent中的13行的条件判断的!onInterceptTouchEvent变成false,不进入这个条件判断里面,即拦截子View的事件。但是这时点击的不是子View控件,所以不会在dispatchTouchEvent的31行返回true,令方法立马结束,不会使ViewGroup的touch事件没有执行


此处输入图片的描述
此处输入图片的描述

***结论:

  • 要想拦截子View的事件,就重写ViewGroup的onInterceptTouchEvent方法,使其返回true。***默认是false不拦截子View事件

  • 而且子View没有onInterceptTouchEvent

  • 如果ViewGroup的onInterceptTouchEvent方法,使其返回true即拦截子View事件,那么在拦截了同一序列事件中的ACTION_DOWN后,onInterceptTouchEvent不会在被调用,并且继续拦截剩下的ACTION_MOVE,ACTION_UP

  • 另外注意下如果子View设置了FLAG_DISALLOW_INTERCEPT这个标记位,具体看一下《开发艺术探索》,可以让ViewGroup只能拦截ACTION_DOWN,不能拦截ACTION_MOVE,ACTION_UP

  • 拦截方式有外部拦截法于内部拦截法,一般推荐使用较为简单的外部拦截法

  • 所有函数的返回值都是显而易见的,True:拦截、消费处理。False:不拦截、不消费处理。只有onTouch()的返回值是怪怪的,返回的是false的时候onClick()才会执行!!!


事件消费传递###

如果子View的onTouchEvent返回的是false,那么就会交给他的父容器onTouchEvent处理,如果所有的元素都不处理这个事件的话,就会交给Activity的onTouchEvent处理。

只要找到了事件处理者的话,只要当前ACTION会有去找处理者的过程,而之后的每个ACTION会直接被之前找到的处理者消费掉,不会有那个找处理者的那个查找过程。

【那onTouch的返回值有对消费传递有影响吗?】
  答案是有影响的,即使onTouch的返回值是false,就会调用onTouchEvent,所以事件传递消费还是要看onTouchEvent;如果onTouch的返回值是true的话,事件就会被处理掉,而且onTouchEvent不会被调用。

【在onTouchEvent的返回值指的是每个case判断中对每个ACTION的处理后的返回值,还是指onTouchEvent最底下的返回值?】
一般指的是每个case判断中对每个ACTION的处理后的返回值,但是onTouchEvent最底下的返回值是必须要写的,因为函数必须要有返回值。

View的onTouchEvent默认返回的是true,即处理消费事件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容