帶你從源碼一步步分析Android View面試中的事件分發流程


在這裏插入圖片描述

前言

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

事件分發機制

事件分發原因

Android中頁面上的View是以樹型結構顯示的,View會重疊在一起,當我們點擊的地方有多個View可以響應的時候,這個點擊事件應該給誰呢?爲了解決這個問題,Google設計了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. 如果dispatchTouchEvent在進行事件分發的時候,View沒有消費ACTION_DOWN事件,即返回true,則之後的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的才執行該方法

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章