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

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

1.概述

??我們還是繼承一下《Android 消息處理機(jī)制》的格式,先來概述一下今天的內(nèi)容,假裝符合面向?qū)ο蟮睦^承特性。。。
??在事件傳遞機(jī)制中,必須講解的三個(gè)方法:

??1.public boolean dispatchTouchEvent(MotionEvent ev)方法,這個(gè)方法作用主要是用來分發(fā)事件。也就是說,當(dāng)一個(gè)事件傳遞當(dāng)前View的dispatchTouchEvent方法里面,這個(gè)方法可以決定將事件分發(fā)到哪里去,這里的分發(fā)到哪里去表示有兩個(gè)意思:1.將事件分發(fā)到子View(如果有子View的話);2.將事件分發(fā)到分發(fā)到自己的onTouchEvent方法里面去消耗。
??2.public boolean onInterceptTouchEvent(MotionEvent ev)方法,這個(gè)方法的作用是用來決定當(dāng)前的View或者ViewGroup是否攔截這個(gè)事件,如果返回true的話,那么就表示攔截;反之,表示不攔截。前排預(yù)警一下,這個(gè)方法有很多的坑,不是返回一個(gè)true或者false那么簡單。
??3.public boolean onTouchEvent(MotionEvent event)方法,這個(gè)方法是具體消耗事件的方法,如果返回true的話,表示當(dāng)前的View已經(jīng)將這個(gè)事件消耗了。

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



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



??這里從圖中可以看出來,事件是從ViewGroupA開始的,先調(diào)用A的dispatchTouchEvent方法,進(jìn)行分發(fā),同時(shí)還會調(diào)用A的onInterceptTouchEvent方法,如果onInterceptTouchEvent方法返回的是false,表示ViewGroupA不攔截此事件,于是將事件傳遞給ViewGroupB,ViewGroupB也進(jìn)行跟ViewGroupA一樣的操作。如果ViewGroupB也不進(jìn)行攔截的話,那么首先就會傳遞到View的dispatchTouchEvent方法,由于View再沒有子View了,所以不能進(jìn)行向下分發(fā),所以只能傳遞到View的onTouchEvent方法里面來。如果View消耗了這個(gè)事件的話,那么這個(gè)事件傳遞的流程就在這里結(jié)束,不會繼續(xù)將事件傳到ViewGroupB的onTouchEvent方法里面去;反之如果View不消耗這個(gè)事件的話,那么就繼續(xù)往上傳遞。
??上面只分析了ViewGroup不對事件進(jìn)行攔截的情況,下面來分析一下當(dāng)一個(gè)ViewGroup攔截了事件的情況。例如:

??一旦,ViewGroupA對事件進(jìn)行攔截,直接將事件傳遞給ViewGroupA的onTouchEvent方法里面去。
??這個(gè)大的流程差不多就是這樣的,可能中間有非常多的細(xì)節(jié)沒有提及到,但是不急,待會的源碼分析有你們好受的?。。」_玩笑!

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

(1).DecorView

??當(dāng)我們用手指在屏幕點(diǎn)擊時(shí),事件首先被傳遞到Activity的dispatchTouchEvent方法。對的哦!你沒有看錯(cuò),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);
    }

??可見,當(dāng)Activity的dispatchTouchEvent方法接收到了一個(gè)事件之后,Activity會將這個(gè)傳遞到Window里面去,我們再去看看:

public abstract boolean superDispatchTouchEvent(MotionEvent event);

??哦豁,我們發(fā)現(xiàn)superDispatchTouchEvent所在的Window類是一個(gè)抽象類,怎么辦?不急,在Window類解釋中,google爸爸給我們這么說的(代碼根據(jù) api 26):

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

??這里說的是,Window抽象類的唯一實(shí)現(xiàn)類在是android.view.PhoneWindow。然后我們到PhoneWindow里面去看看相應(yīng)的方法:

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

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

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

