View的事件體系-View的事件分發機制

上面介紹了View的基礎知識以及View的滑動,本節將介紹View的一個核心知識點:事件分發機制。事件分發截止不僅僅是核心知識點更是難點,不少初學者甚至中介開發者面對這個問題是都會覺得很困惑。另外View的另一大難題滑動衝突,它的解決辦法的理論基礎就是事件分發機制,因此掌握好View的事件分發機制是十分重要的。本節將深入介紹View的事件分發機制。

1.點擊事件的傳遞規則
在介紹點擊事件的傳遞規則之前,首先我們要明白這裏要分析的對象是MotionEvent,即點擊事件,關於MotionEvent在之前已經進行了介紹。所謂點擊事件的分發,其實就是對MotionEvent事件的分發過程,即當一個MotionEvent產生了以後,系統需要把這個事件傳遞給一個具體的View,而這個傳遞的過程就是分發的過程。點擊事件的分發過程由三個很重要的方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面我們先介紹一下這幾個方法。

public boolean dispatchTouchEvent(MotionEvent ev)
用來進行事件的分發。如果事件能夠傳遞給當前View,那麼此方法一定會被調用,返回結果受當前View的ouTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。

public boolean onInterceptTouchEvent(MotionEvent ev)
在上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。

public boolean onTouchEvent(MotionEvent ev)
dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接受到事件。

上述的三個方法到底由什麼區別呢?它們是什麼關係?其實它們的關係可以用如下僞代碼表示:

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

上述僞代碼已經將三者的關係表現得淋漓盡致。通過上面得僞代碼,我們也可以大致瞭解點擊事件的傳遞規則:對於一個根ViewGroup來說,點擊事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent就會被調用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接着事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調用;如果這個ViewGroup的onInterceptTouchEvent方法返回false就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的dispatchTouchEvent方法就會被調用,如此反覆直到事件被最終處理。
當一個View需要處理事件時,如果它設置了onTouchListener,那麼onTouchListener中的onTouch方法會被回調。這時事件如何處理還要看onTouch的返回值,如果返回false,則當前View的onTouchEvent方法會被調用;如果返回true,那麼onTouchEvent方法將不會被調用。由此可見,給View設置的OnTouchListener,其優先級比onTouchEvent要高。在onTouchEvent方法中,如果當前設置的有OnTouchListener,那麼它的onClick方法會被調用。可以看出,平常我們常用的OnClickListener,其優先級最低,即處於事件傳遞的尾端。

當一個點擊事件產生後,它的傳遞過程遵循如下順序:Activity->Window->View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最後Window再傳遞給頂級View。頂級的View接收到事件後,就會按照事件分發機制去分發事件。考慮一種情況,如果一個View的onTouchEvent返回false,那麼它的父容器的ouTouchEvent將會被調用,以次類推。如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給Activity處理,即Activity的onTouchEvent方法會被調用。這個過程其實也很好理解,我們可以換一種思路,加入點擊事件是一個難題,這個難題最終被上級領導分給了一個程序員去處理(這是事件的分發過程),結果這個程序員搞不定(ouTouchEvent返回false),那麼現在該怎麼辦呢?難題必須要解決,那隻能交給水平更高的上級解決(上級的onTouchEvent被調用),如果上級再搞不定,那智能交給上級的上級去解決,這樣將難題一層層地向上拋,這是公司內部一種很常見地處理問題地過程。從這個角度來看,View的事件傳遞過程還是很貼近顯示的,畢竟程序員也生活在現實中。

關於事件傳遞的機制,這裏給出一些結論,根據這些結論可以更好地理解整個傳遞機制,如下所示。
(1)同一個事件序列是指從手指觸摸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中所產生的一些列事件,這個事件序列以down事件開始,中間含有數量補丁的move,最終以up事件結束。

(2)正常情況下,一個事件序列只能被一個View攔截且消耗。這一條的原因可以參考(3),因爲一旦一個蒜素攔截了某次事件,那麼同一個事件序列內的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個View同時處理,但是通過特殊的手段可以做到,比如一個View將本該自己處理的事情通過onTouchEvent強行傳遞給其他View處理。
(3)某個View一旦決定攔截,那麼這個事件序列只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的onInterceptTouchEvent不再會被調用。這條也很好理解,就是說當一個View決定攔截一個事件後,那麼系統就會把同一個事件序列內的其他方法都直接交給它來處理,因此就不再調用這個View的onInterceptTouchEvent去詢問它是否要攔截了。
(4)某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會被調用。意思是事件一旦交給一個View處理,那麼它必須消耗掉,否則同意事件序列中剩下的事件就不再交給它來處理了,這就好比上級交給程序員意見事,如果這件事沒有處理好,短期內上就就不敢再把事情交給這個程序員了,二者事類似的道理。

