View 體系之 View 事件分發源碼解析
本文原創,轉載請注明出處。
歡迎關注我的 簡書 ,關注我的專題 Android Class 我會長期堅持為大家收錄簡書上高質量的 Android 相關博文。
寫在前面:
前兩天我們分別總結了
View 的位置與事件:
View 的位置與事件
View 的滑動:
View 的滑動
今天我們來聊聊 View 的事件分發。相信每個人都知道 View 的事件分發實在是太重要了,它不僅僅是一個核心知識點,更是一個難點。在我初學 Android 時,View 的事件分發也前前后后看了好多次。雖然也能復述出一個大概,但是仍然有一些知識盲區。所以今天把 View 的事件分發總結出來,算是自己記下一篇學習筆記,未來復習鞏固使用。如果還能為大家解決一些困惑,那就更好了。
當然關于事件分發的文章,前輩們總結了很多,有一篇我認為非常出色:
圖解 Android 事件分發
這篇文章通過圖解的方式,清晰直觀的講明白了事件分發的原則,本文打算對這篇文章做一些補充,補充一下這篇文章的一些關鍵 log,和源碼分析。所以不理解事件分發的朋友可以閱讀下該文。
首先大家想一下,在 Android 中誰是事件分發的掌控者和消費者?沒錯,是 Activity、ViewGroup、View,一個正常的 Android 應用程序他們三個肯定是存在的。而分發的事件就是 MotionEvent 對象。
關于事件分發,有三個關鍵的方法
dispatchTouchEvent
、onInterceptTouchEvent
、onTouchEvent
而 onInterceptTouchEvent
方法是 ViewGroup 特有的。我們在 Activity、ViewGroup、View 中分別打印這幾個方法,來看看不同的返回值對事件分發的影響,來印證上文的觀點,并且分析出事件分發的傳遞規則。
當我們不修改任何返回值,全部為默認實現時:
可以看到 ACTION_DOWN 事件的傳遞原則為,U 型原則,ACTION_MOVE、ACTION_UP 傳遞原則為距離最短原則。
分別來改變 Activity 中dispatchTouchEvent
和 onTouchEvent
的返回值,來看看事件傳遞的 log:
首先分別將 dispatchTouchEvent
的返回值改為 false 或者 true:
可以看到我的這次點擊按鈕的事件在 Activity 中的 dispatchTouchEvent
中消費掉了。
當我改變 MainActivity 中 onTouchEvent
方法的返回值時:
可以看到打印的結果與最初所有方法的默認返回值相同,這也很好理解,因為 Activity 的 onTouchEvent
方法本身就是事件 U型 傳遞的最后一環,不管什么返回值,反正事件都會到這里。
Activity 的看完了,再來看看 ViewGroup 的:
可以看到將 ViewGroup 的 dispatchTouchEvent 返回值改為 false 時,事件就不會再下發了,而是直接傳遞給 Activity 的 onTouchEvent。當 dispatchTouchEvent 返回值改為 true 時,與默認實現相同。
將 onInterceptTouchEvent
的返回值改為 true 時,事件不會再傳遞給 View ,而是傳遞給當前 ViewGroup 的 onTouchEvent。當onInterceptTouchEvent
返回值為 false 時,與默認相同。
首先明確一個概念:事件序列
就是當手指 按下-->滑動-->抬起 的這一完成過程產生的事件流為一個事件序列。
當我將 ViewGroup 的 onTouchEvent
方法的返回值改為 true 時,事件在 ViewGroup 就消費掉了,這里應該注意,onInterceptTouchEvent
如果發生了攔截,那么在一個事件序列中僅調用一次。
關于 View 這兩個方法的返回值就不貼圖了,與引用文章的結論一致。
一些細節
當給一個 View 設置 onTouchListener
時,它的 onTouch
方法就會回調,如果 onTouch
方法的返回值為 false,則該 View 的 onTouchEvent 方法會被調用,如果 onTouch
方法的返回值為 true,則該 View 的 onTouchEvent 方法就不會調用了,事件會直接在該 View 的 disPatchTouchEvent 中消費。另外 onClick 方法是在 onTouchEvent 方法中調用的。所以這幾個方法的優先級關系為:
onTouch>onTouchEvent>onClick
一個 View 的 onTouchEvent 的返回值是與這個 View 本身的 onClick 和 onLongClick 屬性相關的,只有這兩個屬性同時為 false 則 onTouchEvent 才會為 false,View 的 onLongClick 默認都為 false,而 onClick 屬性不同,比如 button 的為 true,textview 的為 false。
事件分發的源碼解析
在這部分內容中,我們看看事件分發在源碼上的處理,事件最初都是在 Acitivity 中產生,然后分發給根 ViewGroup,最后再發給相應的 View。那先來看看 Activity 的 dispatchTouchEvent
方法。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
這段代碼很簡單,當 ACTION_DOWN 來了的時候,回調 onUserInteraction
方法作為事件起始的回調。
然后來看看 getWindow().superDispatchTouchEvent(ev)
方法的返回值是如何的。
首先關于 Window 和 PhoneWindow 類的關系可以上面這篇我曾經總結的文章。
PhoneWindow:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
跟進,看看 DectorView 的 superDispatchTouchEvent(event)
方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DectorView 繼承自 FrameLayout,所以這里也是調用到了 ViewGroup 的 dispatchTouchEvent,事件順利傳到了 ViewGroup
來看看 ViewGroup 對事件的分發
代碼比較多,我們分段來看:
// 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;
}
這段代碼的意義是,判斷是否要調用 onInterceptTouchEvent
方法,可以看到 if 判斷的條件語句為:if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
先看第一個,當事件為 ACTION_DOWN 時,肯定會調用 onInterceptTouchEvent
,那 mFirstTouchTarget
是什么呢?由后面的代碼可知,當事件不被攔截并且交給子元素處理時,mFirstTouchTarget != null
。所以當被當前 View 攔截的時候,mFirstTouchTarget == null
,ACTION_DOWN、ACTION_MOVE 事件來的時候,條件就不成立了,所以 onInterceptTouchEvent
方法也不會再次調用,這也就是為什么之前說,當此 ViewGroup 確定攔截事件的時候,onInterceptTouchEvent
之后在事件為 ACITON_DOWN 的時候調用一次。
這有一個 flag 比較重要,FLAG_DISALLOW_INTERCEPT,它的值由子 View 的 requestDisallowInterceptTouchEvent
決定,由子 View 請求父 View 不要攔截事件。當然此屬性對 ACTION_DOWN 是無效的,原因是:
// 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();
}
在 dispatchTouchEvent
的開頭就重置了 FLAG 的狀態。
這里我們會有兩個結論:
- onInterceptTouchEvent 方法并不是每次都調用,而如果事件傳遞進來,dispatchTouchEvent 才是每次都會調用的。
-
requestDisallowInterceptTouchEvent
可以干預父 View 的事件分發過程,有助于我們解決滑動沖突。
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)) {
// 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);
alreadyDispatchedToNewTouchTarget = true;
break;
}
這段代碼首先判斷所有的子 View 是否具有接受事件的能力,1 沒有進行動畫 2 點擊的位置在 View 的坐標范圍內。dispatchTransformedTouchEvent
中有這樣一行代碼:
handled = child.dispatchTouchEvent(event);
所以到這里,就調用到了子 View 的 dispatchTouchEvent 方法。
在 addTouchTarget 方法中:
mFirstTouchTarget = target;
mFirstTouchTarget 被賦值,也就是當子 View 處理事件時,mFirstTouchTarget 不為 null.
看完了 ViewGroup 對事件分發的處理,我們來看看 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) {
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;
}
因為 View 不會再有子 View 了,所以他的 dispatchTouchEvent 方法比較簡單,首先如果這個 View 設置了 onTouchListener,并且 onTouch 方法返回值為 true 時,會進入判斷條件,方法直接返回 true,就不會走到 onTouchEvent 方法里面了。所以這里也印證了我們之前的觀點,也就是 onTouch 方法優先級大于 onTouchEvent。
再來看看 View 的 onTouchEvent 方法的源碼:
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
可以看到這里即使 View 是 disable 的,依然可以消耗事件。
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE)
switch (action) {
case MotionEvent.ACTION_UP:
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();
}
}
}
break;
可以看到當這個 View 的 LongClick 或者 Clickable 屬性有一個為 true,就可以消耗這個事件,并且在 ACTION_UP 調用 performClick():
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;
}
所以到這里這個 View 的點擊事件也響應了。
到這里,我們事件分發的源碼就分析完畢了。
寫在后面:
本文更多的是對上面那篇引用文章的源碼補充,兩篇結合起來看,對事件分發的理解就應該足夠了。這幾天看源碼看得頭疼。。。關于本文的結論總結,我準備過一陣回頭溫習的時候補充下,希望大家喜歡。