Android讀書筆記--從原始碼角度剖析View事件分發機制
在開始描述問題之前先說點題外話,寫這篇文章的初衷一方面為了構建Android知識體系,另一方面是真心覺得這個是Android面試必問的知識點。網上這方面的部落格和書籍講解這方面的知識也不少,講的也很到位。正所謂只有自己理解了才是自己的,所以在閱讀了他們的文章後,加上自己的理解特此記錄一篇~,以便加深理解和記憶!如理解有誤的地方請留言說明,我們一起探討,謝謝!
聯絡方式:郵箱([email protected])
1.必備知識點
事件的分發說白了,就是使用者與應用的互動過程(手指與螢幕接觸)中,發生的一系列事件傳遞與處理過程。
1.1 事件分發涉及的物件--MotionEvent
典型事件型別:
ACTION_DOWN——手指剛觸碰螢幕那一刻(按下) ACTION_MOVE——手指在螢幕上移動(移動) ACTION_UP——手指抬起那一刻(抬起) 複製程式碼
一個事件序列:就是從手指按下 View 開始直到手指離開 View 產生的一系列事件。
ACTION_DOWN-> ACTION_UP ACTION_DOWN->...ACTION_MOVE...->ACTION_UP 複製程式碼
1.2 事件分發涉及的方法
1. dispatchTouchEvent(MotionEvent ev)
用來進行事件分發。返回結果受當前 View 的 onTouchEvent 和子 View 的 dispatchTouchEvent 方法的影響,表示是否消耗當前事件。
2. onInterceptTouchEvent(MotionEvent ev)
在上述dispatchTouchEvent
方法內部呼叫,用來進行當前事件是否攔截校驗。這裡有一點要注意的地方就是如果當前View攔截了某個事件(一般指ACTION_DOWN),那麼在同一個事件序列
(上面講過這個概念)當中,此方法不會被再次呼叫——即不會做二次攔截校驗。
注:Activity和View內部沒有此方法
3. onTouchEvent(MotionEvent ev)
在上述dispatchTouchEvent
方法內部呼叫,返回結果表示是否消耗當前事件。這裡同上也有一點要注意,如果當前方法返回false
(不消耗),那麼同一個事件序列
中,當前View無法再次接收到事件。
上述方法的關係可用下面的一段偽程式碼表示:
public boolean dispatchTouchEvent(MotionEvetn e){ if(onInterceptTouchEvent(ev)){//是否攔截 return onTouchEvent(e);//攔截事件處理:是否消耗 } return child.dispatchTouchEvent(e);//不攔截:子類View分發 } 複製程式碼
通過上面的虛擬碼可以大致瞭解到事件的傳遞規則:對於一個根ViewGroup
來說,點選事件產生後,首先會傳遞給它,這時它的dispatchTouchEvent
就會被呼叫,如果這個ViewGroup
的onInterceptTouchEvent
方法返回true
,就說明攔截當前事件,接著事件就會交給這個ViewGroup
的onTouchEvent
方法處理。反之onInterceptTouchEvent
方法返回false
,就不攔截當前事件,這時當前事件就會傳遞給它的子View
,接著View
的dispatchTouchEvent
方法就會呼叫,如此反覆直到事件最終被處理。
1.3 事件傳遞過程遵循如下過程
Activity -> Windown(PhoneWindow) -> DecorView(FrameLayout) -> contentView(setContentView) ->..ViewGroup..->View 複製程式碼
2. 事件分發原始碼解析
根據上面瞭解到的事件傳遞的過程分析,下面我們就一步一步撕開它神祕的面紗,從內部瞭解它的呼叫關係。
2.1 Activity對點選事件的分發過程
點選事件用MontionEvent
表示,當一個點選操作發生時,最先傳遞給當前Activity
,由Activity
的dispatchTouchEvent
方法進行事件分發,具體的工作由Window
來完成。Window
會將事件傳遞給DecorView
,DecorView
一般就是當前介面的底層容器(即setContentView所設定的View
的父容器),通過Activity.getWindow().getDecorView()可以獲得。因此我們先從Activity
的dispatchTouchEvent
開始分析。
原始碼-1:Activity#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); } public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; } 複製程式碼
現在分析上述程式碼,通過原始碼瞭解到事件交給Activity所附屬的Window進行分發,如果getWindow().superDispatchTouchEvent(ev)
返回true
,事件到此結束,返回false
,說明下級所有View的onTouchEvent
都返回了false
,則Activity的onTouchEvent
將會被呼叫(如上)
通過上面瞭解到getWindow().superDispatchTouchEvent(ev)
這個才是分發的關鍵,看原始碼:
原始碼-2: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 { /** * 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
,為什麼呢?到這裡您可以詳細閱讀下上面Window
類的說明,發現此處已經指明瞭Window
的唯一實現就是android.view.PhoneWindow
,好傢伙,隱藏的夠深的,那麼請移駕,謝謝~
原始碼-3:PhoneWindow#superDispatchTouchEvent相關程式碼
// This is the top-level view of the window, containing the window decor. private DecorView mDecor; @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } 複製程式碼
到這裡邏輯就清晰了吧!雖然程式碼只有一行,但已經足以說明問題了,此處具體邏輯移交給DecorView
(這就是我們前面說的視窗的頂級View-->ViewGroup),即Activity#setContentView
設定的View
就是DecorView
的子View。目前事件傳遞到了DecorView
這裡,由於DecorVieW
即成自FrameLayout
且是父View
,那麼得出結論--最終事件會傳遞給View
,到這一步並不是我們的重點,事件如何通過頂級View
進行傳遞消費才是我們的重頭戲,請繼續,謝謝~
2.2 頂級View對點選事件的分發過程
關於點選事件如何在View
中進行分發,上面已經做了描述,這裡就直接上ViewGroup
原始碼,原始碼如下:
dispatchTouchEvent
方法內容較多分如下幾個片段說明:
原始碼-4:ViewGroup#dispatchTouchEvent——攔截邏輯處理
// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); } // Check for interception. final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } 複製程式碼
-
是否攔截條件:事件型別為
ACTION_DOWN || mFirstTouchTarget != null
; -
mFirstTouchTarget:每次開始(
ACTION_DOWN
)都會被初始化為null
,當事件由ViewGroup
的子元素成功處理時,它指向子元素; -
當事件由
ViewGroup
攔截時,條件mFirstTouchTarget != null
不成立,即當ACTION_MOVE
和ACTION_UP
事件到來時,由於第一條攔截條件不滿足,則onInterceptTouchEvent
不再呼叫:應證了一旦當前View攔截事件,那麼同一事件序列的其它事件都不再進行攔截校驗,直接交給它處理。 -
FLAG_DISALLOW_INTERCEPT
標記位:這個標記位一旦設定後(requestDisallowInterceptTouchEvent
),ViewGroup
將無法攔截除了ACTION_DOWN
以外的其它點選事件(ACTION_DOWN
事件會重置此標記位,將導致子View中設定的這個標記位無效)。 -
面對
ACTION_DOWN
事件時,ViewGroup
總是會呼叫自己的onInterceptTouchEvent
方法來詢問自己是否要攔截事件,這一點從上面的原始碼中可以看出來。
原始碼-5:ViewGroup#dispatchTouchEvent——初始化
// 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);//重置 mFirstTouchTarget = null resetTouchState();//重置FLAG_DISALLOW_INTERCEPT標記位 } 複製程式碼
從上面的程式碼可以看出,ViewGroup
會在ACTION_DOWN
事件到來時會做重置狀態的操作,因此子View
呼叫requestDisallowInterceptTouchEvent
並不能影響ViewGroup
對ACTION_DOWN
事件的處理。
總結:
-
ViewGroup
決定攔截事件(ACTION_DOWN
)後,那麼後續的點選事件將會預設交給它處理且不再呼叫它的onInterceptTouchEvent
方法。 -
FLAG_DISALLOW_INTERCEPT
這個標誌的作用是讓ViewGroup
不再攔截事件,當然前提是ViewGroup
不攔截ACTION_DOWN
事件。 -
FLAG_DISALLOW_INTERCEPT
為解決滑動衝突解決提供了新的思路。
原始碼-6:ViewGroup#dispatchTouchEvent——不攔截,遍歷子View
final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { 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); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//*子元素呼叫dispatchTouchEvent方法* // 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(); //儲存當前子View:mFirstTouchTarget newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus did not handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); //... } 複製程式碼
原始碼-7:ViewGroup#dispatchTouchEvent——子View下發主要邏輯呼叫
/** * 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 do not 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) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } //... } 複製程式碼
子View
是否能夠接收點選事件有以下兩點衡量:
- 子元素是否在播放動畫
- 點選事件的座標是否落在子元素的區域內
上面這部分程式碼說明的是ViewGroup
不攔截情況下,事件向子View
下發的過程.即主要呼叫方法為dispatchTransformedTouchEvent
,它的內部實際上呼叫的就是子元素的dispatchTouchEvent
方法(可通過上面的原始碼-7
看得出來).通過具體分析可看出,如果child.dispatchTouchEvent(event)
返回true
,那麼mFirstTouchTarget
(addTouchTarget
方法內部操作)就會被賦值同時跳出for迴圈,這裡是否對mFirstTouchTarget
賦值,將會影響ViewGroup
的攔截策略,如下所示:
原始碼-8:ViewGroup#dispatchTouchEvent——賦值mFirstTouchTarget
/** * 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; mFirstTouchTarget = target; return target; } 複製程式碼
mFirstTouchTarget
如果為null
,將會預設攔截接下來同一序列的所有事件。(不做二次攔截校驗)
遍歷所有子元素,都沒有處理包含兩種情況:
ViewGroup
此時ViewGroup
將會呼叫super.dispatchTouchEvent(evet)
,這一點可以從上述原始碼-8
可以看出,很顯然這裡ViewGroup
繼承自View
,所以這裡就轉到View
的dispatchTouchEvent
方法,即點選事件交由View
處理,那麼請繼續看下面的分析。
2.3 View對點選事件的處理過程
View(不包含ViewGroup)對點選事件的處理稍微簡單,它沒有onInterceptTouchEvent
方法且無法向下傳遞事件,只能自己處理,請看它的dispatchTouchEvent
方法,如下:
原始碼-9:View#dispatchTouchEvent——View點選事件處理
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @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. //... boolean result = false; //... if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } //... return result; } 複製程式碼
從上面的程式碼可以看出:OnTouchListener的onTouch
比onTouchEvent(event)
優先順序高,如果設定了OnTouchListener
且mOnTouchListener.onTouch
返回true
那麼onTouchEvent(event)
將不會呼叫,反之將會呼叫onTouchEvent(event)
,見下文:
原始碼-10: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(); 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 does not 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 do not have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed.Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; //... } return true; } return false; } 複製程式碼
從上面的程式碼看出:影響事件的消耗因素有兩個:CLICKABLE
和LONG_CLICKABLE
只要有一個為true
,那麼它就會消耗這個事件,即onTouchEvent
方法返回true
,實際呼叫方法為performClick();
,在其內部呼叫OnClickListener#onClick
方法。
到此點選事件的分發機制的原始碼分析就完了,但是Android 的學習才剛開始,還有很長的路要走,下面附上從別處盜來的圖,覺得不錯可以看下