1. 程式人生 > >詳細解析Android的View事件分發機制 附帶原始碼分析

詳細解析Android的View事件分發機制 附帶原始碼分析

前言

在Android中,事件分發機制是一塊很重要的知識點,掌握這個機制能幫你在平時的開發中解決掉很多的View事件衝突問題,這個問題也是面試中問的比較多的一個問題了,今天就來總結下這個知識點。

事件分發機制

事件分發原因

Android中頁面上的View是以樹型結構顯示的,View會重疊在一起,當我們點選的地方有多個View可以響應的時候,這個點選事件應該給誰,為了解決這個問題就需要一個事件分發機制

事件分發物件

Touch事件,即將每一個Touch事件(MotionEvent)傳遞給View,至於最終這個事件有沒有處理看接收事件者的邏輯而定

當用戶觸控式螢幕幕的時候,就會產生Touch事件(Touch事件被封裝成MotionEvent物件),其主要分為如下幾種

  • MotionEvent.ACTION_DOWN:使用手指點選螢幕這一瞬間,產生該事件,是所有事件的開始
  • MotionEvent.ACTION_MOVE:使用手指在螢幕滑動的時候產生該事件
  • MotionEvent.ACTION_CANCLE:非人為原因結束當前事件
  • MotionEvent.ACTION_UP:手指離開螢幕一瞬間產生該事件

一次完整的Touch事件,是從使用者手指觸控式螢幕幕(伴隨著一次ACTION_DOWN事件)到使用者手指離開螢幕(伴隨著一次ACTION_UP事件)這一過程,整個過程如下

ACTION_DOWN(一次) --> ACTION_MOVE(N次) --> ACTION_UP(一次)

事件分發方法

  • dispatchTouchEvent(MotionEvent ev) :從方法名也能看出它的作用是對事件進行分發;當一個事件由底層驅動檢測到了之後,會進行上報,最終會交由Activity的該方法處理,來決定是自己消費還是繼續傳遞下去
  • onInterceptTouchEvent(MotionEvent ev) :當一個事件分發到ViewGroup後,它可以決定是否對該事件進行攔截,該方法只有ViewGroup擁有
  • onTouchEvent(MotionEvent event) :這是事件分發流程的最後一個方法了,即是否消費該次事件

事件分發參與者

  • Activity:包含ViewGroup和View
  • ViewGroup:包含ViewGroup和View
  • View:並不包含其它View,只有自己

事件分發流向一般是Activity --> ViewGroup --> … --> View

注意:

  • 子View可以通過requestDisallowInterceptTouchEvent方法干預父View的事件分發過程(ACTION_DOWN事件除外),而這就是我們處理滑動衝突常用的關鍵方法
  • 如果View設定了onTouchListener,在重寫的onTouch方法中返回true,那麼它的onTouchEvent方法不會被呼叫,因為在View的dispatchTouchEvent中onTouch優先於onTouchEvent執行;onClick方法也不會被呼叫,因為onClick是在onTouchEvent中回撥的

事件分發流程

  1. 當手指觸控式螢幕幕後,底層Input驅動從/dev/input/路徑下讀寫以event[NUMBER]為名的硬體輸入裝置節點獲取事件(可以通過adb shell getevent 檢視你的裝置下的節點,Android也是從這些節點獲取這些原始資料再封裝後提供給開發者使用;如果做遊戲開發可能就直接獲取這些原始資料自己處理了),經過一系列呼叫後傳遞到了DecorView的dispatchTouchEvent方法
  2. 在DecorView中,會通過Window的內部介面Callback,將事件繼續傳遞,因為Activity實現了該介面,故事件分發到Activity;Activity獲取到事件後,在dispatchTouchEvent方法中先將事件分發到該Activity所在的window,實際型別是PhoneWindow,這個window又將事件交給它的頂級view即DecorView處理
  3. DecorView是FrameLayout的子類,即ViewGroup的子類,自己沒有處理,只是繼續將事件交由ViewGroup處理;就這樣一個事件就從Activity轉到了ViewGroup
  4. ViewGroup在dispatchTouchEvent方法進行分發,如果自己的onInterceptTouchEvent方法攔截此次事件,就把事件交給自身的onTouchEvent方法處理;反之遍歷自己的子View,繼續將事件分發下去,只要有一個子View消費了這個事件,那就停止遍歷
  5. 事件會傳遞到子View的dispatchTouchEvent方法,如果給子View註冊了OnTouchListener,且返回true,那事件分發就到此結束;反之就會繼續將事件傳遞到子View的onTouchEvent方法
  6. 子View會在ACTION_UP事件中回撥View的onClick監聽,如果子View沒有消費此次事件,就會按照分發流程反過來傳遞回去到Activity;如果到了Activity還沒人消費(包括Activity自己),那就會銷燬這個事件