DecorView又是什么鬼?DecorView其實(shí)我們界面的頂級容器,也就是我們視圖樹的根,是被添加到Window的。而DecorView作為頂級View,一般情況下,它內(nèi)部會類似于LinearLayout的豎直布局,在這個(gè)布局里面有上下兩個(gè)部分,上面是標(biāo)題欄,下面是Content View部分,在Activity 的setContView所設(shè)置的布局文件就是添加到Content View的部分。如圖:



??通常來說,我們可以通過如下代碼來我們自己設(shè)置的ContentView對象

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

??從這里,我們知道DecorView肯定是一個(gè)ViewGroup對象,我們繼續(xù)點(diǎn)擊dispatchTouchEvent方法,發(fā)現(xiàn)進(jìn)入到了ViewGroup的dispatchTouchEvent方法里面來了。
??好嘛,費(fèi)了半天的勁,我們終于看到了重頭戲了。好了好了,我們整裝待發(fā),準(zhǔn)備好好的來看一下這個(gè)方法!不過我們先來總結(jié),我們獲取了哪些信息:

??1.一個(gè)事件首先會被傳遞到Activity的dispatchTouchEvent方法里面,然后最終會傳遞DecorView中去,最后通過DecorView調(diào)用ViewGroup的dispatchTouchEvent方法來進(jìn)行事件的分發(fā)。
??2.DecorView是一個(gè)Activity的根本局,實(shí)際上他也是一個(gè)ViewGroup。

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

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

            // 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當(dāng)前的ViewGroup是否需要攔截當(dāng)前的事件。我們發(fā)現(xiàn)在這段代碼里面發(fā)現(xiàn)了另一個(gè)比較眼熟的方法onInterceptTouchEvent方法。從代碼中,我們可以看出,ViewGroup判斷一個(gè)事件是否需要判斷實(shí)在dispatchTouchEvent方法里面對方法進(jìn)行調(diào)用。
??然后我們再看看調(diào)用onInterceptTouchEvent方法的條件。首先,action為ACTION_DOWN的話,需要判斷當(dāng)前的是否攔截,這個(gè)非常好理解。但是mFirstTouchTarget是什么什么意思?實(shí)際上呢,這個(gè)從后面的代碼邏輯中可以看出來,當(dāng)ViewGroup的子元素成功處理一個(gè)事件的時(shí)候,mFirstTouchTarget會被賦值并指向該子元素。換一句話說,當(dāng)ViewGroup不攔截事件,將事件交由給子元素來處理時(shí),mFirstTouchTarget就不為null了。也就是說,當(dāng)事件序列的開始--ACTION_DOWN來到時(shí),這時(shí)候mFirstTouchTarget是為null(因?yàn)檫@是第一次來,所以事件還沒有傳遞給它的子元素),如果此時(shí)ViewGroup在onInterceptTouchEvent返回為true的話,表示攔截這個(gè)事件序列,然后后面的ACTION_MOVE和ACTION_UP來到時(shí),由于此時(shí)調(diào)用onInterceptTouchEvent方法的條件不符合,所以onInterceptTouchEvent不會再被調(diào)用。為什么這里調(diào)用onInterceptTouchEvent方法的條件不符合呢,因?yàn)榈谝淮蔚膁own事件被ViewGroup攔截了,從而導(dǎo)致down事件沒有被傳遞到子View,所以mFirstTouchTarget肯定為null,當(dāng)ACTION_MOVE和ACTION_UP兩個(gè)事件來到,actionMasked == MotionEvent.ACTION_DOW || mFirstTouchTarget != null肯定為false的!
??從而,我們從這段里面得到一個(gè)結(jié)論,一旦一個(gè)ViewGroup在onInterceptTouchEvent方法里面對ACTION_DOWN事件進(jìn)行攔截,屬于同一個(gè)事件序列的后續(xù)事件也會被攔截,同時(shí)onInterceptTouchEvent方法只會被調(diào)用一次,也就是對ACTION_DOWN進(jìn)行攔截的那一次!
??說到這里,那么有沒有辦法對其他事件進(jìn)行需求性的攔截呢?有的,這個(gè)問題,我們后續(xù)再講!現(xiàn)在就講的話,就不能顯示我牛逼了!哈哈,開玩笑的,應(yīng)該時(shí)時(shí)刻刻記住自己就是一個(gè)菜雞!
??在剛剛的那段代碼中,我們還發(fā)現(xiàn)有這個(gè)判斷

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

