Android View 事件分发机制源码详解(ViewGroup篇)

前言

我们在学习View的时候,不可避免会遇到事件的分发,而往往遇到的很多滑动冲突的问题都是由于处理事件分发时不恰当所造成的。因此,深入了解View事件分发机制的原理,对于我们来说是很有必要的。由于View事件分发机制是一个比较复杂的机制,因此笔者将写成两篇文章来详细讲述,分别是ViewGroup和View。因为我们平时所接触的View都不是单一的View,往往是由若干个ViewGroup组合而成,而事件的分发又是由ViewGroup传递到它的子View的,所以我们先从ViewGroup的事件分发说起。注意,以下源码取自安卓5.0(API 21)。

三个重要方法

public boolean dispatchTouchEvent(MotionEvent ev)

该方法用来进行事件的分发,即无论ViewGroup或者View的事件,都是从这个方法开始的。

public boolean onInterceptTouchEvent(MotionEvent ev)

在上一个方法内部调用,表示是否拦截当前事件,返回true表示拦截,如果拦截了事件,那么将不会分发给子View。比如说:ViewGroup拦截了这个事件,那么所有事件都由该ViewGroup处理,它内部的子View将不会获得事件的传递。(但是ViewGroup是默认不拦截事件的,这个下面会解释。)注意:View是没有这个方法的,也即是说,继承自View的一个子View不能重写该方法,也无需拦截事件,因为它下面没有View了,它要么处理事件要么不处理事件,所以最底层的子View不能拦截事件。

public boolean onTouchEvent(MotionEvent ev)

这个方法表示对事件进行处理,在dispatchTouchEvent方法内部调用,如果返回true表示消耗当前事件,如果返回false表示不消耗当前事件。

以上三个方法非常重要,贯穿整个View事件分发的流程,它们的关系可以用如下伪代码呈现:

public boolean dispatchTouchEvent(MotionEvent ev){
    boolean handle = false;
    if(onInterceptTouchEvent(ev)){
        handle = onTouchEvent(ev);
    }else{
        handle = child.dispatchTouchEvent(ev);
    }
    return handle;
}

由以上伪代码可得出如下结论:如果一个事件传递到了ViewGroup处,首先会判断当前ViewGroup是否要拦截事件,即调用onInterceptTouchEvent()方法;如果返回true,则表示ViewGroup拦截事件,那么ViewGroup就会调用自身的onTouchEvent来处理事件;如果返回false,表示ViewGroup不拦截事件,此时事件会分发到它的子View处,即调用子View的dispatchTouchEvent方法,如此反复直到事件被消耗掉。
接下来,我们将从源码的角度来分析整个ViewGroup事件分发的流程是怎样的。

从Activity到根ViewGroup

我们知道,事件产生于用户按下屏幕的一瞬间,事件生成后,经过一系列的过程来到我们的Activity层,那么事件是怎样从Activity传递到根ViewGroup的呢?由于这个问题不在本文的讨论范围,所以这里简单提一下:事件到达Activity时,会调用Activity#dispatchTouchEvent方法,在这个方法,会把事件传递给Window,然后Window把事件传递给DecorView,而DecorView是什么呢?它其实是一个根View,即根布局,我们所设置的布局是它的一个子View。最后再从DecorView传递给我们的根ViewGroup。
所以在Activity传递事件给ViwGroup的流程是这样的:Activity->Window->DecorView->ViewGroup

ViewGroup事件分发源码解析

接下来便是本文的重要,对ViewGroup#dispatchTouchEvent()方法源码进行解读,由于源码比较长,所以这里分段贴出。

1、对ACTION_DOWN事件初始化

首先看如下所示的源码:

    ...
    // Handle an initial down.
      if (actionMasked == MotionEvent.ACTION_DOWN) {
          // Throw away all previous state when starting a new touch gesture.
          // The framework may have dropped the up or cancel event for the previous gesture
          // due to an app switch, ANR, or some other state change.
          //这里把mFirstTouchTarget设置为null
          cancelAndClearTouchTargets(ev);
          resetTouchState();
      }

首先这里先判断事件是否为DOWN事件,如果是,则初始化,把mFirstTouchTarget置为null。由于一个完整的事件序列是以DOWN开始,以UP结束,所以如果是DOWN事件,那么说明是一个新的事件序列,所以需要初始化之前的状态。这里的mFirstTouchTarget非常重要,后面会说到当ViewGroup的子元素成功处理事件的时候,mFirstTouchTarget会指向子元素,这里要留意一下。

2、检查ViewGroup是否要拦截事件

接着我们往下看:

// Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {  // 1
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);    // 2
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    ...
    // Check for cancelation.
    final boolean canceled = resetCancelNextUpFlag(this)  || actionMasked == MotionEvent.ACTION_CANCEL;

