Android事件分发机制

最近因为个人原因需要准备一些资料,所以就萌生了把应用层所涉及的知识点做个基类,不仅仅是简单的知识结论,同时还希望能从源码角度上做个积累,让自己日后在学习中都能有时间拿出来翻翻,可能每次花费几分钟的时候都会有新的感悟,同时也是为了锻炼自己的能力,毕竟不是做纯移动端的,技术还是挺薄弱的,而且之前去大公司面没拿到啥好成绩哈,自嘲一下<..>,希望大家共同加油吧。接下来记录的是Android事件分发机制,其中有些图片是借鉴别的同学,大家多多包含哈。

  • 事件分发流程
  • 源码解析
    -View分发
    -ViewGroup分发

事件分发流程

事件分发我们需要关注主要是三个方法dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。但是这三个方法对应的返回值或执行super方法都会导致最终事件分发的走向。

若都返回super方法,则事件分发的流程如下图

image.png

整个事件流向应该是从Activity---->ViewGroup--->View 从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View--->ViewGroup--->Activity从下往上调用onTouchEvent方法。

若dispatchTouchEvent和onTouchEvent返回true,则事件分发的流程如下图

image.png

当dispatchTouchEvent或onTouchEvent返回true,事件就会停止传递被消耗的。为什么会这样呢,等下我们分析下代码。

若dispatchTouchEvent和onTouchEvent返回false,则事件分发的流程如下图

image.png

关注下标注蓝线的部分,当返回false时,事件都会回传给父控件的onTouchEvent处理,同样这是为什么呢,我们也可以通过源码来看看哈。

接下来总结一下。我们常见的使用场景如滑动冲突控制,就需要改变如onIterceptTouchEvent的返回值等。
ViewGroup和View 的dispatchTouchEvent 是做事件分发,那么这个事件可能分发出去的四个目标

注:------> 后面代表事件目标需要怎么做。
1、 自己消费,终结传递。------->return true ;
2、 给自己的onTouchEvent处理-------> 调用super.dispatchTouchEvent()系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的onTouchEvent处理。
3、 传给子View------>调用super.dispatchTouchEvent()默认实现会去调用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就会把事件传给子类。
4、 不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent------->return false;
注: 由于View没有子View所以不需要onInterceptTouchEvent 来控件是否把事件传递给子View还是拦截,所以View的事件分发调用super.dispatchTouchEvent()的时候默认把事件传给自己的onTouchEvent处理(相当于拦截),对比ViewGroup的dispatchTouchEvent 事件分发,View的事件分发没有上面提到的4个目标的第3点。

ViewGroup和View的onTouchEvent方法是做事件处理的,那么这个事件只能有两个处理方式:

1、自己消费掉,事件终结,不再传给谁----->return true;
2、继续从下往上传,不消费事件,让父View也能收到到这个事件----->return false;View的默认实现是不消费的。所以super==false。

ViewGroup的onInterceptTouchEvent方法对于事件有两种情况:

1、拦截下来,给自己的onTouchEvent处理--->return true;
2、不拦截,把事件往下传给子View---->return false,ViewGroup默认是不拦截的,所以super==false;

关于ACTION_MOVE 和 ACTION_UP

上面讲解的都是针对ACTION_DOWN的事件传递,ACTION_MOVE和ACTION_UP在传递的过程中并不是和ACTION_DOWN 一样,你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。具体这句话很多博客都说了,但是具体含义是什么呢?我们来看一下下面的具体分析。

上面提到过了,事件如果不被打断的话是会不断往下传到叶子层(View),然后又不断回传到Activity,dispatchTouchEvent 和 onTouchEvent 可以通过return true 消费事件,终结事件传递,而onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,ACTION_MOVE和ACTION_UP 会在哪些函数被调用,之前说了并不是哪个函数收到了ACTION_DOWN,就会收到 ACTION_MOVE 等后续的事件的。
下面通过几张图看看不同场景下,ACTION_MOVE事件和ACTION_UP事件的具体走向并总结一下规律。

1、我们在ViewGroup1 的dispatchTouchEvent 方法返回true消费这次事件
ACTION_DOWN 事件从(Activity的dispatchTouchEvent)--------> (ViewGroup1 的dispatchTouchEvent) 后结束传递,事件被消费(如下图红色的箭头代码ACTION_DOWN 事件的流向)。
下图中红色的箭头代表ACTION_DOWN 事件的流向,蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image.png

2、我们在ViewGroup2 的dispatchTouchEvent 返回true消费这次事件

image.png

3、我们在View 的dispatchTouchEvent 返回true消费这次事件
这个我不就画图了,效果和在ViewGroup2 的dispatchTouchEvent return true的差不多,同样的收到ACTION_DOWN 的dispatchTouchEvent函数都能收到 ACTION_MOVE和ACTION_UP。
所以我们就基本可以得出结论如果在某个控件的dispatchTouchEvent 返回true消费终结事件,那么收到ACTION_DOWN 的函数也能收到 ACTION_MOVE和ACTION_UP。
4、我们在View 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image.png