??其中,我們需要關(guān)注的是FLAG_DISALLOW_INTERCEPT 標(biāo)記位,這個(gè)標(biāo)記位是通過ViewGroup里面的requestDisallowInterceptTouchEvent方法來設(shè)置的,一般用于子View。一旦FLAG_DISALLOW_INTERCEPT被設(shè)置了,也就是說,我們在子View里面調(diào)用父布局的requestDisallowInterceptTouchEvent方法,那么ViewGroup將無法攔截除ACTION_DOWN以外的其他點(diǎn)擊事件。
??這里為什么時(shí)候是ACTION_DOWN以外的點(diǎn)擊事件呢?這是因?yàn)?,ACTION_DOWN事件會重置FLAG_DISALLOW_INTERCEPT標(biāo)記位,導(dǎo)致子View設(shè)置的這個(gè)標(biāo)記位無效。我們來看看代碼:

            // 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,從而導(dǎo)致我們的子View調(diào)用requestDisallowInterceptTouchEvent方法失效!
??經(jīng)過上面的代碼,如果ViewGroup不對事件進(jìn)行攔截,那么就會將這個(gè)事件分發(fā)到能夠接收到這個(gè)事件的子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();
                    }

??根據(jù)任玉剛老師在《Android 開發(fā)藝術(shù)探索》中對這段代碼的解釋,一個(gè)子View是否能夠接收到點(diǎn)擊事件主要由兩點(diǎn)來衡量:子View是否是否在播放動畫和點(diǎn)擊事件是否落在子View的的區(qū)域內(nèi)。如果這兩個(gè)事件能夠滿足的話,那么事件就會交給它來處理。
??這里將會詳細(xì)的講解一下,事件到底是怎么傳遞到子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;
                            }

??這個(gè)是事件分發(fā)代碼,其中,我們會發(fā)現(xiàn),如果當(dāng)前子View會消耗這個(gè)事件,也就是說dispatchTransformedTouchEvent方法返回true,那么將會將當(dāng)前的View添加target的鏈表,而我們說的mFirstTouchTarget就是指向這個(gè)鏈表的頭!這個(gè)就相當(dāng)于完成的分發(fā)了嗎?
??NO!NO!沒有那么的簡單,我們會發(fā)現(xiàn)前面有段代碼:

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

??如果當(dāng)遍歷第一個(gè)子View的時(shí)候,這里的newTouchTarget就會返回的不是null,豈不是下面的dispatchTransformedTouchEvent根本就來不及調(diào)用。像這種情況,應(yīng)該怎么辦?我們發(fā)現(xiàn),只要在這段代碼里面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;
                }

??如果說,之前已經(jīng)將事件分發(fā)下去了,alreadyDispatchedToNewTouchTarget && target == newTouchTarget這個(gè)條件肯定為true。所以,如果在dispatchTransformedTouchEvent方法之前break,從而導(dǎo)致跳出循環(huán),alreadyDispatchedToNewTouchTarget肯定是為false的,因?yàn)檫@個(gè)變量在調(diào)用了dispatchTransformedTouchEvent方法之后會被置為true。這行代碼在之前循環(huán)遍歷子View里面。

                                alreadyDispatchedToNewTouchTarget = true;

??所以,只要在之前沒有調(diào)用dispatchTransformedTouchEvent方法就break,肯定會進(jìn)入else的代碼里面?,F(xiàn)在的關(guān)鍵是理解resetCancelNextUpFlag是什么意思?我們先來看看這個(gè)方法:

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

