Android 源碼分析 - 事件分發(fā)機制

??之前分析了一下Android中的消息傳遞機制,不知道對各位有沒有幫助!哈哈,別怪我寫的太垃圾了......也不要說的太多的廢話了,直接進入今天的主題--Android 事件分發(fā)機制婶溯。還是那樣,文章如有錯誤,請各位指正迄委,本文參考資料:
??1.任玉剛老師的《Android 開發(fā)藝術探索》
??2.徐宜生老師的《Android 群英傳》
??注意褐筛,本文的所有代碼都是 API 26,如果是其他的版本叙身,會做特別說明渔扎!

1.概述

??我們還是繼承一下《Android 消息處理機制》的格式,先來概述一下今天的內容信轿,假裝符合面向對象的繼承特性晃痴。。财忽。
??在事件傳遞機制中倘核,必須講解的三個方法:

??1.public boolean dispatchTouchEvent(MotionEvent ev)方法,這個方法作用主要是用來分發(fā)事件即彪。也就是說紧唱,當一個事件傳遞當前View的dispatchTouchEvent方法里面,這個方法可以決定將事件分發(fā)到哪里去隶校,這里的分發(fā)到哪里去表示有兩個意思:1.將事件分發(fā)到子View(如果有子View的話)漏益;2.將事件分發(fā)到分發(fā)到自己的onTouchEvent方法里面去消耗。
??2.public boolean onInterceptTouchEvent(MotionEvent ev)方法惠况,這個方法的作用是用來決定當前的View或者ViewGroup是否攔截這個事件遭庶,如果返回true的話,那么就表示攔截稠屠;反之,表示不攔截翎苫。前排預警一下权埠,這個方法有很多的坑,不是返回一個true或者false那么簡單煎谍。
??3.public boolean onTouchEvent(MotionEvent event)方法攘蔽,這個方法是具體消耗事件的方法,如果返回true的話呐粘,表示當前的View已經將這個事件消耗了满俗。

??可能大家看我寫了這些,還是覺得一臉懵逼作岖。這三個方法的意思大家都懂唆垃,說這些有什么用。大哥痘儡,不要急辕万,我們來慢慢的分析。
??當前一個事件發(fā)生了,事件傳遞的流程是從上層依次傳遞到下層渐尿,直到這個事件被處理醉途,例如:



??上圖中,當在事件發(fā)生點發(fā)生了事件砖茸,它的傳遞順序是:ViewGroupA ->ViewGroupB ->View隘擎。然后我們在結合上面的三個方法來更加形象的展示一下,事件分發(fā)的順序:



??這里從圖中可以看出來凉夯,事件是從ViewGroupA開始的嵌屎,先調用A的dispatchTouchEvent方法,進行分發(fā)恍涂,同時還會調用A的onInterceptTouchEvent方法宝惰,如果onInterceptTouchEvent方法返回的是false,表示ViewGroupA不攔截此事件再沧,于是將事件傳遞給ViewGroupB尼夺,ViewGroupB也進行跟ViewGroupA一樣的操作。如果ViewGroupB也不進行攔截的話炒瘸,那么首先就會傳遞到View的dispatchTouchEvent方法淤堵,由于View再沒有子View了,所以不能進行向下分發(fā)顷扩,所以只能傳遞到View的onTouchEvent方法里面來拐邪。如果View消耗了這個事件的話,那么這個事件傳遞的流程就在這里結束隘截,不會繼續(xù)將事件傳到ViewGroupB的onTouchEvent方法里面去扎阶;反之如果View不消耗這個事件的話,那么就繼續(xù)往上傳遞婶芭。
??上面只分析了ViewGroup不對事件進行攔截的情況东臀,下面來分析一下當一個ViewGroup攔截了事件的情況。例如:

??一旦犀农,ViewGroupA對事件進行攔截惰赋,直接將事件傳遞給ViewGroupA的onTouchEvent方法里面去。
??這個大的流程差不多就是這樣的呵哨,可能中間有非常多的細節(jié)沒有提及到赁濒,但是不急,待會的源碼分析有你們好受的C虾Α>苎住!哈哈纹坐,開玩笑枝冀!

2.ViewGroup的事件分發(fā)

(1).DecorView