事件分發原始碼

以下原始碼基於API24

對應上面的流程,當有Touch事件後,步驟如下

DecorView.dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

此處的cb指的是window內部的Callback介面,Activity實現了這個介面,接下來進入Activity

Activity.dispatchTouchEvent

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

這個方法就是Activity用來處理觸控式螢幕事件,我們可以重寫這個方法,並返回true/false,這樣在事件分發到window前就能進行攔截,Activity內的ViewGroup或者View將收不到事件

一個觸控式螢幕事件都是以ACTION_DOWN開始,那就肯定會進入 onUserInteraction()方法

public void onUserInteraction() {
}

這是一個空方法,它的呼叫時機如下: 當一個按鍵事件,觸控式螢幕事件或者trackball事件分發到Activity的時候,它就會被呼叫;如果你希望在Activity正在執行的時候瞭解使用者和裝置用某種方式互動,可以重寫這個方法;不過需要注意的是這個方法只響應touch-down這種觸控手勢,不會響應接下來的touch-move和touch-up

與這個方法相對應的一個方法就是onUserLeaveHint,它同樣也是一個空方法,它的呼叫時機如下: 當在使用者操作的情況下Activity進入後臺,這個方法會作為Activity生命週期的一部分被呼叫;比如,使用者按下home鍵,當前Activity就會進入後臺,它就會被呼叫,並且是在onPause之前呼叫;但是比如有電話打進來了導致Activity被動進入後臺,這個方法就不會被呼叫

接下來進入第二個if語句

getWindow().superDispatchTouchEvent

通過getWindow()獲取到的是一個Window物件,但是它是在Activity的attach方法中進行例項化,實際型別是PhoneWindow,也是在這裡實現了Callback介面

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {
            ......
			mWindow = new PhoneWindow(this, window);
			mWindow.setCallback(this);
			......
}

這裡就轉到PhoneWindow,如下

PhoneWindow.superDispatchTouchEvent

//這是視窗的頂層檢視
private DecorView mDecor
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

DecorView .superDispatchTouchEvent

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

DecorView 是FrameLayout的子類,FrameLayout又是ViewGroup的子類,這裡就會走到ViewGroup

