文章中出現的源碼均基於8.0java
事件分發機制不只僅是核心知識點更是難點,而且仍是View的一大難題滑動衝突解決方法的理論基礎,所以掌握好View的事件分發機制是十分重要的。android
事件分發的對象是點擊事件(Touch事件),而當用戶觸摸屏幕時,將產生點擊事件。ios
事件類型分爲四種,以下所示:markdown
類型 | 說明 |
---|---|
MotionEvent.ACTION_DOWN | 手指剛接觸屏幕,通常爲事件的開始 |
MotionEvent.ACTION_MOVE | 手指在屏幕移動,在移動的過程當中會產生多個move事件 |
MotionEvent.ACTION_UP | 手指從屏幕上鬆開的一瞬間 |
MotionEvent.ACTION_CANCEL | 結束事件,非人爲緣由 |
同一個事件序列:指從手指剛接觸屏幕,到手指離開屏幕的那一刻結束,在這一過程產生的一系列事件,這個序列通常以down事件開始,中間含有多個move事件,最終以up事件結束app
事件分發的本質,其實就是將點擊事件(MotionEvent)傳遞到某個具體的View處理的整個過程ide
事件傳遞的順序:Activity->Window->DecorView->ViewGroup->View。一個點擊事件發生後,老是先傳遞給當前的Activity,而後經過Window傳給DecorView再傳給ViewGroup,最終傳到View。oop
在《開發藝術探索》中的事件分發的順序是:Activity->Window->View,而有的博客上的順序是:Activity->ViewGroup->View,不過其實二者是同樣的(下列會從源碼進行分析)。Window是抽象類,其惟一實現類爲PhoneWindow,PhoneWindow將事件直接傳遞給DecorView,而DecorView繼承FrameLayout,FrameLayout又是ViewGroup的子類,因此兜兜轉轉最後也能夠認爲Window事件分發的實現實際上是ViewGroup來實現的。因此也能夠認爲事件傳遞的順序是:Activity->ViewGroup->View。源碼分析
View的事件分發機制主要由事件分發->事件攔截->事件處理三步來進行邏輯控制,很巧的這三步恰好對應了三個核心方法佈局
用來進行事件的分發,若是事件可以傳遞給當前View,則該方法必定會被調用。返回結果受當前View的onTouchEvent和下級的dispatchTouchEvent的影響,表示是否消耗當前事件。動畫
原型:public boolean dispatchTouchEvent(MotionEvent ev)
return:
需注意的是在Activity,ViewGroup,View中只有ViewGroup有這個方法。故一旦有點擊事件傳遞給View,則View的onTouchEvent方法就會被調用
在dispatchTouch Event內部使用,用來判斷是否攔截事件。若是當前View攔截了某個事件,那麼該事件序列的其它方法也由當前View處理,故該方法不會被再次調用,由於已經無須詢問它是否要攔截該事件。
原型:public boolean onInterceptTouchEvent(MotionEvent ev)
return:
在dispatchTouchEvent中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,若是不消耗,則在同一事件序列中,當前View沒法再接受到剩下的事件,而且事件將從新交給它的父元素處理,即父元素的onTouchEvent會被調用
原型:public boolean onTouchEvent(MotionEvent ev)
return:
在分析事件分發機制時,應該從事件分發的順序入手一步一步解剖。從上文咱們知道事件分發順序爲:Activity->Window->DecorView->ViewGroup->View。因爲Window與DecorView能夠看做是Activity->ViewGroup的過程,故這裏將從三部分經過源碼來分析事件分發機制:
咱們知道,當一個點擊事件發生時,事件老是最早傳遞到當前Activity中,由Activity的dispatchTouchEvent來進行事件分發。而Activity會將事件傳遞給Window對象來分發,Window對象再傳遞給DecorView。下面將進行源碼分析來驗證這個過程:
源碼:Activity#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) { //點擊事件的開始通常爲按下事件,因此老是true if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } //若是Activity所屬Window的dispatchTouchEvent返回了ture //則Activity.dispatchTouchEvent返回ture,點擊事件中止往下傳遞 if (getWindow().superDispatchTouchEvent(ev)) { return true; } //若是Window的dispatchTouchEvent返回了false,則點擊事件傳遞給Activity.onTouchEvent return onTouchEvent(ev); } 複製代碼
上面代碼爲Activity中的dispatchTouchEvent的源碼,經過源碼咱們能夠知道當點擊事件發生時,首先會執行onUserInteraction();這個方法又是什麼呢?不急,咱們跟蹤下去。
//空方法,當該Activity在棧頂時,觸屏點擊home,back,menu會觸發此方法 public void onUserInteraction() { } 複製代碼
從源碼中能夠看出在Activity中該方法爲空方法,當該Activity在棧頂時,觸屏點擊home,back,menu會觸發此方法,因此這個方法能夠實現屏保功能。讓咱們回到Activity中的dispatchTouchEvent方法中,接着調用了getWindow().superDispatchTouchEvent(ev)方法將事件交給Activity所附屬的Window進行分發,若是最終事件被消耗了,則返回true,若是事件沒人處理,則Activity調用在本身的onTouchEvent()方法來處理事件。
getWindow是一個Window對象,在Window源碼中咱們能夠發現其實Window就是一個抽象類,顯而易見其方法天然是抽象方法,因此咱們必須找出其具體實現類。
源碼:Window#superDispatchTouchEvent
/** * 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. */ public abstract class Window { ... //抽象方法 public abstract boolean superDispatchTouchEvent(MotionEvent event); ... } 複製代碼
從源碼中咱們能夠發如今Window類前面的註釋中是有解釋的,這時候就要考驗偶們的英語能力遼!其實整體上來講仍是挺容易理解的,其實咱們只要關注後面一部分的註釋就行。
The only existing implementation of this abstract class isandroid.view.PhoneWindow, which you should instantiate when needing a Window.
從這裏咱們能夠知道它的惟一實現類就是PhoneWindow,廢話很少說,咱們直接看看PhoneWindow中superDispatchTouchEvent方法的實現是如何的呢?
源碼:PhoneWindow#superDispatchTouchEvent
private DecorView mDecor; @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } 複製代碼
從源碼中能夠發現PhoneWindow將事件直接傳遞給了DecorView,而這個DecorView又是何方聖神呢?以下所示
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks { ...... public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } ...... } @RemoteView public class FrameLayout extends ViewGroup { ...... } 複製代碼
其實DecorView就是咱們經過setContentView設置佈局的父容器,咱們能夠經過getWindow().getDecorView().findViewById(android.R.id.content).getChildAt(0)這個方式就能獲取到setContentView中設置的佈局。從DecorView的源碼中能夠發現DecorView是繼承FrameLayout,而FrameLayout又是繼承ViewGroup的,故DecorView的間接父類爲ViewGroup,在DecorView中superDispatchTouchEvent方法是使用super來調用父類的dispatchTouchEvent,故等於調用ViewGroup的dispatchTouchEvent方法(從源碼中咱們能夠得知FrameLayout並無dispatchTouchEvent這個方法),因而DecorView將事件傳遞到了ViewGroup去處理。也能夠這麼說,事件已經傳遞到了頂級View也就是Activity中經過setContentView所設置的View(頂級View一般爲ViewGroup)。
流程圖以下:
從上面Activity事件的分發機制咱們能夠知道,ViewGroup事件分發機制是從dispatchTouchEvent()開始的,因此咱們從這部分的源碼開始分析,因爲該方法代碼量不少,下面將根據須要貼出相關代碼:
源碼:ViewGroup#dispatchTouchEvent
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ........ // Check for interception. final boolean intercepted; //是否攔截 /* * 當事件由ViewGroup的子元素處理時,mFirstTouchTarget會被賦值並指向子元素 */ 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。mFirstTouchTarget 從後面的代碼才能知道其做用。它的做用就是:當事件被ViewGroup的某個子View處理時,mFirstTouchTarget 就會指向這個子View。因此當事件被這個ViewGroup攔截時,子類就不會處理這個事件,所以mFirstTouchTarget =null,那麼這個時候ACTION_MOVE和ACTION_UP事件到來時,因爲判斷條件爲false,將致使ViewGroup的onInterceptTouchEvent不會再被調用,而後intercepted被賦予true,因此同一事件序列的其它事件都會默認交給該ViewGroup來處理。在上面源碼中咱們還能夠發現這麼一句語句:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 複製代碼
FLAG_DISALLOW_INTERCEPT是個標記位,這個標記位是經過 requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法來設置的,通常用在子View中。若是FLAG_DISALLOW_INTERCEPT被設置後,ViewGroup將沒法攔截除了ACTION_DOWN之外的其它點擊事件,這是由於ViewGroup在分發事件中,若是是ACTION_DOWN事件,將會重置FLAG_DISALLOW_INTERCEPT這個標記位。讓咱們來看看源碼。
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(); //對FLAG_DISALLOW_INTERCWPT進行重置 } 複製代碼
上面源碼是在判斷是否攔截的前面的,因此可以重置標記位,從這裏咱們也能夠發現,當點擊事件爲ACTION_DOWN時,ViewGroup老是會調用本身的onInterceptTouchEvent來詢問本身是否要攔截事件。
requestDisallowInterceptTouchEvent方法針對的是ACTION_DOWN之外的其餘事件,而且是在不攔截ACTION_DOWN事件的狀況下才會起做用。
接下來讓咱們瞧瞧ViewGroup再也不攔截事件的時候,事件的分發狀況,源碼以下:
final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { //遍歷ViewGroup的全部子元素 final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } /** ** 判斷子元素是否可以接受到點擊事件: ** 子元素是否在播動畫和點擊事件的座標是否落在子元素的區域內 ** 若是某個元素知足這兩個條件,則事件交給它來處理 **/ if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //dispatchTransformedTouchEvent實際調用的是子元素的dispatchTouchEvent方法 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); //記錄ACTION_DOWN事件已經被處理了 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); } 複製代碼
從上面代碼中咱們能夠知道,不攔截事件時,首先會遍歷ViewGroup的全部子元素,而後判斷子元素是否可以接受到點擊事件。判斷的依據是:子元素是否在播放動畫和點擊事件的座標是否落在子元素的區域內。若是找到一個目標子View來處理事件時,則調用dispatchTransformedTouchEvent()方法。來看看這個方法重要實現邏輯:
if (child == null) { handled = super.dispatchTouchEvent(event); } else { ........ handled = child.dispatchTouchEvent(event); } 複製代碼
能夠發現因爲在上面中的child並不等於null,因此將直接調用子元素的dispatchTouchEvent方法,使得事件傳遞到子View上,而後繼續分發。
你覺得這樣就結束了?答案確定是沒有,從上面源碼中咱們能夠發現當子元素的dispatchTouchEvent返回true後,還有相應操做:
newTouchTarget = addTouchTarget(child, idBitsToAssign); //記錄ACTION_DOWN事件已經被處理了 alreadyDispatchedToNewTouchTarget = true; break; 複製代碼
這幾行代碼完成了mFirstTouchTarget的賦值並終止了對子元素的遍歷。若是子元素的dispatchTouchEvent返回false,則ViewGroup就會把事件分發給下一個元素(若是還有子元素的話),看到這你也許又納悶了,mFirstTouchTarget的賦值?怎麼沒看見mFirstTouchTarget的影子呢,答案其實在addTouchTarget這個方法中:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; } 複製代碼
這個方法能夠看出其實mFirstTouchTarget是一種單鏈表結構,首先根據座標點找到了目標子View,而後將子View放在鏈表頭上,從而實現了mFirstTouchTarget!=null。
到這裏就完成了ViewGroup一輪的事件分發了,然而尚未結束,若是遍歷了全部子元素後事件都沒有被合適處理呢?
沒有合適處理包括了兩種狀況:
那麼這時候ViewGroup將會本身處理點擊事件(當ViewGroup攔截了事件時也是作一樣的處理)。
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } 複製代碼
能夠看出這時候仍是調用了dispatchTransformedTouchEvent方法,不過這時候第三個參數不是child而是null,因此會調用下面這句代碼:
//dispatchTransformedTouchEvent handled = super.dispatchTouchEvent(event); 複製代碼
super其實就是View中的dispatchTouchEvent方法,因此點擊事件開始交由View來處理。
ViewGroup並無調用onTouchEvent,ViewGroup也沒有去重寫onTouchEvent
流程圖以下:
從上面對ViewGroup事件分發機制可知,View事件分發機制是從dispatchTouchEvent開始的。
源碼:View#dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... //判斷窗口是否被遮擋,若是被遮擋則返回false,好比有時候兩個View是會重疊的,致使其中一個被遮擋了。 if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; //判斷是否設置了mOnTouchListener,若是設置了onTouchListener,且onTouch方法返回了ture, //則result = true if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //在result = ture狀況下,就不會調用onTouchEvent,可見onTouchListener的優先級高於onTouchEvent if (!result && onTouchEvent(event)) { result = true; } } ...... return result; } 複製代碼
因爲View是一個單獨元素,沒有子元素能夠繼續向下傳遞事件,只能本身處理事件,因此代碼也會明顯減小。從上面的源碼中咱們能夠看到View對點擊事件的處理過程,result表明是否消耗該事件,而後進行onTouchListener的判斷,若是onTouchListenter中的onTouch方法返回了true,那麼就不會再調用onTouchEvent方法,因而可知onTouchListener的優先級高於onTouchEvent。
而後來看看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(); 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設置了代理,將會執行代理的onTouchEvent方法 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ..... boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; ...... //通過種種判斷 performClickInternal(); break; case MotionEvent.ACTION_DOWN: .... break; case MotionEvent.ACTION_CANCEL: .... break; case MotionEvent.ACTION_MOVE: .... break; } //若該控件可點擊,就必定返回true return true; } //若該控件不可點擊,就必定返回false return false; } 複製代碼
從上面代碼能夠知道只要View的CLICKABLE,LONG_CLICKABLE,CONTEXT_CLICKABLE有一個爲true,那麼它就會消耗該事件,無論它是否是DISABLE狀態。而後假如控件可點擊,就對四種事件類型進行相對應的處理,這裏值得一說的是ACTION_UP事件,從源碼中能夠發如今ACTION_UP事件發生時,會觸發performClickInternal方法。這個方法內部實現又是怎樣的呢?以下:
private boolean performClickInternal() { // Must notify autofill manager before performing the click actions to avoid scenarios where // the app has a click listener that changes the state of views the autofill service might // be interested on. notifyAutofillManagerOnClick(); return performClick(); } 複製代碼
咱們能夠發現最後仍是會調用performClick,而在performClick內部中:
public boolean performClick() { // We still need to call this method to handle the cases where performClick() was called // externally, instead of through performClickInternal() notifyAutofillManagerOnClick(); 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; } 複製代碼
只要咱們經過setOnClickListener爲View註冊點擊事件,那麼就會給li.mOnClickListener賦值,則會調用onClick方法。
流程圖以下:
從流程圖咱們能夠發現onTouch,onTouchEvent,onClick的優先級:onTouch>onTouchEvent>onClick
到這裏,咱們已經經過源碼將點擊事件的分發機制梳理一遍了。事件分發的大體過程以下:
參考博客: