View 的事件分發攔截機制
這一個知識點也是寫爛了的,可是作為 Android 開發者又不得不學習這部分,學習了呢,總覺得要寫點東西出來才覺得有感覺,得,就有這一篇文章了。
API 27
流程介紹
在單點觸控中,我們對螢幕的點選,滑動,抬起等一系的動作都是由一個一個MotionEvent物件組成的觸控事件。MotionEvent 是對一個對一個事件的封裝,裡面包括動作、座標等等資訊,根據不同動作,主要有以下三種事件型別:
- ACTION_DOWN:手指剛接觸螢幕,按下去的那一瞬間產生該事件
- ACTION_MOVE:手指在螢幕上移動時候產生該事件
- ACTION_UP:手指從螢幕上鬆開的瞬間產生該事件
要要注意觸控事件不是獨立的,而是成組的,每一組事件都是由按下事件開始的,由抬起事件或者取消事件結束。我們把 由 ACTION_DOWN 開始(按下),ACTION_UP (抬起)或者 ACTION_CANCEL(取消) 結束 的一組事件稱為 事件序列 或者說 事件流 。取消事件是一種特殊的事件,它對應的是事件序列非人為的提前結束。
舉個例子:
點選事件:ACTION_DOWN -> ACTION_UP
滑動事件:ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP
Android 每產生一個 TouchEvent 事件,他會先問最表面是否消費,如果不消費就交給他的ViewGroup,一層一層向上傳遞,最終被 消費 掉(消費就是以為著事件被處理了,程式碼體現為返回值,true為消費,false為不消費, 消費後不再傳遞 )。TouchEvent 不斷產生,事件就會不斷分發,處理,實現對事件對應的操作進行判斷和反饋處理。
還是舉個栗子:
一個button被點選一下,就會產生兩個 TouchEvent 事件,當第一個 TouchEvent 產生,button 發現自己被按下,背景風格變成按下狀態,如水波紋、顏色變深等。當第二個Up 的 TouchEvent 產生、分發的時候,button判別自己被點選,背景風格恢復預設狀態,並且如果設定了 ClickListener
的話,呼叫 OnClick
方法。
那麼如果你的ViewGroup裡面不止一個View呢(不是廢話嗎),不止一個ViewGroup呢?那是不是我就要制定一個機制來決定誰來處理這個事件啊?安排
當事件剛觸控到螢幕的時候,即 ACTION_DOWN 這個 MotionEvent 產生的時候,如果ViewGroup中的View消費(返回true),就將這個View記錄下來。 後續這一個事件流都直接交給它處理。

事件分發機制-簡圖.png
其實只有 ACTION_DOWN 事件需要返回 true,其後的像 UP啊,Move啊,他們的返回值並沒有什麼影響,但是還是推薦都寫成true,降低維護成本。
當情況複雜,比如說你現在操作的是列表,點一下會觸發點選事件,滑一下就會滑動,那麼這樣的隔著一個View如何實現的呢?這就是依靠著的就是 事件攔截機制 。
我們將這個過程細分,當你觸控的時候(DOWN事件),這個事件其實是先傳到Activity、再傳到ViewGroup、最終再傳到 View,先問問ViewGroup你攔不攔截啊?一層一層的向下問,如果攔截呢,就直接交給他,如果不攔截呢?就直接往下傳,直到傳到底層的View,底層的View沒有攔截方法,直接問他消不消費,不消費,向上分發,問他的ViewGroup是否分發,如果消費就直接交給它消費掉。這樣的話,就可以把消費的權力先交給子View,在合適的時候父View可以馬上接管過來。
那麼滑動的過程呢?就是在DOWN事件發生的時候,先交給子View消費,當出現MOVE事件的時候,列表發現這個是滑動,需要自己處理,就攔截並且消費掉。但是這時候View還等著後續的事件流,就比如說背景風格還是按下狀態,那麼父View就會發給它一個cancel事件,讓他恢復狀態,並且後續事件交給攔截的父View來處理。

事件分發攔截機制-詳細圖解
始於 Activity
點選事件產生最先傳遞到當前的Activity,由Acivity的dispatchTouchEvent方法來對事件進行分發。
/** * Called to process touch screen events.You can override this to * intercept all touch screen events before they are dispatched to the * window.Be sure to call this implementation for touch screen events * that should be handled normally. * * @param ev The touch screen event. * * @return boolean Return true if this event was consumed. */ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
程式碼很簡單,我們來一行一行進行解析。最開始,就是就是判斷當前這個事件是否是按下這個事件( MotionEvent.ACTION_DOWN
),如果是,就執行一個空方法( onUserInteraction()
等待程式猿大爺重寫)
/** * Called whenever a key, touch, or trackball event is dispatched to the * activity.Implement this method if you wish to know that the user has * interacted with the device in some way while your activity is running. * This callback and {@link #onUserLeaveHint} are intended to help * activities manage status bar notifications intelligently; specifically, * for helping activities determine the proper time to cancel a notfication. * * <p>All calls to your activity's {@link #onUserLeaveHint} callback will * be accompanied by calls to {@link #onUserInteraction}.This * ensures that your activity will be told of relevant user activity such * as pulling down the notification pane and touching an item there. * * <p>Note that this callback will be invoked for the touch down action * that begins a touch gesture, but may not be invoked for the touch-moved * and touch-up actions that follow. * * @see #onUserLeaveHint() */ public void onUserInteraction() { }
這裡多說幾句,這個空方法是在哪些時候會呼叫呢?畢竟我們也是要重寫的嘛,那就必須知道其執行的時期: activity在分發各種事件的時候會呼叫該方法,旨在提供幫助Activity智慧地管理狀態列通知 。當此activity在棧頂時,觸屏點選按home,back,menu鍵等都會觸發此方法。下拉statubar、旋轉螢幕、鎖屏不會觸發此方法。所以它會用在屏保應用上,因為當你觸屏機器,就會立馬觸發一個事件,而這個事件又不太明確是什麼,正好屏保滿足此需求;或者對於一個Activity,控制多長時間沒有使用者點響應的時候,自己消失等。
我們接著往下看 getWindow().superDispatchTouchEvent(ev)
:
public Window getWindow() { return mWindow; }
直接返回當前介面的 mWindow,mWindow 是什麼啊,是 Window ,Window 我們都知道,是一個 抽象類,它的唯一實現類就是 PhoneWindow,那我們來點一下 superDispatchTouchEvent(MotionEvent)
/** * Used by custom windows, such as Dialog, to pass the touch screen event * further down the view hierarchy. Application developers should * not need to implement or call this. * */ public abstract boolean superDispatchTouchEvent(MotionEvent event);
Window 的抽象方法啊,那我們在 PhoneWindow找一找
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
哇,實現要不要就這麼簡單,直接由Window 直接傳遞給了 mDecor,mDecor是什麼啊?是 DecorView。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks { }
DecorView就是Window的頂級View,是一個ViewGroup,我們通過setContentView設定的View是它的子View(Activity的setContentView,最終是呼叫PhoneWindow的setContentView).
這裡放一張 Activity->檢視 的圖片