ViewGroup.dispatchTouchEvent

		@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
    
    	//用於除錯目的的一致性驗證程式
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

		//這個變數用於標記事件是否被消費
        boolean handled = false;
        
        //根據應用安全策略過濾觸控事件
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 處理initial down發生後的初始化操作
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 新的ACTION_DOWN事件來了,需要取消並清除之前的touch Targets
                //清空掉mFirstTouchTarget
                cancelAndClearTouchTargets(ev);
                //重置觸控狀態
                resetTouchState();
            }

            //標記是否攔截事件
            final boolean intercepted;
            
            // 當ACTION_DOWN來了或者已經發生過ACTION_DOWN,並且將mFirstTouchTarget賦值 就檢測ViewGroup是否需要攔截事件.
            //只有發生過ACTION_DOWN事件,mFirstTouchTarget != null
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    
                //子View可以通過呼叫父View的requestDisallowInterceptTouchEvent方法設定mGroupFlags值
                //以此告訴父View是否攔截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //如果子view 沒有告訴父View別攔截事件,那父View就判斷自己是否需要攔截事件
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // 重新恢復action  以防被改變了
                } else {
                		//這裡表明子View告訴父View不要攔截事件
                    intercepted = false;
                }
            } else {
            	//當mFirstTouchTarget=null(沒有子View被分配處理),且不是initial down事件時(事件已經初始化過了),ViewGroup繼續攔截觸控
                //繼續設定為true
                intercepted = true;
            }



            // 如果當前事件是ACTION_CANCEL,或者view.mPrivateFlags被設定了PFLAG_CANCEL_NEXT_UP_EVENT
            //那麼當前事件就取消了
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            //split表示當前的ViewGroup是不是支援分割MotionEvent到不同的View當中
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            //新的TouchTarget
            TouchTarget newTouchTarget = null;
            //是否把事件分發給了新的TouchTarget
            boolean alreadyDispatchedToNewTouchTarget = false;
            //不取消事件,同時不攔截事件才進入該區域
            if (!canceled && !intercepted) {

                //把事件分發給所有的子檢視,尋找可以獲取焦點的檢視
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;
								
				//如果是這三種事件就得遍歷子View
                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;

                    // 對於這個PointerId 清空更早的 touch targets 
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    //如果當前ViewGroup有子View且newTouchTarget=null
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        
                        // 在視圖裡從前到後掃描一遍獲取可以接收事件的子View
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        
                        //遍歷所有子View,找到一個來接收事件
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            //如果當前子View沒有獲取焦點,則跳過這個子View
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

							//如果當前子View不可見且沒有播放動畫 或者 不在觸控點範圍內,跳過這個子View
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

							//如果在觸控目標列表找到了與該子View對應的TouchTarget,說明這個view正在接收事件,不需要再遍歷,直接退出
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            

                            resetCancelNextUpFlag(child);
                            
                            //子view處於觸控位置,就將事件分發給子View,如果該子View返回true,說明消費了這個事件,就跳出遍歷
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 獲取TouchDown的時間點
                                mLastTouchDownTime = ev.getDownTime();
                                // 獲取TouchDown的Index
                                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;
                                }
                                //獲取TouchDown的x,y座標
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //新增到觸控目標列表 同時給mFirstTouchTarget賦值
                                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();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // 到這裡說明沒有子View接收事件,那就把最近一次的觸控目標賦值給newTouchTarget
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // mFirstTouchTarget賦值是在通過addTouchTarget方法獲取的;
        		// 只有處理ACTION_DOWN事件,才會進入addTouchTarget方法。
        		// 這也正是當View沒有消費ACTION_DOWN事件,則不會接收其他MOVE,UP等事件的原因
            if (mFirstTouchTarget == null) {
                // 那就只能ViewGroup自己處理事件了
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // 到這裡就說明有子View接收了ACTION_DOWN事件,那後續的move up等事件就繼續分發給這個觸控目標
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                    	//如果view.mPrivateFlags被設定了PFLAG_CANCEL_NEXT_UP_EVENT 或者事件被ViewGroup攔截了
                    	//那子View需要取消事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                                
                        //繼續分發事件給子View
                        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;
                }
            }

   
            //當發生擡起或取消事件,更新觸控目標列表
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            	//如果是多點觸控下的手指擡起事件,就要根據idBit從TouchTarget中移除掉對應的Pointer(觸控點)
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

這個方法內容有點多,需要拆分開分析

第一步:事件初始化

第一個進來的是ACTION_DOWN事件,那需要做一些初始化:

  • 第一件事就是清空所有的 TouchTarget,並將mFirstTouchTarget值為null;mFirstTouchTarget的型別也是TouchTarget,是ViewGroup的一個內部類,描述一個觸控的檢視和它捕獲的指標的id;mFirstTouchTarget 可以理解為如果事件由子View去處理時mFirstTouchTarget 會被賦值並指向子View
  • 第二件事是重置狀態值,通過FLAG_DISALLOW_INTERCEPT重置mGroupFlags值
ViewGroup.cancelAndClearTouchTargets
/**
  	* 取消和清空所有的 touch targets.
  	*/
	private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
	}
    
    /**
     * 清空所有的 touch targets.
     */
    private void clearTouchTargets() {
        TouchTarget target = mFirstTouchTarget;
        if (target != null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while (target != null);
            mFirstTouchTarget = null;
        }
    }
    
    /**
     * 重置所有觸控狀態以準備新週期.
     */
    private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

第二步:攔截判斷

接下來就需要判斷是否需要攔截事件:

首先看條件是

			//標記是否攔截事件
            final boolean intercepted;
            
            // 當ACTION_DOWN來了或者已經發生過ACTION_DOWN,並且將mFirstTouchTarget賦值 就檢測ViewGroup是否需要攔截事件.
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                    
                //子View可以通過呼叫父View的requestDisallowInterceptTouchEvent方法設定mGroupFlags值
                //以此告訴父View是否攔截事件
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //如果子view 沒有告訴父View別攔截事件,那父View就判斷自己是否需要攔截事件
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // 重新恢復action  以防被改變了
                } else {
                	//這裡表明子View告訴父View不要攔截事件
                    intercepted = false;
                }
            } else {
            	//當mFirstTouchTarget=null(沒有子View被分配處理),且不是initial down事件時(事件已經初始化過了),ViewGroup繼續攔截觸控
                //繼續設定為true
                intercepted = true;
            }
  • 當事件是ACTION_DOWN或者 mFirstTouchTarget != null才會去判斷要不要攔截,由第一步可知,當事件是ACTION_DOWN的時候,mFirstTouchTarget 肯定為null,所以這裡只有兩種情況會進入:ACTION_DOWN事件來了需要判斷攔截;ACTION_DOWN事件中如果有子View接收了事件(這樣mFirstTouchTarget 就賦值了),那接下來的事件也需要判斷是否攔截事件
  • 上面條件的反向邏輯就是事件是ACTION_DOWN事件以後的事件(比如move或者up)且mFirstTouchTarget 為null,說明在ACTION_DOWN事件中就判斷了需要攔截事件或者沒有子View處理事件,那接下來的事件就沒必要分發了,繼續攔截

第一個if語句裡面是攔截判斷邏輯是

  • 先通過與運算獲得mGroupFlags 的值,子view可以通過呼叫父view的requestDisallowInterceptTouchEvent方法設定mGroupFlags 的值,告訴父view不要攔截事件
  • 如果disallowIntercept 為true,說明子view要求父view不要攔截,就將intercepted 設定false
  • 如果disallowIntercept 為false,表明子view沒有提出不要攔截請求,那就呼叫onInterceptTouchEvent看看自己是不是需要攔截事件
ViewGroup.requestDisallowInterceptTouchEvent
@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // 如果已經設定過了,就返回
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // 依次告訴父view
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }
   
ViewGroup.onInterceptTouchEvent
 /**
     * ViewGroup可在這個方法裡攔截所有觸控事件,預設是不攔截事件,開發者可以重寫這個方法決定是否要攔截
     * 如下四個條件都成立,返回true,攔截事件
     * 第一個:觸控事件是否來自滑鼠指標裝置
     * 第二個:觸控事件是否是ACTION_DOWN
     * 第三個:檢查是否按下了滑鼠或手寫筆按鈕(或按鈕組合),也就是說使用者必須實際按下
     * 第四個:觸控點是否在滾動條上
     */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

第三步:ACTION_DOWN事件分發

接下來就需要遍歷子View,然後將ACTION_DOWN事件分發給能接收事件的子View

  • 如果當前子View沒有獲取焦點,則跳過這個子View
  • 如果當前子View不可見且沒有播放動畫 或者 不在觸控點範圍內,跳過這個子View
  • 如果在觸控目標列表找到了與該子View對應的TouchTarget,說明這個view正在接收事件,不需要再遍歷,直接退出
  • 如果子view處於觸控位置,就呼叫dispatchTransformedTouchEvent方法將事件分發給子View,如果該方法返回true,說明子View消費了這個事件,那就不需要再尋找子view接收事件了,跳出遍歷