(5)如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點擊事件會消失,此時父元素的onTouchEvent並不會被調用,並且當前View可以持續收到後續事件,最終這些消失的點事件會傳遞給Activity處理。
(6)ViewGroup默認不攔截任何事件。Android源碼中ViewGroup的onInterceptTouchEvent方法默認返回false。
(7)View沒有onInterceptTouchEvent方法,一點由點擊事件傳遞給它,那麼它的onTouchEvent方法就會被調用。
(8)View的onTouchEvent默認都會消耗事件(返回true),除非他是不可點擊(clickable和longClickable同時爲false)。View的longClickable屬性默認爲false,clickable屬性要分情況,比如Button的clickable屬性默認爲true,而TextView得clickable屬性默認爲false。
(9)View得enable屬性不影響onTouchEvent得默認返回值。哪怕一個View時disable狀態的,只要它的clickable或者longClickable有一個爲true,那麼它的onTouchEvent就返回true。
(10)onClick會發送的前提時當前View是可點擊的,並且它收到了down和up的事件。

(11)事件傳遞額過程是由外向內的,及時間總是先傳遞給父元素,然後再由父元素分發給子元素,通過requestDisallowInterceptTouchEvent方法可以再子元素中干預父元素的事件分發過程,但是ACTION_DOWN事件除外。

2.事件分發的源碼解析

上一節分析了View的事件分發機制,本節將會從源碼的角度去進一步分析、證實上面的結論。

1.Activity對點擊事件的分發過程。
點擊事件用MotionEvent來表示,當一個點擊操作發生時,事件最先傳給給當前Activity,由Activity的dispatchTouchEvent來進行事件派發,具體的工作是由Activity內部的Window來完成的。Window會將事件傳遞給decor view,decor view一般是當前界面的底層容器(setContentView所設置的View的父容器),通過Activity.getWindow.getDecorView()可以獲得。我們先從Activity的dispatchTouchEvent開始分析。

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

現在分析上面的代碼。首先事件開始交給Activity所附屬的Window進行分發,如果返回true,整個事件循環就結束了,返回false意味着事件沒人處理,所有View的onTouchEvent都返回了false,那麼Activity的onTouchEvent就會被調用。
接下來看Window是如何將書劍傳遞給ViewGroup的。通過源碼我們知道,Window是個抽象類,而Window的superDispatchTouchEvent方法也是個抽象方法,因此我們必須找到Window的實現類才行。

    public abstract boolean superDispatchTouchEvent(MotionEvent event);


那麼到底Window的實現類是什麼呢?其實是PhoneWindow,這一點從Window的源碼中也可以看出來,再Window的說明中,有這麼一段話:

/**
 * Abstract base class for a top-level window look and behavior policy.  An
 * instance of this class should be used as the top-level view added to the
 * window manager. It provides standard UI policies such as a background, title
 * area, default key processing, etc.
 *
 * <p>The only existing implementation of this abstract class is
 * android.view.PhoneWindow, which you should instantiate when needing a
 * Window.
 */

上面這段話的大概意思是:window類可以控制頂級View的外觀和行爲策略,它的唯一實現位於android.policy.PhoneWindow中,當你要實例化這個Window類的時候,你並知道它的細節,因爲這個類會被重構,只有一個工程方法可以使用。儘管着看起來有點模糊,不過我們可以看一下android.policy.PhoneWindow這個類,儘管實例化的時候此類會被重構,進式重構而已,功能是類似的。
由於Window的唯一實現類是PhoneWindow,因此接下來看一下PhoneWindow是如何處理點擊事件的, 如下所示。
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

到這裏邏輯就很清晰了,PhoneWindow將事件直接傳遞給了DecorView,這個DecorView是什麼呢?請看下面:

    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
    
    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;
    
    @Override
    public final View getDecorView() {
        if (mDecor == null) {
            installDecor();
        }
        return mDecor;
    }