??這里,我是看不懂代碼的。但是可以從方法的注釋來看他的意思,這個(gè)方法作用是,如果之前這個(gè)View的flag被重置過,那么就返回true,反之返回false。簡而言之,相對于同一個(gè)View來說的話,如果第一次調(diào)用這個(gè)方法的話,返回的是false;反之則返回的true。
??所以,在這里,我們就可以理解到了,只要是在調(diào)用dispatchTransformedTouchEvent方法之前就break的話,resetCancelNextUpFlag返回的肯定是true。這個(gè)是為什么呢?因?yàn)橹灰猤etTouchTarget返回的不是null,表示的意思就是當(dāng)前的View已經(jīng)被添加到了mFirstTouchTarget所在的鏈表中,也就是說在當(dāng)前這個(gè)事件之前,有可能有個(gè)事件傳遞到當(dāng)前的這個(gè)View,并且執(zhí)行了,所以被添加到鏈表中的。因?yàn)檫@段代碼在dispatchTransformedTouchEvent方法為的true才執(zhí)行的:

                                newTouchTarget = addTouchTarget(child, idBitsToAssign);

??從而得知,只要newTouchTarget不為null的話,resetCancelNextUpFlag方法返回的肯定是true。而這里cancelChild變量還由intercepted變量來決定,這個(gè)待會再細(xì)講,因?yàn)樽兞刻孛吹目恿耍?br> ??這樣我們就能得知,如果一個(gè)View對一個(gè)事件序列的事件進(jìn)行處理,但是后續(xù)如果有一個(gè)事件不會處理的話,那這個(gè)View會收到一個(gè)ACTION_CANCEL類型的事件!
??以上就是ViewGroup對子View的事件分發(fā)大概的解釋,不敢說特別詳細(xì)!下面來總結(jié)一下:

??1.當(dāng)一個(gè)事件傳遞到ViewGroup里面的話,首先會根據(jù)事件類型或者mFirstTouchTarget 是否null來判斷是否調(diào)用onInterceptTouchEvent方法,當(dāng)然這個(gè)過程中還要考慮FLAG_DISALLOW_INTERCEPT標(biāo)記位。簡而言之,當(dāng)前DOWN事件來到時(shí),ViewGroup首先詢問onInterceptTouchEvent是否需要攔截。這里需要注意的是,如果有子View處理這個(gè)事件了,會導(dǎo)致mFirstTouchTarget不為null,從而可以形成一種父ViewGroup可以攔截非ACTION_DOWN事件的局面!還需要注意的是,整個(gè)詢問攔截的過程還需要考慮子View調(diào)用requestDisallowInterceptTouchEvent方法來請求不要我的事件!哎,感覺子View好可憐,動不動就會ViewGroup折磨?。。?!
??2.當(dāng)ViewGroup不對事件進(jìn)行攔截時(shí),ViewGroup會將相應(yīng)的事件傳遞到子View里面!
??3.如果整個(gè)事件序列的ACTION_DOWN沒有子View來處理,最終會傳遞到ViewGroup方法里面處理。因?yàn)楫?dāng)mFirstTouchTarget為null時(shí),會調(diào)用ViewGroup自己的onTouchEvent方法!但是這里需要的注意,整個(gè)事件序列,除了ACTION_DOWN會傳遞到子View的onTouchEvent之外,后續(xù)的事件都只會到達(dá)子View的dispatchTouchEvent方法,不會到達(dá)onTouchEvent方法里面。這個(gè)原因待會再講View的方法來解釋!
??4.當(dāng)一個(gè)事件序列中間(記住這里是中間,開始的情況參考 3 ,結(jié)尾可以參考這個(gè))的某個(gè)事件沒有子View來處理的話,那么在TouchTarget鏈上的所有View都會收到一個(gè)ACTION_CANCEL事件,并且會將這些子View從鏈上recycle掉。從而得知,只要一個(gè)子View不對一個(gè)事件進(jìn)行處理,那么在這個(gè)事件序列上的其他類型的事件都不會交給它來處理。
??5.如果一個(gè)事件序列從ACTION_DOWN開始,就被攔截了。這個(gè)事件序列的所有事件不會在傳遞的到子View。因?yàn)锳CTION_DOWN來的時(shí)候,mFirstTouchTarget本來為空,由于onInterceptTouchEvent方法返回true,所以導(dǎo)致if (!canceled && !intercepted)語句進(jìn)入不了,進(jìn)而導(dǎo)致mFirstTouchTarget在整個(gè)事件序列都為空,所以一直在調(diào)用這個(gè)代碼,從而導(dǎo)致整個(gè)事件序列的都不能傳遞下去:
            // 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事件的攔截