ViewGroup.dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // 發生取消操作時,不再執行後續的任何操作
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    //由於某些原因,發生不一致的操作,那麼將拋棄該事件
    if (newPointerIdBits == 0) {
        return false;
    }

    //分發的主要區域
    final MotionEvent transformedEvent;
    //判斷預期的pointer id與事件的pointer id是否相等
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                //不存在子檢視時,ViewGroup呼叫View.dispatchTouchEvent分發事件,再呼叫ViewGroup.onTouchEvent來處理事件
                handled = super.dispatchTouchEvent(event); 
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
                //將觸控事件分發給子ViewGroup或View;
                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY); //調整該事件的位置
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event); //拷貝該事件,來建立一個新的MotionEvent
    } else {
        //分離事件,獲取包含newPointerIdBits的MotionEvent
        transformedEvent = event.split(newPointerIdBits);
    }

    if (child == null) {
        //不存在子檢視時,ViewGroup呼叫View.dispatchTouchEvent分發事件,再呼叫ViewGroup.onTouchEvent來處理事件
        handled = super.dispatchTouchEvent(transformedEvent); 
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            //將該檢視的矩陣進行轉換
            transformedEvent.transform(child.getInverseMatrix());
        }
        //將觸控事件分發給子ViewGroup或View;
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    //回收transformedEvent
    transformedEvent.recycle();
    return handled;
}

該方法是ViewGroup真正處理事件的地方,分發子View來消費事件,過濾掉不相干的pointer ids。當子檢視為null時,MotionEvent將會發送給該ViewGroup;不為null,最終呼叫View.dispatchTouchEvent方法來分發事件。

這個方法呼叫完畢,回到ViewGroup.dispatchTouchEvent會呼叫addTouchTarget方法

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

可以看到在這裡給 mFirstTouchTarget賦值了

當子控制元件消費了事件,mFirstTouchTarget不為空;當子控制元件沒有消費事件或者被攔截,mFirstTouchTarget為空

第四步:ACTION_MOVE ACTION_UP事件分發

在第三步過後,ViewGroup可能會找到有子View消費事件

  • 如果事件被攔截,mFirstTouchTarget==null,那接下來的事件最終呼叫View.dispatchTouchEvent方法來分發事件
  • 如果ViewGroup沒有子View,mFirstTouchTarget==null,那接下來同上
  • 如果有子View,但是子View沒消費事件,mFirstTouchTarget==null,那接下來同上
  • 如果有子View,且子View消費了ACTION_DOWN事件,但是在dispatchTouchEvent返回了false(即dispatchTransformedTouchEvent返回false,那addTouchTarget就不會被呼叫),mFirstTouchTarget==null,那接下來的處理也同上
  • 接下來就是mFirstTouchTarget不為null了,那就需要將後續事件分發給消費ACTION_DOWN事件的View了

通過對ViewGroup.dispatchTouchEvent方法的分析,我們知道不管有沒有子View消費事件,最終事件都會進入View.dispatchTouchEvent方法,那我們繼續一探究竟

View.dispatchTouchEvent

