源碼跟蹤分析View的事件分發機制
想要寫好Android的界面,解決View的滑動沖突是十分重要的,因此需要對Android的事件分發機制有一定了解和認識。之前校招面試的時候自己也被問過相關問題,加上自己最近在寫一個小Demo遇到的一些玄學Bug,索性就系統的學習了一下。下面分享一下學習成果,同時也希望有人能指出我的錯誤和不足。
寫在前面的話
- 1.dispatchTouchEvent和onTouchEvent返回true則表示當前View要消耗點擊事件,就不再向下分發,返回false則開始向上回溯;
- 2.ViewGroup是繼承自View類的,只不過ViewGroup是容器它可以有子View而View沒有;
- 3.同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序列以down事件開始,中間夾雜著許多個move事件,最終以up事件結束;
- 4.一個MotionEvent對象即封裝了一個點擊事件。
先介紹點擊事件分發過程中3個比較重要的方法:
public boolean dispatchTouchEvent(MotionEvent ev)
這個方法的作用是用來分發點擊事件的,如果點擊事件傳遞到了當前 View ,那么該方法一點會被調用,返回結果受當前 View 的onTouchEvent和下級 View 的dispatchTouchEvent的返回值的影響,表示是否消耗當前事件。如果所有 View 都不消耗,那么該點擊事件最后會交給 Activity 的onTouchEvent方法處理。
public boolean onInterceptEvent(MotionEvent ev)
在上述方法內部調用,返回結果表示是否攔截事件。如果當前View攔截了某個事件,那么在同一個事件序列中此方法不會被再次調用。
public boolean onTouchEvent(MotionEvent ev)
在dispatchTouchEvent中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前 View 無法再次接收到事件。
下面這幅圖是參照 圖解 Android 事件分發機制 這篇博客畫的一幅流程圖:
事件分發流程圖.png
從左上角開始,點擊事件總是先傳遞給Activity,再由activity向下分發。下面就按照圖中1-9的順序從源碼角度跟蹤點擊事件的去向。
1. Activity#dispatchTouchEvent -> ViewGroup#dispatchTouchEvent
當一個點擊事件發生后首先會傳遞給Activity,由Activity的dispatchTouchEvent(MotionEvent ev)來接收并進行分發,首先看一下Activity的dispatchTouchEvent方法源碼:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
onUserInteraction()方法是一個空方法,第一個if可以不看。接著看第二個if,即如果getWindow().superDispatchTouchEvent(ev)返回true,則Activity的dispatchTouchEvent返回true。
這時候點擊事件就從Activity傳遞到getWindow()返回的Window了,Android里面的Window類是一個抽象類,Window類的唯一一個實現類是PhoneWindow,接著看看PhoneWindow的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
先來看一下mDecor這個對象,這是一個DecorView類對象,這個DecorView是整個應用窗口的根View:
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
看一下它的superDispatchTouchEvent方法:
public boolean superDispatchTouchEvent(MotionEvent event){
return super.dispatchTouchEvent(event);
}
因為DecorView是繼承自FrameLayout的,即間接繼承自GroupView,所以上述super.dispatchTouchEvent方法調用的即是GroupView的dispatchTouchEvent,自此點擊事件就傳遞到了GroupView了。
2.ViewGroup#dispatchTouchEvent -> Activity#onTouchEvent
回過頭看Activity的dispatchTouchEvent方法,如果getWindow().superDispatchTouchEvent(ev)返回了false即ViewGroup的dispatchTouchEvent返回false,則return onTouchEvent(ev)這時候點擊事件就交給Activity的onTouchEvent去處理了。
3. ViewGroup#dispatchTouchEvent -> ViewGroup#onInterceptTouchEvent
來看一下ViewGroup的dispatchTouchEvent 中是否攔截相關邏輯:
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 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);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
}
先看第一個if的判斷條件,事件類型為ACTION_DOWN或者mFirstTouchTarget != null。這個mFirstTouchTarget 在當前ViewGroup的子View決定攔截處理事件時會被賦值,這時候mFirstTouchTarget != null成立。當ACTION_MOVE和ACTION_UP過來,actionMasked == MotionEvent.ACTION_DOWN不成立,這時候如果mFirstTouchTarget == null說明沒有子View要處理這次點擊事件,則intercepted = true, 即當前ViewGroup要攔截這次事件(因為沒有子View處理,而且事件能傳遞到當前ViewGroup,所以當前ViewGroup攔截)。如果mFirstTouchTarget != null則點擊事件再繼續往下傳遞。
如果第一個if條件成立,進入到if內部。mGroupFlags和FLAG_DISALLOW_INTERCEPT是2個標志位。mGroupFlags這個標志位我不知道是用來做什么的,也希望知道的能給我說明下。FLAG_DISALLOW_INTERCEPT是通過requestDisallowInterceptTouchEvent來設置,一般是通過在子View調用該方法來設置該標志,該標志一旦設置,ViewGroup將無法攔截除了ACTION_DOWN之外的其他點擊事件,因為ACTION_DOWN事件會重置FLAG_DISALLOW_INTERCEPT標志位。
第二個if的條件,如果disallowIntercept為false即允許攔截,intercepted = onInterceptTouchEvent(ev)這句執行,此時點擊事件就傳遞到了onInterceptTouchEvent。而onInterceptTouchEvent是默認返回false的,即不攔截。
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
4. ViewGroup#onInterceptTouchEvent -> ViewGroup#onTouchEvent
看一下ViewGroup點擊事件向下分發源碼
public boolean dispatchTouchEvent(MotionEvent ev) {
...
final View[] children = mChildren;
//遍歷所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null) ? children[childIndex] : preorderedList.get(childIndex);
//判斷子View是否能接收點擊事件
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
//判斷當前view是否已存在mFirstTouchTarget隊列中,當多點觸控時,多次點擊同一個view只保存一個
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//事件已傳遞到子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
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;
}
}
...
}
上面的dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)里面調用的就是就是子View的dispatchTouchEvent,該方法中有如下一段代碼:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
這里有2種情況。第一種情況,如果child == null,則handled = super.dispatchTouchEvent(event),此時會調用ViewGroup的父類View的dispatchTouchEvent,先來看一下該類中的部分方法:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false ;
...
if (onFilterTouchEventForSecurity(event)) {
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 ;
}
ListenerInfo 在OnTouchListener 被設置的時候會初始化,onFilterTouchEventForSecurity做的是一些標志位判斷,所以直接來看這句
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))
當OnTouchListener 被設置后不為null并且OnTouchListener的onTouch返回了true,則result = true。那么下一個if判斷!result為false,根據&&運算符特性則onTouchEvent不會被調用。即只要設置了OnTouchListener則onTouchEvent就不會被調用,可見OnTouchListener的優先級高于onTouchEvent。到這里點擊事件就傳遞給onTouchEvent了,這里的onTouchEvent是GroupView的onTouchEvent,先記住這點。
5.ViewGroup#onTouchEvent -> Activity#onTouchEvent
如果onTouchEvent方法return false,將間接導致ViewGroup的dispatchTouchEvent返回false,則發生前面 2 中 的情況,即點擊事件傳遞到Activity的onTouchEvent。
6.ViewGroup#onInterceptTouchEvent -> View#dispatchTouchEvent
4中提到的2種情況,當child == null不成立時,此時會調用child.dispatchTouchEvent方法,這樣點擊事件就傳遞到了子View的dispatchTouchEvent方法了。
7.View#dispatchTouchEvent -> View#onTouchEvent
參考4中View的dispatchTouchEvent 方法的部分代碼,如果View設置了OnTouchListener,onTouchEvent就不會調用;反之,onTouchEvent會被調用,此時點擊事件就傳遞到View的onTouchEvent了。
8.View#diapatchTouchEvent -> ViewGroup#onTouchEvent
如果View的diapatchTouchEvent返回false說明當前View不準備消費點擊事件,那么前面提到的mFirstTouchTarget就為null ,因為前面說過只有子View明確表示要消費點擊事件,它才會被賦值。如果mFirstTouchTarget == null,ViewGroup就會自己處理點擊事件:
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
dispatchTransformedTouchEvent這個方法在前面提到過,只不過當時第三個參數是child不是null,這樣前面child == null就直接成立,所以handled = super.dispatchTouchEvent(event)會被調用,從而調用onTouchEvent。
9.View#onTouchEvent -> ViewGroup#onTouchEvent
當點擊事件傳遞到View的onTouchEvent,但是該方法返回了false,間接導致View的dispatchTouchEvent返回了false,從而出現8當中的情況,從而ViewGroup的onTouchEvent被調用。
總結:
- 1.點擊事件從Activity中的dispatchTouchEvent開始向下分發,向上回溯是決定getWindow().superDispatchTouchEvent(ev)的返回值;
- 2.onInterceptTouchEvent方法不是每一次都會被調用,只有當傳遞過來的為ACTION_DOWN事件或者mFirstTouchTarget != null;
- 3.View的dispatchTouchEvent方法和onTouchEvent方法返回了false,那么它的父容器View(如果有的話)的onTouchEvent方法一定被調用,因為此時child為null(參考8和9);
- 4.子View的onTouchEvent要在父View的onTouchEvent之前被調用;
- 5.onTouchListener的優先級高于onTouchEvent,如果前者被賦值,后者將被屏蔽;
- 6.可以通過調用requestDisallowInterceptTouchEvent設置FLAG_DISALLOW_INTERCEPT這個標志位來攔截除了ACTION_DOWN以外的其他點擊事件,因為來ACTION_DOWN到來時會重置標志位。
參考鏈接:
參考書籍:
《Android開發藝術探索》
來自:http://www.jianshu.com/p/5597d6295130