??還記得我在前面挖的兩個(gè)坑嗎?第一個(gè)是在概述里面說的,前排預(yù)警一下,onInterceptTouchEvent方法有很多的坑,不是返回一個(gè)true或者false那么簡單;第二個(gè)是在(2)里面的,有沒有辦法對其他事件進(jìn)行需求性的攔截?
??到這里來看看,這兩個(gè)坑好像就像是一個(gè)問題,都是關(guān)于onInterceptTouchEvent方法。
??其實(shí)在之前我們簡單的介紹onInterceptTouchEvent方法的作用和使用,但是只是粗略的介紹,在這里將稍微詳細(xì)的解釋。

A.onInterceptTouchEvent方法的調(diào)用時(shí)機(jī)

??先說明一下,這里先不考慮FLAG_DISALLOW_INTERCEPT標(biāo)記位的影響。
??在dispatchTouchEvent方法,我們知道,當(dāng)一個(gè)事件序列的開始,也就是ACTION_DOWN來到時(shí),會調(diào)用onInterceptTouchEvent方法來詢問是否需要攔截此事件序列!這種情況下,應(yīng)該非常容易的理解!
??另一種情況便是mFirstTouchTarget 不為null的時(shí)候。那mFirstTouchTarget不為null究竟是什么情況呢?我們從dispatchTouchEvent方法里面可以看出來,當(dāng)一個(gè)事件被子View消耗了,那么會將當(dāng)前的這個(gè)View封裝成一個(gè)Target對象,然后添加到一個(gè)鏈表的鏈頭,而mFirstTouchTarget則是指向這個(gè)鏈表的鏈頭。也就是說,當(dāng)前mFirstTouchTarget不為null的時(shí)候,表示在同一個(gè)事件序列,當(dāng)前事件前面的事件被子View消耗掉了!mFirstTouchTarget不為null表示的就是這個(gè)意思!
??如上的情況下,我們可以形象的解釋,將你的媽媽比喻為ViewGroup,而子View當(dāng)成你,你開始打游戲表示一個(gè)事件序列的開始。如上的情況就是這樣的,你開始打游戲的時(shí)候,你媽媽沒有攔截你的行為,因此你可以順利的打開游戲,開心的吃雞,如果中途你媽媽叫你去打醬油,可是此時(shí)你正在決賽圈說你沒空,你媽媽就生氣了,把你的網(wǎng)線拔了,相當(dāng)于是攔截你的行為,導(dǎo)致你的吃雞夢想泡湯了!這個(gè)比喻能夠說明上面的情況,也就是說,當(dāng)子View在ACTION_MOVE的非常開心的時(shí)候,父ViewGroup有資格讓子View不開心!哈哈哈哈!!!
??上面的解釋就是,當(dāng)不考慮FLAG_DISALLOW_INTERCEPT標(biāo)記位時(shí),onInterceptTouchEvent方法的調(diào)用時(shí)機(jī)。
??那么我們現(xiàn)在來考慮FLAG_DISALLOW_INTERCEPT標(biāo)記位。
??首先說一下,標(biāo)記位對事件序列的開始事件--ACTION_DOWN無效的!只有當(dāng)子View在ACTION_MOVE的非常開心的時(shí)候,才有資格向父ViewGroup申請不要攔截我的事件!這個(gè)請求是有效的!
??如上便是onInterceptTouchEvent方法的調(diào)用時(shí)機(jī)。這里對onInterceptTouchEvent方法的調(diào)用時(shí)機(jī)做一個(gè)簡單的總結(jié):

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

B.onInterceptTouchEvent方法對非ACTION_DOWN的事件進(jìn)行攔截