Activity 結構
是不是簡單幾步就實現了由Activity到ViewGroup的傳遞,這個中間傳遞者呢,就是Window。
上面傳遞到了 DecorView,他直接呼叫了 ViewGroup 的 dispatchTouchEvent()
進行分發了。
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
在陷入複雜的分發邏輯之前,我們先看 Acivity#dispatchTouchEvent留下的一個尾巴 -- 最後這個 return onTouchEvent(ev);
/** * Called when a touch screen event was not handled by any of the views * under it.This is most useful to process touch events that happen * outside of your window bounds, where there is no view to receive it. * * @param event The touch screen event being processed. * * @return Return true if you have consumed the event, false if you haven't. * The default implementation always returns false. */ public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) {// 當超出邊界要關閉Window,且超出邊界,且頂層的 DecorView 不為空 finish(); return true; } return false;// 預設情況 }
Activity#onTouchEvent 是我們經常重寫的方法,執行了 onTouchEvent
表示 getWindow().superDispatchTouchEvent(ev)
返回的是 false,我們都知道 在事件分發體系中,true 表示消費了這個事件(處理了這個事件) ,那麼onTouchEvent 被呼叫表示這個事件沒有任何View消費,只能交給 Activity 處理,如何處理?就是呼叫 onTouchEvent 這個方法。
來看一下Window#shouldCloseOnTouch
/** @hide */ public boolean shouldCloseOnTouch(Context context, MotionEvent event) { final boolean isOutside = event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) || event.getAction() == MotionEvent.ACTION_OUTSIDE; if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) { return true; } return false; }
這裡判斷mCloseOnTouchOutside標記及是否為ACTION_DOWN事件,同時判斷event的x、y座標是不是超出Bounds,然後檢查FrameLayout的content的id的DecorView是否為空,進行簡單判斷,由此決定是否銷燬這個 Activity。
到這裡 Activity 這一層就分析完了。我們在這裡理一下:
onUserInteraction();
ViewGroup
書接上文,當我們將事件交給 ViewGroup#dispatchTouchEvent ,那他怎麼處理的呢?
真的可以說是超級長了,牆裂推薦使用編輯器看。還有就是看註釋。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { // 檢查合法性程式碼省略 boolean handled = false;// 是否消費 if (onFilterTouchEventForSecurity(ev)) { // 以安全策略判斷是否可以分發,true->可以分發 final int action = ev.getAction();// 事件動作不同的位儲存有不同的資訊 final int actionMasked = action & MotionEvent.ACTION_MASK;// 事件型別 // 註釋1 // 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);// 將當前事件分發下去,並且將整個TouchTarget連結串列回收 resetTouchState();// 重置Touch狀態標識 } // Check for interception.標記ViewGroup是否攔截Touch事件的傳遞 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {// 當事件是按下或者已經找到能夠接收touch事件的目標元件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;// 是否禁止攔截註釋2 if (!disallowIntercept) {// 如果自己可以攔截,預設可以 intercepted = onInterceptTouchEvent(ev);// 註釋3 預設不攔截,用於重寫 ev.setAction(action); // restore action in case it was changed } else {// 不可以攔截,直接將intercepted 設定為false intercepted = false; } } else {// 注意,重點,當不是事件序列開始,而且還沒有設定分發的子View,那麼只有一種可能,就是在這之前就被我自己攔截過了,後續序列我預設攔截消費 // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. // 不是事件流開始的 ACTION_DOWN,也沒有事件流的消費元件,那麼直接攔截。 intercepted = true; } // If intercepted, start normal event dispatch. Also if there is already // a view that is handling the gesture, do normal event dispatch. if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } // Check for cancelation. 檢查 cancel 事件 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; // 開始事件分發 // Update list of touch targets for pointer down, if needed. // 是否把事件分發給多個子View,設定: ViewGroup#setMotionEventSplittingEnabled final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null;// 用於儲存已經是事件流承受者的TargetView(在mFirstTouchTarget 這個事件流消費者連結串列中) boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) {// 不取消,不攔截,就分發 // If the event is targeting accessiiblity focus we give it to the // view that has accessibility focus and if it does not handle it // we clear the flag and dispatch the event to all children as usual. // We are looking up the accessibility focused host to avoid keeping // state since these events are very rare. View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; // 處理ACTION_DOWN事件 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 // 當前 MotionEvent 的動作標識 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount;// 子View數量 if (newTouchTarget == null && childrenCount != 0) {// 有子View可分發 final float x = ev.getX(actionIndex);// 得到點選的X座標 final float y = ev.getY(actionIndex);// 得到y座標 // Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildTouchDispatchChildList();// 子View的集合 註釋4(順序問題) 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);// 得到下標,正常情況下就是 i final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex);// 取出 i 對用的View // 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)//注意,這就是主要的篩選條件:1. 能不能接收事件(不可見或者在動畫) || !isTransformedTouchPointInView(x, y, child, null)) {// 2. 是不是在他的範圍內 ev.setTargetAccessibilityFocus(false); continue; } // 註釋5 如果在 mFirstTouchTarget中,就返回當前這個封裝了child 的 TouchTarget,沒有就返回null(注意,這時候這個View已經是在) newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) {// 在mFirstTouchTarget 這個事件流消費者連結串列中,找到事件流的消費者,跳出迴圈 // Child is already receiving touch within its bounds. newTouchTarget.pointerIdBits |= idBitsToAssign; break;// 像UP、MOVE等事件就是從這裡跳出迴圈的 } resetCancelNextUpFlag(child);// 重置flag:cancel next up if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 註釋6 重中之重就是這裡分發,看子View是否消費 // Child wants to receive touch within its bounds. 如果消費了 mLastTouchDownTime = ev.getDownTime();// 更新按下事件 if (preorderedList != null) { // childIndex points into presorted list, find original index // 找到在ViewGroup 中儲存的child,最原始的下標 for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j;// 找到ViewGroup 中的陣列的原始下標,儲存在ViewGroup的成員變數中 break; } } } else {// 臨時的排過序的陣列為null mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX();// 被消費的事件流的DOWN事件的觸控點X(起點x座標) mLastTouchDownY = ev.getY();// 起點y座標 newTouchTarget = addTouchTarget(child, idBitsToAssign);// 將消費事件流的子View的父View(當前ViewGroup)記錄在消費的連結串列頭插入操作可見註釋7 alreadyDispatchedToNewTouchTarget = true;// 表示已經成功分發給自己的子View break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } // for迴圈結束 if (preorderedList != null) preorderedList.clear(); }// 處理是 if (newTouchTarget == null && childrenCount != 0),意味著子View不為0並且沒有記錄的情況下的處理 //dispatchTransformedTouchEvent方法返回false,意味著子View也不消費 if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event.沒有child接收事件 // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } }// DOWN 事件的處理結束 } // Dispatch to touch targets. if (mFirstTouchTarget == null) {// 子View不消費 // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);// 交給自己處理(原始碼下面有) } else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it.Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget;// 頭節點 while (target != null) { final TouchTarget next = target.next;// 後驅節點 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {// 這兩個值是在第一次dispatchTransformedTouchEvent的時候返回true賦值的,意味著事件被子View消費 handled = true;// 如果被消費了 } else { // 不分發給子View,意味著被攔截或者子View與父ViewGroup臨時檢視分離(mPrivateFlags設定了PFLAG_CANCEL_NEXT_UP_EVENT),就向記錄在的 // 是否分發給子View final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;// 當前ViewGroup是否攔截 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {// 如果不分發分發子View,呼叫dispatchTransformedTouchEvent傳送cancel事件,已經分發過了就排除新的觸控目標 handled = true;// 是否自己或者子View消費 } if (cancelChild) {// 事件不分發給子View,有可能是被攔截了 if (predecessor == null) {// 具體連結串列操作看 註釋8 mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } // Update list of touch targets for pointer up or cancel, if needed. if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
註釋1:
這裡呢,就是當一個 ACTION_DOWN 事件來了以後,需要清除一些以前事件序列的標記,開始下一個事件序列。 在 cancelAndClearTouchTargets(ev) 方法中有一個非常重要的操作就是將mFirstTouchTarget設定為了null ,在resetTouchState()方法中重置Touch狀態標識。
mFirstTouchTarget 是 TouchTarget,ViewGroup 的成員變數, 記錄要消費整個事件流的View ,一個觸控事件可能有多個View可以接收到,該引數把他們連線成鏈狀。
註釋2
這裡介紹一下幾個基礎知識,讓大家知道為什麼有這個事件攔截。
當我們按下的時候,即 ACTION_DOWN 發生的時候,標誌著整個事件流的開始,這時候我們會 去找整個事件流的處理者 ,對應的就是整個事件分發流程,一旦 找到 這個事件流的處理者(消費了這個事件的ACTION_DOWN),那麼 後續的整個事件流都會直接傳送 給這個處理者進行消費掉。
就比如說螢幕上有一個button,我滑動一下按鈕,則從 ACTION_DOWN 的時候找到消費這個事件的元件了,然後button表現出按下狀態。而後續整個 ACTION_MOVE 事件和 ACTION_UP 事件都直接傳送給這個button處理。當下一個事件流來到又重複上述過程。
當情況變複雜的時候,比如說是列表,首先一來就是一個 ACTION_DOWN 事件,可是我也不知道他是點選還是按下啊,所以只能分發下去,交給了item消費了, 可是我發現他是滑動事件,那麼我就要從子View 中把消費事件的權利搶過來,就是攔截了 。而item呢?還是一個按下狀態,就傳送一個 ACTION_CANCEL 事件給他讓他恢復狀態。這裡呢,意思就是說, 當一個事件流我交給子View消費過後,後續不再分發給我,但是在整個事件流處理過程中,我可以隨時攔截,交給我來處理 。
而假如我是子View,我又不希望我的ViewGroup攔截怎麼辦呢?當然有辦法:ViewGroup#requestDisallowInterceptTouchEvent
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too // 已經處於這種狀態 return; } if (disallowIntercept) { mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }
很簡單,設定 ViewGroup的標誌位,並遞迴告訴父ViewGroup不要攔截。
註釋3
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; }
在當前ViewGroup可以攔截的情況下,看自己攔不攔截呢?不攔截,滑鼠那個事件就不考慮了,看到沒有, 預設返回false,不攔截 。當然這個方法主要也是用於我們重寫。
註釋4
preorderedList中的順序:按照addView或者XML佈局檔案中的順序來的,後addView新增的子View,會新增在列表的後面,會因為Android的UI後重新整理機制顯示在上層;
在事件分發的時候倒序遍歷分發,那麼最上層的View就可以最先接收到這個事件流,並決定是否消費這個事件流。
註釋5
/** * Gets the touch target for specified child view. * Returns null if not found. */ private TouchTarget getTouchTarget(@NonNull View child) { for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { if (target.child == child) { return target; } } return null; }
從這裡我們可以很清楚的明白,首先儲存消費事件的目標元件的資料結構是連結串列,其次 mFirstTouchTarget 就是頭節點。而 getTouchTarget 就是遍歷整個連結串列,如果有就返回這個TouchTarget,沒有就返回null,最後返回的值儲存在 newTouchTarget 中。
這裡我們介紹一下 TouchTarget ,TouchTarget 作為 ViewGroup 的內部類,原理很像Message的原理。 ofollow,noindex">Android 的訊息機制 介紹傳送門
/* Describes a touched view and the ids of the pointers that it has captured. * * This code assumes that pointer ids are always in the range 0..31 such that * it can use a bitfield to track which pointer ids are present. * As it happens, the lower layers of the input dispatch pipeline also use the * same trick so the assumption should be safe here... */ private static final class TouchTarget { private static final int MAX_RECYCLED = 32;// 回收池最大容量 private static final Object sRecycleLock = new Object[0];// 回收時候同步控制需要持有的物件鎖 private static TouchTarget sRecycleBin; // 回收池的頭節點,注意是 static private static int sRecycledCount;// 當前回收池的數量 public static final int ALL_POINTER_IDS = -1; // all ones // The touched child view. public View child;//儲存的資料:View。整個事件流的消費者 // The combined bit mask of pointer ids for all pointers captured by the target. public int pointerIdBits; // The next target in the target list. public TouchTarget next;//下一個節點 private TouchTarget() {// 不能在外部new出來 } // 將傳入的資料封裝成一個TouchTarget連結串列的結點 public static TouchTarget obtain(@NonNull View child, int pointerIdBits) { if (child == null) {// 需要傳入封裝的物件吖 throw new IllegalArgumentException("child must be non-null"); } final TouchTarget target;// 最後構建出來儲存的連結串列節點 synchronized (sRecycleLock) {// 拿到同步鎖 if (sRecycleBin == null) { target = new TouchTarget();// 回收池為空,直接內部new出來 } else { target = sRecycleBin;// 將頭節點作為目標節點 sRecycleBin = target.next;// 將頭節點下移一個 sRecycledCount--;// 回收池數量減一 target.next = null;// 將取出的節點與連結串列的聯絡斷掉 } } target.child = child;// 裝進節點 target.pointerIdBits = pointerIdBits; return target; } // 提供回收當前節點的方法 public void recycle() { if (child == null) { throw new IllegalStateException("already recycled once"); } synchronized (sRecycleLock) {// 拿到同步鎖 if (sRecycledCount < MAX_RECYCLED) {// 沒有超過回收池容量 next = sRecycleBin;// 當前回收節點指向回收池連結串列的頭結點 sRecycleBin = this;// 回收池頭結點指向自己,相當於上移 sRecycledCount += 1;// 數量加1 } else { next = null;// 置空,help Gc } child = null;// 抹除記錄的資料 } } }
既然最後是一條以為頭結點的連結串列,那麼他到底存的是哪些View呢?上一張圖:

mFirstTouchTarget 連結串列
當我們按下 button2 的時候,會一層一層的傳下去,最下層的消費了,然後返回上層接著執行程式碼(方法呼叫的時候是當前方法就被壓入棧中,呼叫方法執行結束再彈出執行),上層會在 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
的時候得到true,將剛剛消費的子View(ViewGroup/View)記錄進連結串列。
註釋6
下面就是在第一次什麼都沒有的時候進行分發,注意哦,這裡還在迴圈裡面,就意味著這次迴圈沒找到記錄,並且觸控點在這個ViewGroup範圍內,可見,那我就分發。
接下來詳細看一下ViewGroup#dispatchTransformedTouchEvent
/** * Transforms a motion event into the coordinate space of a particular child view, * filters out irrelevant pointer ids, and overrides its action if necessary. * If child is null, assumes the MotionEvent will be sent to this ViewGroup instead. */ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled;// 是否消費 // Canceling motions is a special case.We don't need to perform any transformations // or filtering.The important part is the action, not the contents. final int oldAction = event.getAction();// 獲取當前事件 if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {// 取消,或者是取消事件 event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) {// 傳進來的子View為空 handled = super.dispatchTouchEvent(event);// 當前ViewGroup 來執行,呼叫的是父類View的方法 } else { handled = child.dispatchTouchEvent(event);// 直接交給傳進來的子View,在這裡就是迴圈的時候倒序獲取的View } event.setAction(oldAction);// 設定為 ACTION_CANCEL return handled; } // Calculate the number of pointers to deliver.計算要傳遞的指標數。 final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; // If for some reason we ended up in an inconsistent state where it looks like we // might produce a motion event with no pointers in it, then drop the event. if (newPointerIdBits == 0) {// 異常情況,放棄處理 return false; } // If the number of pointers is the same and we don't need to perform any fancy // irreversible transformations, then we can reuse the motion event for this // dispatch as long as we are careful to revert any changes we make. // Otherwise we need to make a copy. final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } // Perform any necessary transformations and dispatch. if (child == null) { 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()); } handled = child.dispatchTouchEvent(transformedEvent); } // Done. transformedEvent.recycle();// 回收TouchTarget return handled; }
這裡引用大神的分析:
在dispatchTouchEvent()中多次呼叫了dispatchTransformedTouchEvent()方法,而且有時候第三個引數為null,有時又不是,他們到底有啥區別呢?這段原始碼中很明顯展示了結果。在 dispatchTransformedTouchEvent()
原始碼中可以發現多次對於child是否為null的判斷,並且均做出如下類似的操作。其中,當 child == null
時會將Touch事件傳遞給該ViewGroup自身的dispatchTouchEvent()處理,即 super.dispatchTouchEvent(event)
(也就是View的這個方法,因為ViewGroup的父類是View);當child != null時會呼叫該子view(當然該view可能是一個View也可能是一個ViewGroup)的dispatchTouchEvent(event)處理,即 child.dispatchTouchEvent(event)
。別的程式碼幾乎沒啥需要具體注意分析的。
具體的什麼時候會傳空呢,我們接著往下看,後面會分析和總結。
註釋7
/** * Adds a touch target for specified child to the beginning of the list. * Assumes the target child is not already present. */ private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);// 獲取節點,並將資料裝進去 target.next = mFirstTouchTarget;// 將新節點的next指向下一個節點 mFirstTouchTarget = target;// 頭結點記錄為當前節點 return target;// 返回頭節點 }
到這裡,整個 ViewGroup 層就結束啦,這裡來總結下, dispatchTransformedTouchEvent()
什麼時候會傳入一個null的child呢?
- ViewGroup 沒有子View
- 子元素處理了點選事件,但是在 dispatchTouchEvent 中返回了false,這一般都是因為子 View 在onTouchEvent 中返回了 false。
註釋8
這裡主要分析的是迴圈中的連結串列操作
while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; }
View 最後可能接收到進行消費
我們知道前面按著正常情況下,就是呼叫View的dispatchTouchEvent方法,將事件傳遞給子View,接下來就是View的show time。
View#dispatchTouchEvent
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * 傳遞給目標View 或者 檢視它是否是目標 * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ 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)) {// 根據引數確定是否可以分發:這是一種安全策略(正常情況況下為true) if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {// 作為滾動條拖動就直接處理滾動事件,並直接消費,返回true result = true;// 滾動條的時候 } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo;// 各種listener定義在一起的靜態內部類,包括我們熟悉的 onClickListener if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED// 驗證 li 中的 mOnTouchListener 不為空,可以呼叫 && li.mOnTouchListener.onTouch(this, event)) {// 呼叫onTouch 方法 result = true;// onTouch返回true就消費 } if (!result && onTouchEvent(event)) {// onTouch 不消費就交給onTouchEvent,消費就變true 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; }
看著註釋基本都可以看懂,但是這裡又一個東西得看一下,方便對一些事件的理解,那就是 onTouchEvent 方法:
/** * Implement this method to handle touch screen motion events. * <p> * If this method is used to detect click actions, it is recommended that * the actions be performed by implementing and calling * {@link #performClick()}. This will ensure consistent system behavior, * including: * <ul> * <li>obeying click sound preferences * <li>dispatching OnClickListener calls * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when * accessibility features are enabled * </ul> * * @param event The motion event. * @return True if the event was handled, false otherwise. */ public boolean onTouchEvent(MotionEvent event) { final float x = event.getX();// 獲取點選座標 final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction();// 獲取Action型別 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; } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } 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)) {// post到主執行緒執行這個Runnable,這Runnable是由View實現,內部呼叫li.mOnClickListener.onClick(this); 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; } return false; }
所有的流程最後都可以歸結到這張圖上

事件分發攔截機制-詳細圖解
整個事件傳遞就這樣結束了,在這個過程中,攔截分發的程式碼交錯在一起,我這裡總結一下流程:
-
事件分發開始於Activity#dispatchTouchEvent,先交給getWindow().superDispatchTouchEvent(ev),返回false再交給Activity#onTouchEvent(ev)
-
在 PhoneWindow()#superDispatchTouchEvent(ev) 中,直接交給了頂層View:DecorView#superDispatchTouchEvent
-
在 DecorView#superDispatchTouchEvent 直接 super.dispatchTouchEvent(event),意味著呼叫父類ViewGroup#dispatchTouchEvent 處理。
-
呼叫 ViewGroup#onInterceptTouchEvent 判斷是否攔截
如果攔截,就super.
如果不攔截並且是事件流的開始的話(DOWN 事件),就呼叫ViewGroup#dispatchTransformedTouchEven 分發下去
如果分發成功,就將分發成功的View存在 mFirstTouchTarget 連結串列中
如果遍歷分發,沒人消費,或沒有子View的話,就呼叫父類(也是View啊)的 dispatchTouchEvent,這裡面就會執行onTouch / onTouchEvent 方法