5、我们在ViewGroup 2 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image.png

6、我们在ViewGroup 1 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image.png

7、我们在Activity 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image.png

8、我们在View的dispatchTouchEvent 返回false并且Activity 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
image.png

9、我们在View的dispatchTouchEvent 返回false并且ViewGroup 1 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image.png

10、我们在View的dispatchTouchEvent 返回false并且在ViewGroup 2 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

image.png

11、我们在ViewGroup2的dispatchTouchEvent 返回false并且在ViewGroup1 的onTouchEvent返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
image.png

12、我们在ViewGroup2的onInterceptTouchEvent 返回true拦截此次事件并且在ViewGroup 1 的onTouchEvent返回true消费这次事件。
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
image.png

对于在onTouchEvent消费事件的情况:在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。
对于ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

分发机制源码解析

其实在上述的结论中,都是可以从源码上一探究竟的。那我们先从View的事件分发开始吧,主要还是看dispatchTouchEvent和onTouchEvent方法。

  • View事件分发源码
    1. public boolean dispatchTouchEvent(MotionEvent event) {  
    2.     if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
    3.             mOnTouchListener.onTouch(this, event)) {  
    4.         return true;  
    5.     }  
    6.     return onTouchEvent(event);  
    7. }  
    1. public boolean onTouchEvent(MotionEvent event) {  
    2.     final int viewFlags = mViewFlags;  
    3.     if ((viewFlags & ENABLED_MASK) == DISABLED) {  
    4.         // A disabled view that is clickable still consumes the touch  
    5.         // events, it just doesn't respond to them.  
    6.         return (((viewFlags & CLICKABLE) == CLICKABLE ||  
    7.                 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));  
    8.     }  
    9.     if (mTouchDelegate != null) {  
    10.         if (mTouchDelegate.onTouchEvent(event)) {  
    11.             return true;  
    12.         }  
    13.     }  
    14.     if (((viewFlags & CLICKABLE) == CLICKABLE ||  
    15.             (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
    16.         switch (event.getAction()) {  
    17.             case MotionEvent.ACTION_UP:  
    18.                 boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
    19.                 if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
    20.                     // take focus if we don't have it already and we should in  
    21.                     // touch mode.  
    22.                     boolean focusTaken = false;  
    23.                     if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
    24.                         focusTaken = requestFocus();  
    25.                     }  
    26.                     if (!mHasPerformedLongPress) {  
    27.                         // This is a tap, so remove the longpress check  
    28.                         removeLongPressCallback();  
    29.                         // Only perform take click actions if we were in the pressed state  
    30.                         if (!focusTaken) {  
    31.                             // Use a Runnable and post this rather than calling  
    32.                             // performClick directly. This lets other visual state  
    33.                             // of the view update before click actions start.  
    34.                             if (mPerformClick == null) {  
    35.                                 mPerformClick = new PerformClick();  
    36.                             }  
    37.                             if (!post(mPerformClick)) {  
    38.                                 performClick();  
    39.                             }  
    40.                         }  
    41.                     }  
    42.                     if (mUnsetPressedState == null) {  
    43.                         mUnsetPressedState = new UnsetPressedState();  
    44.                     }  
    45.                     if (prepressed) {  
    46.                         mPrivateFlags |= PRESSED;  
    47.                         refreshDrawableState();  
    48.                         postDelayed(mUnsetPressedState,  
    49.                                 ViewConfiguration.getPressedStateDuration());  
    50.                     } else if (!post(mUnsetPressedState)) {  
    51.                         // If the post failed, unpress right now  
    52.                         mUnsetPressedState.run();  
    53.                     }  
    54.                     removeTapCallback();  
    55.                 }  
    56.                 break;  
    57.             case MotionEvent.ACTION_DOWN:  
    58.                 if (mPendingCheckForTap == null) {  
    59.                     mPendingCheckForTap = new CheckForTap();  
    60.                 }  
    61.                 mPrivateFlags |= PREPRESSED;  
    62.                 mHasPerformedLongPress = false;  
    63.                 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
    64.                 break;  
    65.             case MotionEvent.ACTION_CANCEL:  
    66.                 mPrivateFlags &= ~PRESSED;  
    67.                 refreshDrawableState();  
    68.                 removeTapCallback();  
    69.                 break;  
    70.             case MotionEvent.ACTION_MOVE:  
    71.                 final int x = (int) event.getX();  
    72.                 final int y = (int) event.getY();  
    73.                 // Be lenient about moving outside of buttons  
    74.                 int slop = mTouchSlop;  
    75.                 if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
    76.                         (y < 0 - slop) || (y >= getHeight() + slop)) {  
    77.                     // Outside button  
    78.                     removeTapCallback();  
    79.                     if ((mPrivateFlags & PRESSED) != 0) {  
    80.                         // Remove any future long press/tap checks  
    81.                         removeLongPressCallback();  
    82.                         // Need to switch from pressed to not pressed  
    83.                         mPrivateFlags &= ~PRESSED;  
    84.                         refreshDrawableState();  
    85.                     }  
    86.                 }  
    87.                 break;  
    88.         }  
    89.         return true;  
    90.     }  
    91.     return false;  
    92. }