/**
     * 將觸控事件向下傳遞到目標檢視,或者這個View是目標檢視。
     *
     * @return 返回true 表示消費了事件,反之返回false 
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
				
				......

        boolean result = false;

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //在Down事件之前,如果存在滾動操作則停止。不存在則不進行操作
            stopNestedScroll();
        }

		//過濾觸控事件以應用安全策略
        if (onFilterTouchEventForSecurity(event)) {
        
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }

            ListenerInfo li = mListenerInfo;
            // 如果給View設定了OnTouchListener
            //且該view是可點選的
            //且OnTouchListener.onTouch返回true
            //那說明該View消費了該事件,返回true
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

			//如果OnTouchListener.onTouch沒有消費事件且View的onTouchEvent方法返回true,那返回true
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        // 如果這是手勢的結束,則在巢狀滾動後清理;
        //如果我們嘗試了ACTION_DOWN但是我們不想要其餘的手勢,也要取消它。
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

這裡有兩點比較重要

  • 如果開發者設定OnTouchListener監聽,且在onTouch方法返回true,說明view消費了事件
  • 如果沒有設定監聽,那就呼叫View的onTouchEvent方法去處理事件

可以看出OnTouchListener.onTouch是優先於onTouchEvent執行的,只要前者返回true,那後者就不會執行了,事件到此為止結束

接下來看看onTouchEvent的邏輯

View.onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

		//如果這個view是禁用的,可以通過setEnabled()設定是否禁用
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // 即使設定了禁用,但是隻要這個view滿足CLICKABLE ,LONG_CLICKABLE ,CONTEXT_CLICKABLE其中一種
            //任然算消費該事件,只是沒有響應而已
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

		 //當View狀態為ENABLED
		//且這個view滿足CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE其中一種,就消費這個事件
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // 獲取焦點處於可觸控模式
                        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) {
                            //這是Tap操作,移除長按回調方法
                            removeLongPressCallback();

                            // 如果處於按下狀態盡執行點選操作
                            if (!focusTaken) {
                                // 使用Runnable併發布而不是直接呼叫performClick 
                                //這樣可以在單擊操作開始之前更新檢視的其他可視狀態
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                //呼叫View.OnClickListener
                                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:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // 確定是否處於可滾動的檢視內
                    boolean isInScrollingContainer = isInScrollingContainer();

                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        //當處於可滾動檢視內,則延遲TAP_TIMEOUT,再反饋按壓狀態,用來判斷使用者是否想要滾動。預設延時為100ms
                    		postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

                    } else {
                        //當不再滾動檢視內,則立刻反饋按壓狀態
                        setPressed(true, x, y);
                        //檢測是否是長按,如果長按,回撥OnLongClickListener.onLongClick
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

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

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;
    }

這裡有幾點需要注意

  1. 只要是這個view滿足CLICKABLE ,LONG_CLICKABLE ,CONTEXT_CLICKABLE其中一種,不管通過setEnabled()設定禁用還是可用,都會返回true,認為消費事件
  2. View的longClickable預設為false,clickable需要區分情況,如Button的clickable預設為true,而TextView的clickable預設為false;但是View的setOnClickListener會預設將View的clickable設定成true,View的setOnLongClickListener同樣會將View的longClickable設定成true
  3. 在ACTION_DOWN操作中,如果是長按,回撥OnLongClickListener.onLongClick
  4. 在ACTION_UP操作中,回撥OnClickListener.onClick

Activity.OnTouchEvent

所有流程走完,假如沒有一個View消費事件,那最終會回到Activity.OnTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        //迴圈判斷是否有ViewGroup或者View消費事件,如果沒有,事件回到activity
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    
public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

事件分發流程圖

這裡借用網路中的圖片

在這裡插入圖片描述

注意點

  1. 觸控事件由Activity.dispatchTouchEvent先處理;再一層層往下分發,當中間的ViewGroup都不消費或者攔截時,進入最底層的View,開始由最底層的OnTouchEvent來處理,如果一直不消費,則最後返回到Activity.OnTouchEvent
  2. 只有ViewGroup有onInterceptTouchEvent攔截方法;在分發過程中,中間任何一層ViewGroup都可以直接攔截,則不再往下分發,而是交由發生攔截操作的ViewGroup的OnTouchEvent來處理
  3. 子View可呼叫父ViewGroup的requestDisallowInterceptTouchEvent方法,來設定disallowIntercept=true,從而阻止父ViewGroup的onInterceptTouchEvent攔截操作
  4. OnTouchEvent由下往上冒泡時,當中間任何一層的OnTouchEvent消費該事件,則不再往上傳遞,表示事件已消費
  5. 如果View沒有消費ACTION_DOWN事件,則之後的ACTION_MOVE等事件都將無法接收
  6. 不管View是DISABLED(禁用)的還是ENABLED(可用)的,只要是CLICKABLE (可點選),LONG_CLICKABLE(可長按) ,都會消費事件
  7. View的setOnClickListener會預設將View的clickable設定成true,View的setOnLongClickListener同樣會將View的longClickable設定成true;所有View的setClickable和setLongClickable最好在兩個監聽方法後呼叫
  8. onTouch優先於onTouchEvent執行,onClick和onLongClick在onTouchEvent中被呼叫,且onLongClick優先於onClick被執行;如果onTouch返回true,就不會執行onTouchEvent;onTouch只有View設定了OnTouchListener,且是enable的才執行該方法

至此,事件分發機制及原始碼分析就結束了