我們知道,通過((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)這種方式就可以獲取Activity所設置的View,這個mDecor顯然就是getWindow().getDecorView()返回的View,而我們通過setContentView設置的View是它的一個子View。目前事件傳遞到了DecorView這裏,由於DecorView繼承自FrameLayout且是父View,所以最終事件傳遞給View。換句話說,事件肯定會傳遞到View,不然應用如何響應點擊事件呢?不過這不是我們的重點,重點是事件到了View以後該如何傳遞,這對我們更有好處。從這裏開始,事件意見傳遞到頂級View了,即再Activity中通過setContentView所設置的View,另外頂級View也叫根View,頂級View一般來說是ViewGroup。

2.頂級View對點擊事件的分發過程

關於點擊事件如何在View中進行分發,上一節一屆做了詳細的介紹,這裏再大致回顧一下。點擊事件到達頂級View(一般是一個ViewGroup)以後,會調用ViewGroup的dispatchTouchEvent方法,然後的邏輯是這樣的:如果頂級ViewGroup攔截事件即onInterceptTouchEvent返回true,則事件由ViewGroup處理,這時如果ViewGroup的mOnTouchListener被設置,則onTouch會被調用,否則onTouchEvent會被調用。也就是說,如果提供的話,onTouch會屏蔽調onTouchEvent。再onTouchEvent中,如果設置了mOnClickListener,則onClick會被調用。如果頂級ViewGroup不攔截事件,則事件會傳遞給它所在的點擊事件鏈上的子View,這時子View的dispatchTouchEvent會被調用。到此爲止,事件已經從頂級View傳遞給了下一層View,接下來的傳遞過程和頂級View是一致的,如此循環,完成整個事件的分發。
首先看ViewGroup對點擊事件的分發過程,其主要實現再ViewGroup的dispatchTouchEvent方法中,這個方法比較長,這裏分段說明。先看下面一段,很顯然,它描述的是當前View是否攔截點擊事情這個邏輯。

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

從上面代碼我們可以看出,ViewGroup在如下兩種情況下會判斷是否要攔截當前事件:事件類型爲ACTION_DOWN或者mFirstTouchTarget!=null是什麼意思呢?這個從後面的代碼邏輯可以看出來,當事件由ViewGroup的子元素成功處理時,mFirstTouchTarget會被賦值並指向子元素,換句話來說,當ViewGroup不攔截事件並將事件交由子元素處理時mFirstTouchTarget !=null。反過來,一旦事件由當前ViewGroup攔截時,mFirstTouchTarget !=null就不成立。那麼當ACTION_MOVE和ACTION_UP事件到來時,由於(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)這個條件爲false,將導致ViewGroup的onInterceptTouchEvent不會再被調用,並且同一序列中的其他事件都會默認交給它處理。

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

從上面的源碼分析,我們可以得出結論:當ViewGroup決定攔截事件後,那麼後續的點擊事件將會默認交給它處理並且不再調用它的onInterceptTouchEvent方法,這正式了上節中結尾處的第3條結論。FLAG_DISALLOW_INTERCEPT這個標識的作用時讓ViewGroup不再攔截事件,當然前提時ViewGroup不攔截ACTION_DOWN事件,這證實了上節末尾處的第11條結論。那麼這段分析對我們有什麼價值呢?總結起來有兩點:第一點onInterceptTouchEvent不是每次事件都會被調用的,如果我們想提前處理所有的點擊事件,要選擇dispatchTouchEvent方法,只有這個方法能確保每次都會被調用,當然前提時事件能夠傳遞到當前的ViewGroup;另外一點,FLAG_DISALLOW_INTERCEPT標記位的作用給我們提供了一個思路,當面對滑動衝突時,我們是不是可以考慮用這種方法去解決問題?關於滑動衝突將在霞姐中進行詳細分析。
接下再看當VIewGroup不攔截事件的時候,事件會向下分發由他的子View進行處理,這段源碼如下所示。
                        final View[] children = mChildren;

                        final boolean customOrder = isChildrenDrawingOrderEnabled();
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder ?
                                    getChildDrawingOrder(childrenCount, i) : i;
                            final View child = children[childIndex];
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                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();
                                mLastTouchDownIndex = childIndex;
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }

上面這段代碼邏輯也很清晰,首先遍歷ViewGroup的所有子元素,然後判斷子元素是否能夠接收到到點擊事件。是否能夠接受點擊事件主要由兩點來衡量:子元素是否再播放動畫和點擊事件的座標是否落在子元素的區域內。如果某個子元素滿足這兩個條件,那麼事件就會傳遞給他來處理。可以看到,dispatchTransformedTouchEvent實際上調用的就是子元素的dispatchTouchEvent方法,在它的內部有如下一段內容,而在上面的代碼中child傳遞的不是null,因此它會直接調用子元素的dispatchTouchEvent方法,這樣事件就交由子元素處理了,從而完成了一輪事件分發。

        if (child == null) {
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            handled = child.dispatchTouchEvent(transformedEvent);
        }