以上代码主要判断ViewGroup是否要拦截事件。定义了一个布尔值intercept来记录是否要进行拦截,这在后面发挥很重要的作用。
①号代码处,首先执行了这个语句:if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null),也即是说,如果事件是DOWN或者mFirstTouchTatget值不为空的时候,才有可能执行②号代码,否则会直接跳过判断是否拦截。为什么要有这个判断呢?这里解释一下,比如说,子View消耗了ACTION_DOWN事件,然后这里可以由ViewGroup继续判断是否要拦截接下来的ACTION_MOVE事件之类的;又比如说,如果第一次DOWN事件最终不是由子View消耗掉的,那么显然mFirstTouchTarget将为null,所以也就不用判断了,直接把intercept设置为true,此后的事件都是由这个ViewGroup处理。
②号处调用了onInterceptTouchEvent()方法,那么我们可以跟进去看看这个onInterceptTouchEvent()做了什么。
2.1、ViewGroup#onInterceptTouchEvent()

public boolean onInterceptTouchEvent(MotionEvent ev) { 
    return false; 
}

可以看出,ViewGroup#onInterceptTouchEvent()方法是默认返回false的,即ViewGroup默认不拦截任何事件,如果想要让ViewGroup拦截事件,那么应该在自定义的ViewGroup中重写这个方法。
2.2、我们再看看原来的代码,会发现还有一个FLAG_DISALLOW_INTERCEPT标志位,这个标志位的作用是禁止ViewGroup拦截除了DOWN之外的事件,一般通过子View的requestDisallowInterceptTouchEvent来设置。
2.3、最后判断是否是CANCEL事件。

根据以上分析,这里小结一下:当ViewGroup要拦截事件的时候,那么后续的事件序列都将交给它处理,而不用再调用onInterceptTouchEvent()方法了,所以该方法并不是每次事件都会调用的。

3、对ACTION_DWON事件的特殊处理

返回ViewGroup#dispatchTouchEvent()源码,我们继续往下看。
接下来是一个If判断语句,内部还有若干if语句,以下先省略所有if体的内容,我们从大体上认识这块代码的作用:

TouchTarget newTouchTarget = null;  // 1
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
    ...// IF体1
    if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        ...// IF体2
    }
}

首先,在每一次调用这个方法的时候,会执行①号代码:在进行判断之前,已经把newTouchTarget和alreadyDispatchedToNewTouchTarget置为null了,这里尤其注意。
接着,判断if(!canceled && !intercepted),表示如果不是取消事件以及ViewGroup不进行拦截则进入IF体1,接着又是一个判断if (actionMasked == MotionEvent.ACTION_DOWN ...)这表示事件是否是ACTION_DOWN事件,如果是则进入IF体2,根据以上两个IF条件,事件是ACTION_DOWN以及ViewGroup不拦截,那么IF体2内部应该是把事件分发给子View了,我们展开IF体2,看看内部实现了什么:

if (actionMasked == MotionEvent.ACTION_DOWN
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    final int actionIndex = ev.getActionIndex(); // always 0 for down
    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
            : TouchTarget.ALL_POINTER_IDS;

        // Clean up earlier touch targets for this pointer id in case they
        // have become out of sync.
        removePointersFromTouchTargets(idBitsToAssign);

        final int childrenCount = mChildrenCount;
        if (newTouchTarget == null && childrenCount != 0) {
            ...// IF体3
        }
        if (newTouchTarget == null && mFirstTouchTarget != null) {
            ...
        }

可以看出,这里获取了childrenCount的值,表示该ViewGroup内部有多少个子View,如果有则进入IF体3,意思就是说,如果有子View就在IF体3里面开始遍历所有子View判断是否要把事件分发给子View。我们展开IF体3:

if (newTouchTarget == null && childrenCount != 0) {
    final float x = ev.getX(actionIndex);
    final float y = ev.getY(actionIndex);
    // Find a child that can receive the event.
    // Scan children from front to back.
    final ArrayList<View> preorderedList = buildOrderedChildList();
    final boolean customOrder = preorderedList == null
            && isChildrenDrawingOrderEnabled();
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) { // 1
        final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex);
        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }

        if (!canViewReceivePointerEvents(child)                
                || !isTransformedTouchPointInView(x, y, child, null)) {  // 2
            ev.setTargetAccessibilityFocus(false);
            continue;
        }

        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }

        resetCancelNextUpFlag(child);
        //把事件分发给子View
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 3
           // Child wants to receive touch within its bounds.
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);  // 4
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }

            // The accessibility focus didn't handle the event, so clear
            // the flag and do a normal dispatch to all children.
            ev.setTargetAccessibilityFocus(false);
        }
        if (preorderedList != null) preorderedList.clear();
}

代码也比较长,我们只关注重点部分。先看①处的代码,是一个for循环,这里表示对所有的子View进行循环遍历,由于以上判断了ViewGroup不对事件进行拦截,那么在这里就要对ViewGroup内部的子View进行遍历,一个个地找到能接受事件的子View,这里注意到它是倒序遍历的,即从最上层的子View开始往内层遍历,这也符合我们平常的习惯,因为一般来说我们对屏幕的触摸,肯定是希望最上层的View来响应的,而不是被覆盖这的底层的View来响应,否则这有悖于生活体验。然后②号代码是If语句,根据方法名字我们得知这个判断语句是判断触摸点位置是否在子View的范围内或者子View是否在播放动画,如果均不符合则continue,表示这个子View不符合条件,开始遍历下一个子View。接着③号代码,这里调用了dispatchTransformedTouchEvent()方法,这个方法有什么用呢?
3.1、我们看看这个方法,ViewGroup#dispatchTransformedTouchEvent():