如果要分析到上述情况,我们还要借助于ViewGroup的分发事件的源码,马上供上哈。
dispatchTouchEvent


    1. public boolean dispatchTouchEvent(MotionEvent ev) {  
    2.     final int action = ev.getAction();  
    3.     final float xf = ev.getX();  
    4.     final float yf = ev.getY();  
    5.     final float scrolledXFloat = xf + mScrollX;  
    6.     final float scrolledYFloat = yf + mScrollY;  
    7.     final Rect frame = mTempRect;  
    8.     boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    9.     if (action == MotionEvent.ACTION_DOWN) {  
    10.         if (mMotionTarget != null) {  
    11.             mMotionTarget = null;  
    12.         }  
    13.         if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
    14.             ev.setAction(MotionEvent.ACTION_DOWN);  
    15.             final int scrolledXInt = (int) scrolledXFloat;  
    16.             final int scrolledYInt = (int) scrolledYFloat;  
    17.             final View[] children = mChildren;  
    18.             final int count = mChildrenCount;  
    19.             for (int i = count - 1; i >= 0; i--) {  
    20.                 final View child = children[i];  
    21.                 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
    22.                         || child.getAnimation() != null) {  
    23.                     child.getHitRect(frame);  
    24.                     if (frame.contains(scrolledXInt, scrolledYInt)) {  
    25.                         final float xc = scrolledXFloat - child.mLeft;  
    26.                         final float yc = scrolledYFloat - child.mTop;  
    27.                         ev.setLocation(xc, yc);  
    28.                         child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
    29.                         if (child.dispatchTouchEvent(ev))  {  
    30.                             mMotionTarget = child;  
    31.                             return true;  
    32.                         }  
    33.                     }  
    34.                 }  
    35.             }  
    36.         }  
    37.     }  
    38.     boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||  
    39.             (action == MotionEvent.ACTION_CANCEL);  
    40.     if (isUpOrCancel) {  
    41.         mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;  
    42.     }  
    43.     final View target = mMotionTarget;  
    44.     if (target == null) {  
    45.         ev.setLocation(xf, yf);  
    46.         if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
    47.             ev.setAction(MotionEvent.ACTION_CANCEL);  
    48.             mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
    49.         }  
    50.         return super.dispatchTouchEvent(ev);  
    51.     }  
    52.     if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
    53.         final float xc = scrolledXFloat - (float) target.mLeft;  
    54.         final float yc = scrolledYFloat - (float) target.mTop;  
    55.         mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
    56.         ev.setAction(MotionEvent.ACTION_CANCEL);  
    57.         ev.setLocation(xc, yc);  
    58.         if (!target.dispatchTouchEvent(ev)) {  
    59.         }  
    60.         mMotionTarget = null;  
    61.         return true;  
    62.     }  
    63.     if (isUpOrCancel) {  
    64.         mMotionTarget = null;  
    65.     }  
    66.     final float xc = scrolledXFloat - (float) target.mLeft;  
    67.     final float yc = scrolledYFloat - (float) target.mTop;  
    68.     ev.setLocation(xc, yc);  
    69.     if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {  
    70.         ev.setAction(MotionEvent.ACTION_CANCEL);  
    71.         target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  
    72.         mMotionTarget = null;  
    73.     }  
    74.     return target.dispatchTouchEvent(ev);  
    75. }  

如果我们重写View的dispatchTouchEvent并返回true,我们跟踪到ViewGroup中第29行代码,你看执行child.dispatchTouchEvent返回true后ViewGroup的该方法也直接return true了,接下来ACTION_UP其他事件类型,我们定位到代码74行,最终会执行target.dispatchTouchEvent,但view的dispatchTouchEvent返回true,说明事件被消费了,不会再进入到ViewGroup的onTouchEvent里。
如果我们重写View的dispatchTouchEvent并返回false或super,那么在ViewGroup代码29行里,我么看到它返回false还会继续走ViewGroup后续代码,target为null,执行super.dispatchTouchEvent方法(ViewGroup的父类是View)这里调用View的dispatchTouchEvent方法->内部调用onTouchEvent方法。
如果我们重写ViewGroup的dispatchTouchEvent方法,直接返回false,我特意弄了这样的场景,看了Activity的dispatchTouchEvent的源码,会发现会执行Activity的onTouchEvent方法

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

其他场景也是差不多的,都是可以套入代码中做分析的,这里要我继续分析也感觉无从入手,大家可以看看源码,找到关键代码分析分析场景。

附上别人分析的源码解析

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

推荐阅读更多精彩内容