如果子元素的dispatchTouchEvent返回true,這時我們暫時不用考慮事件在子元素內部是怎麼分發的,那麼mFirstTouchTarget就會被賦值同事跳出for循環,如下所示。

                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;

這幾行代碼完成了mFirstTouchTarget的賦值並終止對子元素的遍歷。如果子元素的dispatchTouchEvent返回false,ViewGroup就會把事件分發給下一個子元素(如果還有下一個子元素的話)。
其實mFirstTouchTarget真正的賦值過程是在addTouchTarget內部完成的,從下面的addTouchTarget方法內部結構可以看出,mFirstTouchTarget其實是一種單鏈表結構。mFirstTouchTarget是否被賦值將直接影響到ViewGroup對事件的攔截策略,如果mFirstTouchTarget爲null,那麼ViewGroup就默認攔截下來同意序列中所有的點擊事件,這一點在前面已經做了分析。

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

如果遍歷所有的子元素後事件都沒有被合適地處理,這包含兩種情況:第一種是ViewGroup沒有子元素;第二種是子元素處理了點擊事件,但在dispatchTouchEvent中返回了false,這一版是因爲子元素在onTouchEvent中返回了false。這兩種情況下,ViewGroup會自己處理點擊事件,這裏就證實了上節中地第4條結論,代碼如下所示。

            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
            } 

注意上面這段代碼,這裏第三個參數chind爲null,從前面地分析可以知道,它會調用super.dispatchTouchEvent(event),很顯然,這裏就轉到View地dispatchTouchEvent方法,即點擊事件開始交由View來處理,請看下面的分析。

4.View對點擊事件的處理過程
View對點擊事件的處理過程稍微簡單一些,注意這裏的View不包含ViewGroup。先看它的dispatchTouchEvent方法,如下所示。

   public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

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

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

View對點擊事件的處理過程就比較簡單了,因爲View(這裏不包含ViewGroup)是一個單獨的元素,它沒有子元素因此無法向下傳遞事件,所以它只能自己處理事件。從上面的源碼可以看出View對點擊事件的處理過程,首先會判斷有沒有設置OnTouchListener。如果OnTouchListener中的onTouch方法返回true,那麼onTouchEvent就不會被調用,可見OnTouchListener的優先級高於OnTouchEvent,這樣做的好處是方便在外界處理點擊事件。
接着在分析onTouchEvent的實現。先看當View處於不可剪輯狀態下點擊事件的處理過程,如下所示。很顯然,不可用狀態下的View照樣會消耗點擊事件,儘管它看起來不可用。
 final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        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設置有代理,那麼還會執行TouchDelegate的onTouchEvent方法,這個onTouchEvent的工作機制看起來和onTouchListener類似,這裏不深入研究了。

        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

下面再看下onTouchEvent中對點擊事件的具體處理,如下所示。

if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if (!clickable) {
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    if (clickable) {
                        setPressed(false);
                    }
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    break;

                case MotionEvent.ACTION_MOVE:
                    if (clickable) {
                        drawableHotspotChanged(x, y);
                    }

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        // Remove any future long press/tap checks
                        removeTapCallback();
                        removeLongPressCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            setPressed(false);
                        }
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    }
                    break;
            }

            return true;
        }

從上面的代碼來看,只要View的CLICKABLE和LOG_CLICKABLE有一個爲true,那麼它就會消耗這個事件,即onTouchEvent方法返回true,不管他是不是DISABLE狀態,這就證實了上節末尾處第8、第9和第10條結論。然後就是當ACTION_UP事件發生時,會觸發performClick方法,如果View設置了onClickListener,那麼performClick方法內部會調用它的onClick方法,如下所示。

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

View的LONG_CLICKABLE的屬性默認爲false,而CLICKABLE屬性是否爲false和具體的Vie有關,確切來說時可點擊的View其CLICKABLE爲true,不可點擊的View其CLICKABLE爲false,比如Button時可點擊的,TextView是不可點擊的。通過setClickable和setLongClickAble可以分別改變View的CLICKABLE和LONG_CLICKABLE屬性。另外,setOnClickListener會自動將View的CLICKABLE設爲true,setOnLongClickListener則會自動將View的LONG_CLICKABLE設爲true,這一點從源碼中可以看出來,如下所示。

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

    public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }
到這裏,點擊事件的分發機制的源碼實現已經分析完了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章