??當我們用手指在屏幕點擊時舞丛,事件首先被傳遞到Activity的dispatchTouchEvent方法。對的哦果漾!你沒有看錯球切,Activity也有dispatchTouchEvent方法。我們來看看Activity的dispatchTouchEvent方法代碼:

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

??可見绒障,當Activity的dispatchTouchEvent方法接收到了一個事件之后吨凑,Activity會將這個傳遞到Window里面去,我們再去看看:

public abstract boolean superDispatchTouchEvent(MotionEvent event);

??哦豁户辱,我們發(fā)現superDispatchTouchEvent所在的Window類是一個抽象類鸵钝,怎么辦?不急庐镐,在Window類解釋中恩商,google爸爸給我們這么說的(代碼根據 api 26):

The only existing implementation of this abstract class is
android.view.PhoneWindow, which you should instantiate when needing a
Window.

??這里說的是,Window抽象類的唯一實現類在是android.view.PhoneWindow必逆。然后我們到PhoneWindow里面去看看相應的方法:

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

??好嘛怠堪,又繼續(xù)跳,然后我們就到了DecorView類的superDispatchTouchEvent方法里面來了名眉。

    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

DecorView又是什么鬼粟矿?DecorView其實我們界面的頂級容器,也就是我們視圖樹的根损拢,是被添加到Window的陌粹。而DecorView作為頂級View,一般情況下福压,它內部會類似于LinearLayout的豎直布局掏秩,在這個布局里面有上下兩個部分,上面是標題欄隧膏,下面是Content View部分哗讥,在Activity 的setContView所設置的布局文件就是添加到Content View的部分。如圖:



??通常來說胞枕,我們可以通過如下代碼來我們自己設置的ContentView對象

        ViewGroup viewGroup = getWindow().getDecorView().findViewById(android.R.id.content);

??從這里,我們知道DecorView肯定是一個ViewGroup對象魏宽,我們繼續(xù)點擊dispatchTouchEvent方法腐泻,發(fā)現進入到了ViewGroup的dispatchTouchEvent方法里面來了。
??好嘛队询,費了半天的勁派桩,我們終于看到了重頭戲了。好了好了蚌斩,我們整裝待發(fā)铆惑,準備好好的來看一下這個方法!不過我們先來總結,我們獲取了哪些信息:

??1.一個事件首先會被傳遞到Activity的dispatchTouchEvent方法里面员魏,然后最終會傳遞DecorView中去丑蛤,最后通過DecorView調用ViewGroup的dispatchTouchEvent方法來進行事件的分發(fā)。
??2.DecorView是一個Activity的根本局撕阎,實際上他也是一個ViewGroup受裹。

(2).ViewGroup對View事件的分發(fā)

??由于dispatchTouchEvent方法源代碼太多了,所以我就不像消息機制那篇文章貼出完整的代碼虏束,在這里知識貼出部分代碼來進行理解棉饶。
??首先,我們來看看這段代碼:

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    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當前的ViewGroup是否需要攔截當前的事件照藻。我們發(fā)現在這段代碼里面發(fā)現了另一個比較眼熟的方法onInterceptTouchEvent方法。從代碼中汗侵,我們可以看出幸缕,ViewGroup判斷一個事件是否需要判斷實在dispatchTouchEvent方法里面對方法進行調用。
??然后我們再看看調用onInterceptTouchEvent方法的條件晃择。首先冀值,action為ACTION_DOWN的話,需要判斷當前的是否攔截宫屠,這個非常好理解列疗。但是mFirstTouchTarget是什么什么意思?實際上呢浪蹂,這個從后面的代碼邏輯中可以看出來抵栈,當ViewGroup的子元素成功處理一個事件的時候,mFirstTouchTarget會被賦值并指向該子元素坤次。換一句話說古劲,當ViewGroup不攔截事件,將事件交由給子元素來處理時缰猴,mFirstTouchTarget就不為null了产艾。也就是說,當事件序列的開始--ACTION_DOWN來到時滑绒,這時候mFirstTouchTarget是為null(因為這是第一次來闷堡,所以事件還沒有傳遞給它的子元素),如果此時ViewGroup在onInterceptTouchEvent返回為true的話疑故,表示攔截這個事件序列杠览,然后后面的ACTION_MOVE和ACTION_UP來到時,由于此時調用onInterceptTouchEvent方法的條件不符合纵势,所以onInterceptTouchEvent不會再被調用踱阿。為什么這里調用onInterceptTouchEvent方法的條件不符合呢管钳,因為第一次的down事件被ViewGroup攔截了,從而導致down事件沒有被傳遞到子View软舌,所以mFirstTouchTarget肯定為null才漆,當ACTION_MOVE和ACTION_UP兩個事件來到,actionMasked == MotionEvent.ACTION_DOW || mFirstTouchTarget != null肯定為false的葫隙!
??從而栽烂,我們從這段里面得到一個結論,一旦一個ViewGroup在onInterceptTouchEvent方法里面對ACTION_DOWN事件進行攔截恋脚,屬于同一個事件序列的后續(xù)事件也會被攔截腺办,同時onInterceptTouchEvent方法只會被調用一次,也就是對ACTION_DOWN進行攔截的那一次糟描!
??說到這里怀喉,那么有沒有辦法對其他事件進行需求性的攔截呢?有的船响,這個問題躬拢,我們后續(xù)再講!現在就講的話见间,就不能顯示我牛逼了聊闯!哈哈,開玩笑的米诉,應該時時刻刻記住自己就是一個菜雞菱蔬!
??在剛剛的那段代碼中,我們還發(fā)現有這個判斷

                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                  ......
                }