??如果ViewGroup只能對ACTION_DOWN進(jìn)行攔截的話,這樣也太暴力了!因?yàn)檫@樣會導(dǎo)致整個(gè)事件序列都只能被傳遞ViewGroup。所以,ViewGroup對ACTION_MOVE和ACTION_UP事件還是有必要的。其實(shí)這種需求很好的實(shí)現(xiàn),例如:

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

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

??是不是瞬間來了一句臥槽!這么簡單。對!就是這么簡單,但是簡單的背后大有玄機(jī)所在了!例如:
??這是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;
    }
}

??此時(shí),我對View進(jìn)行ACTION_DOWN和ACTION_UP的事件產(chǎn)生,然后打印的log卻是這樣的:



??我們發(fā)現(xiàn),當(dāng)ACTION_DOWN事件產(chǎn)生時(shí),傳遞到子View很正常,但是我們對ACTION_UP事件進(jìn)行攔截的,為什么還是會傳遞子View里面去呢?是不是onInterceptTouchEvent對ACTION_UP事件是無效的呢?瞎猜是沒有用的,此時(shí)我們來看看dispatchTouchEvent的代碼。(其實(shí)這種情況,我在分析dispatchTouchEvent的時(shí)候已經(jīng)非常小聲的說過了哦!?。。。?/p>

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

??上面這段代碼,我們當(dāng)ACTION_UP事件來到,由于mFirstTouchTarget不為null,最終會調(diào)用onInterceptTouchEvent來進(jìn)行詢問是否需要攔截,我們在onInterceptTouchEvent方法里面返回的是true,所以在intercepted肯定為true。然后代碼往下走,最終進(jìn)入這段代碼:

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

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



??哈哈沒錯(cuò),第二次之所以將事件傳遞給子View,那么是因?yàn)锳CTION_MOVE事件被攔截了,從而傳遞過去一個(gè)ACTION_CANCEL事件過去,而不是ACTION_MOVE事件。
??好了,onInterceptTouchEvent方法分析的差不多了,現(xiàn)在該解決在留的兩個(gè)問題。首先,onInterceptTouchEvent方法坑在于onInterceptTouchEvent方法的調(diào)用時(shí)機(jī),待會再總結(jié)里面會總結(jié)一下,這里就不再多余的說了;其實(shí)onInterceptTouchEvent的坑還有就是ACTION_DOWN和ACTION_UP,誰又能想到傳遞子View的根本不是ACTION_UP事件呢?。其次,就是對非ACTION_DOWN的攔截,假設(shè)我們從ACTION_MOVE開始攔截,需要注意的是第一個(gè)ACTION_MOVE事件是不會傳遞子View,也不會傳遞到ViewGroup,只有經(jīng)過這次的處理,后面的事件ViewGroup才算是能夠接收到!
??又該對上面的知識點(diǎn)做一個(gè)總結(jié):

??1.一個(gè)ViewGroup的調(diào)用時(shí)機(jī)是:1.ACTION_DOWN的來到;2.事件序列中間的ACTION_MOVE事件來到,需要注意是這樣情況下,必須保證在同一個(gè)事件序列中, 當(dāng)前事件的前面的事件有被子View消耗過的,也就是,mFirstTouchTarget不能為null。
??2.調(diào)用時(shí)機(jī)還需要的是:如果一個(gè)事件被攔截了,在這個(gè)事件序列里面,onInterceptTouchEvent不會再被調(diào)用。
??3.如果我們想要對非ACTION_DOWN事件進(jìn)行攔截,必須保證同一個(gè)事件序列的前面所有事件都子View執(zhí)行了。
??4.對非ACTION_DOWN事件進(jìn)行攔截,是對下次的事件進(jìn)行攔截,當(dāng)前的事件會被變?yōu)锳CTION_CANCEL傳遞到子View中去。

3.View對事件的處理