...
final boolean handled;
if (child == null) {
    handled = super.dispatchTouchEvent(event);
    } else {
        handled = child.dispatchTouchEvent(event);
    }
    event.setAction(oldAction);
    return handled;
...

方法大体上是这样,做了适当删减,显然,当传递进来的的child不为null时,就会调用子View的dispatchTouchEvent(event)方法,表示把事件交给子View处理,也即是说,子Viwe符合所有条件的时候,事件就会在这里传递给了子View来处理,完成了ViewGroup到子View的事件传递,当事件处理完毕,就会返回一个布尔值handled,该值表示子View是否消耗了事件。怎样判断一个子View是否消耗了事件呢?如果说子View的onTouchEvent()返回true,那么就是消耗了事件。
3.2、在③号代码处判断子View是否消耗事件,如果消耗了事件那么最后便会执行到④号代码:addTouchTarget()。我们来看看这个方法:ViewGroup#addTouchTarget():

private TouchTarget addTouchTarget(View child, int pointerIdBits) {
        TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

可以看到,在这个方法里面,把mFirstTouchTarget指向了child,同时把newTouchTarget也指向child,也即是说,如果子View消耗掉了事件,那么mFirstTouchTarget就会指向子View。在执行完④号代码后,直接break了,表示跳出了循环,因为已经找到了处理事件的子View,所以无需继续遍历了。

小结:整一个if(!canceled && !intercepted){ ... }代码块所做的工作就是对ACTION_DOWN事件的特殊处理。因为ACTION_DOWN事件是一个事件序列的开始,所以我们要先找到能够处理这个事件序列的一个子View,如果一个子View能够消耗事件,那么mFirstTouchTarget会指向子View,如果所有的子View都不能消耗事件,那么mFirstTouchTarget将为null

4、对除了ACTION_DOWN之外的其他事件的处理

第3点是对ACTION_DOWN事件的处理,那么不是ACTION_DOWN的事件将从以下开始处理:

// Dispatch to touch targets.
if (mFirstTouchTarget == null) { 
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS); // 1
    } else {
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it.  Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
            } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) { // 2
                handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
    ...
    return handled;
}

首先是一个if判断语句,判断mFirstTouchTarget是否为Null,如果为null,那么调用①处的代码:dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS),这个方法上面出现过了(见3.1),这里第三个参数为null,那么我们看方法体,会执行super.dispatchTouchEvent(event);这里意思是说,如果找不到子View来处理事件,那么最后会交由ViewGroup来处理事件。接着,如果在上面已经找到一个子View来消耗事件了,那么这里的mFirstTouchTarget不为空,接着会往下执行。
接着有一个if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget)判断,这里就是区分了ACTION_DOWN事件和别的事件,因为在第3.2点的分析我们知道,如果子View消耗了ACTION_DOWN事件,那么alreadyDispatchedToNewTouchTarget和newTouchTarget已经有值了,所以就直接置handled为true并返回;那么如果alreadyDispatchedToNewTouchTarget和newTouchTarget值为null,那么就不是ACTION_DOWN事件,即是ACTION_MOVE、ACTION_UP等别的事件的话,就会调用②号代码,把这些事件分发给子View。

小结:最后这段代码处理除了ACTION_DOWN事件之外的其他事件,如果ViewGroup拦截了事件或者所有子View均不消耗事件那么在这里交由ViewGroup处理事件;如果有子View已经消耗了ACTION_DOWN事件,那么在这里继续把其他事件分发给子View处理。

至此,关于ViewGroup的事件分发机制源码已经分析完毕。下面给出一幅流程图来描述一下以上所分析的内容:


ViewGroup事件分发流程

总结

1、ACTION_DOWN事件为一个事件序列的开始,中间有若干个ACTION_MOVE,最后以ACTION_UP结束。
2、ViewGroup默认不拦截任何事件,所以事件能正常分发到子View处(如果子View符合条件的话),如果没有合适的子View或者子View不消耗ACTION_DOWN事件,那么接着事件会交由ViewGroup处理,并且同一事件序列之后的事件不会再分发给子View了。如果ViewGroup的onTouchEvent也返回false,即ViewGroup也不消耗事件的话,那么最后事件会交由Activity处理。即:逐层分发事件下去,如果都没有处理事件的View,那么事件会逐层向上返回。
3、如果某一个View拦截了事件,那么同一个事件序列的其他所有事件都会交由这个View处理,此时不再调用View(ViewGroup)的onIntercept()方法去询问是否要拦截了。

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

推荐阅读更多精彩内容