??其中史侣,我們需要關注的是FLAG_DISALLOW_INTERCEPT 標記位拴泌,這個標記位是通過ViewGroup里面的requestDisallowInterceptTouchEvent方法來設置的,一般用于子View惊橱。一旦FLAG_DISALLOW_INTERCEPT被設置了蚪腐,也就是說,我們在子View里面調用父布局的requestDisallowInterceptTouchEvent方法税朴,那么ViewGroup將無法攔截除ACTION_DOWN以外的其他點擊事件回季。
??這里為什么時候是ACTION_DOWN以外的點擊事件呢?這是因為正林,ACTION_DOWN事件會重置FLAG_DISALLOW_INTERCEPT標記位茧跋,導致子View設置的這個標記位無效。我們來看看代碼:

            // 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.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

??從dispatchTouchEvent的代碼看來卓囚,上面這段代碼在我們之前那段代碼的前面,所以在ViewGroup在判斷事件是否需要攔截之前诅病,就會重置FLAG_DISALLOW_INTERCEPT哪亿,從而導致我們的子View調用requestDisallowInterceptTouchEvent方法失效粥烁!
??經過上面的代碼,如果ViewGroup不對事件進行攔截蝇棉,那么就會將這個事件分發(fā)到能夠接收到這個事件的子View讨阻。

                    final int childrenCount = mChildrenCount;
                    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 = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, 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)) {
                                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);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 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);
                                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();
                    }

??根據任玉剛老師在《Android 開發(fā)藝術探索》中對這段代碼的解釋,一個子View是否能夠接收到點擊事件主要由兩點來衡量:子View是否是否在播放動畫和點擊事件是否落在子View的的區(qū)域內篡殷。如果這兩個事件能夠滿足的話钝吮,那么事件就會交給它來處理厘贼。
??這里將會詳細的講解一下漆羔,事件到底是怎么傳遞到子View。ViewGroup是通過dispatchTransformedTouchEvent來將事件分發(fā)到子View的验靡!

                           resetCancelNextUpFlag(child);
                           if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 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);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

??這個是事件分發(fā)代碼劲弦,其中耳标,我們會發(fā)現,如果當前子View會消耗這個事件邑跪,也就是說dispatchTransformedTouchEvent方法返回true次坡,那么將會將當前的View添加target的鏈表,而我們說的mFirstTouchTarget就是指向這個鏈表的頭画畅!這個就相當于完成的分發(fā)了嗎砸琅?
??NO!NO轴踱!沒有那么的簡單症脂,我們會發(fā)現前面有段代碼:

                            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;
                            }

??如果當遍歷第一個子View的時候,這里的newTouchTarget就會返回的不是null寇僧,豈不是下面的dispatchTransformedTouchEvent根本就來不及調用摊腋。像這種情況,應該怎么辦嘁傀?我們發(fā)現兴蒸,只要在這段代碼里面break,最后會執(zhí)行這段代碼:

              // 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)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }

??如果說细办,之前已經將事件分發(fā)下去了橙凳,alreadyDispatchedToNewTouchTarget && target == newTouchTarget這個條件肯定為true。所以笑撞,如果在dispatchTransformedTouchEvent方法之前break岛啸,從而導致跳出循環(huán),alreadyDispatchedToNewTouchTarget肯定是為false的茴肥,因為這個變量在調用了dispatchTransformedTouchEvent方法之后會被置為true坚踩。這行代碼在之前循環(huán)遍歷子View里面。

                                alreadyDispatchedToNewTouchTarget = true;

??所以瓤狐,只要在之前沒有調用dispatchTransformedTouchEvent方法就break瞬铸,肯定會進入else的代碼里面∨希現在的關鍵是理解resetCancelNextUpFlag是什么意思?我們先來看看這個方法:

    /**
     * Resets the cancel next up flag.
     * Returns true if the flag was previously set.
     */
    private static boolean resetCancelNextUpFlag(@NonNull View view) {
        if ((view.mPrivateFlags & PFLAG_CANCEL_NEXT_UP_EVENT) != 0) {
            view.mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT;
            return true;
        }
        return false;
    }

??這里嗓节,我是看不懂代碼的荧缘。但是可以從方法的注釋來看他的意思,這個方法作用是拦宣,如果之前這個View的flag被重置過截粗,那么就返回true,反之返回false鸵隧。簡而言之绸罗,相對于同一個View來說的話,如果第一次調用這個方法的話掰派,返回的是false从诲;反之則返回的true。
??所以靡羡,在這里系洛,我們就可以理解到了,只要是在調用dispatchTransformedTouchEvent方法之前就break的話略步,resetCancelNextUpFlag返回的肯定是true描扯。這個是為什么呢?因為只要getTouchTarget返回的不是null趟薄,表示的意思就是當前的View已經被添加到了mFirstTouchTarget所在的鏈表中绽诚,也就是說在當前這個事件之前,有可能有個事件傳遞到當前的這個View杭煎,并且執(zhí)行了恩够,所以被添加到鏈表中的。因為這段代碼在dispatchTransformedTouchEvent方法為的true才執(zhí)行的:

                                newTouchTarget = addTouchTarget(child, idBitsToAssign);

??從而得知羡铲,只要newTouchTarget不為null的話蜂桶,resetCancelNextUpFlag方法返回的肯定是true。而這里cancelChild變量還由intercepted變量來決定也切,這個待會再細講扑媚,因為變量太特么的坑了!
??這樣我們就能得知雷恃,如果一個View對一個事件序列的事件進行處理疆股,但是后續(xù)如果有一個事件不會處理的話,那這個View會收到一個ACTION_CANCEL類型的事件倒槐!
??以上就是ViewGroup對子View的事件分發(fā)大概的解釋旬痹,不敢說特別詳細!下面來總結一下:

??1.當一個事件傳遞到ViewGroup里面的話,首先會根據事件類型或者mFirstTouchTarget 是否null來判斷是否調用onInterceptTouchEvent方法唱凯,當然這個過程中還要考慮FLAG_DISALLOW_INTERCEPT標記位羡忘。簡而言之,當前DOWN事件來到時磕昼,ViewGroup首先詢問onInterceptTouchEvent是否需要攔截。這里需要注意的是节猿,如果有子View處理這個事件了票从,會導致mFirstTouchTarget不為null,從而可以形成一種父ViewGroup可以攔截非ACTION_DOWN事件的局面滨嘱!還需要注意的是峰鄙,整個詢問攔截的過程還需要考慮子View調用requestDisallowInterceptTouchEvent方法來請求不要我的事件!哎太雨,感覺子View好可憐吟榴,動不動就會ViewGroup折磨!D野狻7苑!
??2.當ViewGroup不對事件進行攔截時锥咸,ViewGroup會將相應的事件傳遞到子View里面狭瞎!
??3.如果整個事件序列的ACTION_DOWN沒有子View來處理,最終會傳遞到ViewGroup方法里面處理搏予。因為當mFirstTouchTarget為null時熊锭,會調用ViewGroup自己的onTouchEvent方法!但是這里需要的注意雪侥,整個事件序列碗殷,除了ACTION_DOWN會傳遞到子View的onTouchEvent之外,后續(xù)的事件都只會到達子View的dispatchTouchEvent方法速缨,不會到達onTouchEvent方法里面锌妻。這個原因待會再講View的方法來解釋!
??4.當一個事件序列中間(記住這里是中間鸟廓,開始的情況參考 3 从祝,結尾可以參考這個)的某個事件沒有子View來處理的話,那么在TouchTarget鏈上的所有View都會收到一個ACTION_CANCEL事件引谜,并且會將這些子View從鏈上recycle掉牍陌。從而得知,只要一個子View不對一個事件進行處理员咽,那么在這個事件序列上的其他類型的事件都不會交給它來處理毒涧。
??5.如果一個事件序列從ACTION_DOWN開始,就被攔截了贝室。這個事件序列的所有事件不會在傳遞的到子View契讲。因為ACTION_DOWN來的時候仿吞,mFirstTouchTarget本來為空,由于onInterceptTouchEvent方法返回true捡偏,所以導致if (!canceled && !intercepted)語句進入不了唤冈,進而導致mFirstTouchTarget在整個事件序列都為空,所以一直在調用這個代碼银伟,從而導致整個事件序列的都不能傳遞下去:
            // 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);
            }