??由于View是沒有子View的,所以View不能繼續(xù)對事件繼續(xù)的分發(fā)。相較于ViewGroup,View少了一個(gè)onInterceptTouchEvent方法。所以說,如果一個(gè)事件到達(dá)View,肯定會處理,注意的處理表達(dá)意思是:它可以調(diào)用onTouch或者onTouchEvent方法來處理,或者不處理,最后這個(gè)事件被它的ViewGroup分發(fā)ViewGroup自己進(jìn)行處理。
??所以,View對事件的處理分成兩種情況:一種是自己處理;一種是不處理,父ViewGroup會自己處理,處理的代碼也是調(diào)用View的,因?yàn)閂iewGroup繼承于View。我們一個(gè)一個(gè)的分析。

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

??以上的代碼,我刪除了部分我認(rèn)為不重要的代碼,是不是非常的簡單?其實(shí)意思也非常的簡單,首先如果設(shè)置了OnTouchListener監(jiān)聽的話,onTouch方法是否消耗該事件,如果消耗的話,事件傳遞就結(jié)束了;反之,則將事件傳遞到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;
        }

??從以上的代碼中,我們看出來,如果一個(gè)View的enable屬性是Disable的話,它仍然能夠消耗事件,只是不會做出任何的反應(yīng)而已,正如注釋所說的。
??我們繼續(xù)往下看,我們發(fā)現(xiàn)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其中一個(gè)為true的話,就會對事件進(jìn)行消耗!
??在switch-case里面,我們不看ACTION_DOWN和ACTION_MOVE事件,我們來看看ACTION_UP事件有個(gè)非常眼熟的東西:

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

??哎呀,這個(gè)不是我們喜聞樂見的OnClickListener嗎?開不開心,激不激動?哈哈哈?。。?br> ??從這里,我們可以得出,在一個(gè)View中,onTouch的優(yōu)先級是最高的,其次是onTouchEvent,最后才是onClick方法!

(2).ViewGroup對事件的處理

??ViewGroup對事件的處理在dispatchTransformedTouchEvent方法里面進(jìn)行的,由于dispatchTransformedTouchEvent方法的代碼比較長,這里只看他是怎么調(diào)用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ā)現(xiàn)調(diào)用super.dispatchTouchEvent(event)方法,從而完成了自己對事件的處理,事件處理的流程跟View對事件的處理流程比較相似!

4.總結(jié)

??終于寫完了,我們還是來對我們所有的內(nèi)容做一個(gè)總結(jié):

??1.如果一個(gè)事件序列的ACTION_DOWN事件被ViewGroup攔截,事件序列的后續(xù)事件不再會傳遞到子View,即使在ACTON_MOVE時(shí),子View調(diào)用requestDisallowInterceptTouchEvent方法沒用的。同時(shí),在這種情況下,onInterceptTouchEvent方法只會被調(diào)用一次。
??2.在一個(gè)事件序列中,如果ACTION_DOWN事件沒有被攔截,并且ACTION_DOWN事件被子View消耗了,則ViewGroup有資格去攔截事件序列剩下的事件。
??3.在一個(gè)事件序列中,如果子View不處理ACTION_DOWN事件,此事件序列的后續(xù)事件不會在傳遞到子View。記住,連子View的dispatchTouchEvent方法都不會到達(dá)!
??4.在一個(gè)事件序列中,如果子View開始處理一些事件,事件中途突然被ViewGroup攔截,被攔截的當(dāng)前事件會轉(zhuǎn)換成為ACTION_CANCEL事件傳遞到子View中去,ViewGroup真正獲取事件從下一次事件(不是下一次事件序列!)開始。
??5.在View中,優(yōu)先級最高的onTouch,其次是onTouchEvent,最好是onClick。onClick方法在ACTION_UP時(shí)刻回調(diào)!
??6.如果一個(gè)事件最后連ViewGroup都不處理的話,最終回到Activity的onTouchEvent方法里面來。
??7.當(dāng)View的enable為Disable時(shí),也會消耗事件,只是不會做出任何的反應(yīng)。同時(shí)只要CLICKABLE、LONG_CLICKABLE或者CONTEXT_CLICKABLE其中一個(gè)為true的話,就會對事件進(jìn)行消耗!
??8.View的requestDisallowInterceptTouchEvent方法只是在View消耗ACTION_DOWN事件的前提才有效!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,001評論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內(nèi)容