(3).ViewGroup對View事件的攔截

??還記得我在前面挖的兩個坑嗎你虹?第一個是在概述里面說的,前排預警一下彤避,onInterceptTouchEvent方法有很多的坑傅物,不是返回一個true或者false那么簡單;第二個是在(2)里面的琉预,有沒有辦法對其他事件進行需求性的攔截?
??到這里來看看董饰,這兩個坑好像就像是一個問題,都是關于onInterceptTouchEvent方法圆米。
??其實在之前我們簡單的介紹onInterceptTouchEvent方法的作用和使用卒暂,但是只是粗略的介紹,在這里將稍微詳細的解釋榨咐。

A.onInterceptTouchEvent方法的調用時機

??先說明一下介却,這里先不考慮FLAG_DISALLOW_INTERCEPT標記位的影響。
??在dispatchTouchEvent方法块茁,我們知道齿坷,當一個事件序列的開始,也就是ACTION_DOWN來到時数焊,會調用onInterceptTouchEvent方法來詢問是否需要攔截此事件序列永淌!這種情況下,應該非常容易的理解佩耳!
??另一種情況便是mFirstTouchTarget 不為null的時候遂蛀。那mFirstTouchTarget不為null究竟是什么情況呢?我們從dispatchTouchEvent方法里面可以看出來干厚,當一個事件被子View消耗了李滴,那么會將當前的這個View封裝成一個Target對象,然后添加到一個鏈表的鏈頭蛮瞄,而mFirstTouchTarget則是指向這個鏈表的鏈頭所坯。也就是說,當前mFirstTouchTarget不為null的時候挂捅,表示在同一個事件序列芹助,當前事件前面的事件被子View消耗掉了!mFirstTouchTarget不為null表示的就是這個意思!
??如上的情況下状土,我們可以形象的解釋无蜂,將你的媽媽比喻為ViewGroup,而子View當成你蒙谓,你開始打游戲表示一個事件序列的開始斥季。如上的情況就是這樣的,你開始打游戲的時候彼乌,你媽媽沒有攔截你的行為泻肯,因此你可以順利的打開游戲,開心的吃雞慰照,如果中途你媽媽叫你去打醬油,可是此時你正在決賽圈說你沒空琉朽,你媽媽就生氣了毒租,把你的網線拔了,相當于是攔截你的行為箱叁,導致你的吃雞夢想泡湯了墅垮!這個比喻能夠說明上面的情況,也就是說耕漱,當子View在ACTION_MOVE的非常開心的時候算色,父ViewGroup有資格讓子View不開心!哈哈哈哈C弧T置巍!
??上面的解釋就是妓笙,當不考慮FLAG_DISALLOW_INTERCEPT標記位時若河,onInterceptTouchEvent方法的調用時機。
??那么我們現在來考慮FLAG_DISALLOW_INTERCEPT標記位寞宫。
??首先說一下萧福,標記位對事件序列的開始事件--ACTION_DOWN無效的!只有當子View在ACTION_MOVE的非常開心的時候,才有資格向父ViewGroup申請不要攔截我的事件辈赋!這個請求是有效的鲫忍!
??如上便是onInterceptTouchEvent方法的調用時機。這里對onInterceptTouchEvent方法的調用時機做一個簡單的總結:

??1.ViewGroup有資格一開始ACTION_DOWN,即使子View調用requestDisallowInterceptTouchEvent方法來申請不攔截也沒有用的钥屈。一旦攔截了悟民,整個事件序列就都失去了向下傳遞的能力,直接進入ViewGroup的onTouchEvent方法去處理焕蹄。
??2.View有資格不攔截ACTION_DOWN,而是攔截ACTION_MOVE和ACTION_UP事件逾雄。還是跟攔截ACTION_DOWN的情況比較類似,但是還是有點區(qū)別!

B.onInterceptTouchEvent方法對非ACTION_DOWN的事件進行攔截

??如果ViewGroup只能對ACTION_DOWN進行攔截的話鸦泳,這樣也太暴力了银锻!因為這樣會導致整個事件序列都只能被傳遞ViewGroup。所以做鹰,ViewGroup對ACTION_MOVE和ACTION_UP事件還是有必要的击纬。其實這種需求很好的實現,例如:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {

            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP: {
                return true;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }

??是不是瞬間來了一句臥槽钾麸!這么簡單更振。對!就是這么簡單饭尝,但是簡單的背后大有玄機所在了肯腕!例如:
??這是ViewGroup的代碼:

public class MyViewGroup extends LinearLayout {
    public MyViewGroup(Context context) {
        super(context);
    }
    public MyViewGroup(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public MyViewGroup(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.i("pby123", "1");
        switch (ev.getAction()) {
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP: {
                return true;
            }
        }
        return super.onInterceptTouchEvent(ev);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("pby123", "2");
        return true;
    }
}

??這是View的代碼:

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }
    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }
    public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.i("pby123","3");
        return true;
    }
}

??此時,我對View進行ACTION_DOWN和ACTION_UP的事件產生钥平,然后打印的log卻是這樣的:



??我們發(fā)現实撒,當ACTION_DOWN事件產生時,傳遞到子View很正常涉瘾,但是我們對ACTION_UP事件進行攔截的知态,為什么還是會傳遞子View里面去呢?是不是onInterceptTouchEvent對ACTION_UP事件是無效的呢立叛?瞎猜是沒有用的负敏,此時我們來看看dispatchTouchEvent的代碼。(其實這種情況秘蛇,我在分析dispatchTouchEvent的時候已經非常小聲的說過了哦F渥觥!M妗J痢)

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    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;
            }

??上面這段代碼,我們當ACTION_UP事件來到秽浇,由于mFirstTouchTarget不為null浮庐,最終會調用onInterceptTouchEvent來進行詢問是否需要攔截,我們在onInterceptTouchEvent方法里面返回的是true柬焕,所以在intercepted肯定為true审残。然后代碼往下走,最終進入這段代碼:

                // 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)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }

??是不是感覺又回來了斑举?又來分析這個方法了搅轿,我們知道cancelChild返回的肯定是true,所以dispatchTransformedTouchEvent這一步會給子View發(fā)送一個ACTION_CANCEL事件,然后就行將這個target回收了富玷。
??到這里我們知道了璧坟,第二次ACTION_UP事件根本沒有傳遞到子View里面既穆,傳遞過去的是一個ACTION_CANCEL事件!大家如果不信的話雀鹃,可以去試試幻工!
??這里我們不滿足只是ACTION_DOWN和ACTION_MOVE事件,我們使其也產生ACTION_MOVE事件黎茎。我們來看看這種情況下的log日志:



??哈哈沒錯囊颅,第二次之所以將事件傳遞給子View,那么是因為ACTION_MOVE事件被攔截了傅瞻,從而傳遞過去一個ACTION_CANCEL事件過去踢代,而不是ACTION_MOVE事件。
??好了嗅骄,onInterceptTouchEvent方法分析的差不多了胳挎,現在該解決在留的兩個問題。首先溺森,onInterceptTouchEvent方法坑在于onInterceptTouchEvent方法的調用時機串远,待會再總結里面會總結一下,這里就不再多余的說了儿惫;其實onInterceptTouchEvent的坑還有就是ACTION_DOWN和ACTION_UP,誰又能想到傳遞子View的根本不是ACTION_UP事件呢?伸但。其次肾请,就是對非ACTION_DOWN的攔截,假設我們從ACTION_MOVE開始攔截更胖,需要注意的是第一個ACTION_MOVE事件是不會傳遞子View铛铁,也不會傳遞到ViewGroup,只有經過這次的處理却妨,后面的事件ViewGroup才算是能夠接收到饵逐!
??又該對上面的知識點做一個總結:

??1.一個ViewGroup的調用時機是:1.ACTION_DOWN的來到;2.事件序列中間的ACTION_MOVE事件來到彪标,需要注意是這樣情況下倍权,必須保證在同一個事件序列中, 當前事件的前面的事件有被子View消耗過的捞烟,也就是,mFirstTouchTarget不能為null默辨。
??2.調用時機還需要的是:如果一個事件被攔截了苍息,在這個事件序列里面缩幸,onInterceptTouchEvent不會再被調用。
??3.如果我們想要對非ACTION_DOWN事件進行攔截表谊,必須保證同一個事件序列的前面所有事件都子View執(zhí)行了。
??4.對非ACTION_DOWN事件進行攔截铃肯,是對下次的事件進行攔截患亿,當前的事件會被變?yōu)锳CTION_CANCEL傳遞到子View中去押逼。

3.View對事件的處理

??由于View是沒有子View的挑格,所以View不能繼續(xù)對事件繼續(xù)的分發(fā)。相較于ViewGroup雾消,View少了一個onInterceptTouchEvent方法挫望。所以說媳板,如果一個事件到達View蛉幸,肯定會處理,注意的處理表達意思是:它可以調用onTouch或者onTouchEvent方法來處理提陶,或者不處理隙笆,最后這個事件被它的ViewGroup分發(fā)ViewGroup自己進行處理仲器。
??所以仰冠,View對事件的處理分成兩種情況:一種是自己處理洋只;一種是不處理,父ViewGroup會自己處理肢扯,處理的代碼也是調用View的蔚晨,因為ViewGroup繼承于View铭腕。我們一個一個的分析。

(1).View事件處理流程

??事件首先會被傳遞View的dispatchTouchEvent方法里面浩考,我們來看看析孽,不要怕哦袜瞬!View的dispatchTouchEvent代碼非常的簡單吞滞。

    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        return result;
    }

??以上的代碼,我刪除了部分我認為不重要的代碼赴精,是不是非常的簡單绞幌?其實意思也非常的簡單莲蜘,首先如果設置了OnTouchListener監(jiān)聽的話票渠,onTouch方法是否消耗該事件问顷,如果消耗的話禀梳,事件傳遞就結束了算途;反之嘴瓤,則將事件傳遞到onTouchEvent方法里面去廓脆。
??從這里胆胰,我們可以看出蜀涨,onTouch的優(yōu)先級比onTouchEvent的高!
??我們再來看看onTouchEvent厚柳,由于onTouchEvent方法代碼太長了别垮,這里只看部分:

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }

??從以上的代碼中碳想,我們看出來胧奔,如果一個View的enable屬性是Disable的話,它仍然能夠消耗事件胳泉,只是不會做出任何的反應而已扇商,正如注釋所說的案铺。
??我們繼續(xù)往下看红且,我們發(fā)現switch-case語句被這段代碼包裹:

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
  ......
}

??而clickable是什么呢?我們來看看:

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE

??也就是說嗤放,只要CLICKABLE次酌、LONG_CLICKABLE或者CONTEXT_CLICKABLE其中一個為true的話岳服,就會對事件進行消耗吊宋!
??在switch-case里面璃搜,我們不看ACTION_DOWN和ACTION_MOVE事件鳞上,我們來看看ACTION_UP事件有個非常眼熟的東西:

                                if (!post(mPerformClick)) {
                                    performClick();
                                }

??我們再來看看performClick方法里面有什么東西呢

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

??哎呀唾糯,這個不是我們喜聞樂見的OnClickListener嗎移怯?開不開心这难,激不激動?哈哈哈Q慵选L侨ā星澳!
??從這里禁偎,我們可以得出,在一個View中笆檀,onTouch的優(yōu)先級是最高的酗洒,其次是onTouchEvent樱衷,最后才是onClick方法矩桂!

(2).ViewGroup對事件的處理

??ViewGroup對事件的處理在dispatchTransformedTouchEvent方法里面進行的侄榴,由于dispatchTransformedTouchEvent方法的代碼比較長牲蜀,這里只看他是怎么調用onTouchEvent方法:

            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);

                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }

??我們上面的代碼中發(fā)現調用super.dispatchTouchEvent(event)方法涣达,從而完成了自己對事件的處理度苔,事件處理的流程跟View對事件的處理流程比較相似寇窑!

4.總結

??終于寫完了甩骏,我們還是來對我們所有的內容做一個總結:

??1.如果一個事件序列的ACTION_DOWN事件被ViewGroup攔截饮笛,事件序列的后續(xù)事件不再會傳遞到子View论熙,即使在ACTON_MOVE時,子View調用requestDisallowInterceptTouchEvent方法沒用的媒役。同時酣衷,在這種情況下鸥诽,onInterceptTouchEvent方法只會被調用一次牡借。
??2.在一個事件序列中钠龙,如果ACTION_DOWN事件沒有被攔截碴里,并且ACTION_DOWN事件被子View消耗了咬腋,則ViewGroup有資格去攔截事件序列剩下的事件。
??3.在一個事件序列中寇壳,如果子View不處理ACTION_DOWN事件壳炎,此事件序列的后續(xù)事件不會在傳遞到子View匿辩。記住铲球,連子View的dispatchTouchEvent方法都不會到達!
??4.在一個事件序列中宾肺,如果子View開始處理一些事件锨用,事件中途突然被ViewGroup攔截增拥,被攔截的當前事件會轉換成為ACTION_CANCEL事件傳遞到子View中去掌栅,ViewGroup真正獲取事件從下一次事件(不是下一次事件序列猾封!)開始。
??5.在View中齐莲,優(yōu)先級最高的onTouch,其次是onTouchEvent芒填,最好是onClick氢烘。onClick方法在ACTION_UP時刻回調播玖!
??6.如果一個事件最后連ViewGroup都不處理的話蜀踏,最終回到Activity的onTouchEvent方法里面來果覆。
??7.當View的enable為Disable時局待,也會消耗事件钳榨,只是不會做出任何的反應薛耻。同時只要CLICKABLE饼齿、LONG_CLICKABLE或者CONTEXT_CLICKABLE其中一個為true的話缕溉,就會對事件進行消耗倒淫!
??8.View的requestDisallowInterceptTouchEvent方法只是在View消耗ACTION_DOWN事件的前提才有效敌土!
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末兴枯,一起剝皮案震驚了整個濱河市财剖,隨后出現的幾起案子躺坟,更是在濱河造成了極大的恐慌咪橙,老刑警劉巖美侦,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異具壮,居然都是意外死亡棺妓,警方通過查閱死者的電腦和手機涧郊,發(fā)現死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來批旺,“玉大人汽煮,你說我怎么就攤上這事暇赤。” “怎么了宵凌?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵鞋囊,是天一觀的道長。 經常有香客問我瞎惫,道長溜腐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任瓜喇,我火速辦了婚禮,結果婚禮上乘寒,老公的妹妹穿的比我還像新娘望众。我一直安慰自己,他們只是感情好肃续,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布黍檩。 她就那樣靜靜地躺著,像睡著了一般始锚。 火紅的嫁衣襯著肌膚如雪刽酱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天瞧捌,我揣著相機與錄音棵里,去河邊找鬼润文。 笑死,一個胖子當著我的面吹牛殿怜,可吹牛的內容都是我干的典蝌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼头谜,長吁一口氣:“原來是場噩夢啊……” “哼骏掀!你這毒婦竟也來了?” 一聲冷哼從身側響起柱告,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤截驮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后际度,有當地人在樹林里發(fā)現了一具尸體葵袭,經...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年乖菱,在試婚紗的時候發(fā)現自己被綠了坡锡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡窒所,死狀恐怖鹉勒,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情吵取,我是刑警寧澤贸弥,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站海渊,受9級特大地震影響绵疲,放射性物質發(fā)生泄漏。R本人自食惡果不足惜臣疑,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一盔憨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧讯沈,春花似錦郁岩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挤茄,卻和暖如春如叼,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背穷劈。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人之拨。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像辱志,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內容