Android學習筆記之View的事件分發機制
一、點選事件的傳遞規則
所謂的點選事件分發過程,其實就是當我們點選螢幕,產生了一個MotionEvent之後,系統將這個事件傳遞給一個具體View的過程。總的來說,事件總是先傳遞給Activity,然後傳遞給Window,再傳遞給頂級View(Activity→Window→DecorView),最後再按照事件分發機制一層一層向下去分發事件。而這個分發過程由三個很重要的方法來共同完成:
- dispatchTouchEvent(MotionEvent ev):用來進行事件的分發。如果事件能夠傳遞給當前View,那麼該方法一定會被呼叫,返回結果受當前View的onTouchEvent方法和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。
- onInterceptTouchEvent(MotionEvent ev):在上述方法的內部呼叫,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列中,此方法不會再次呼叫,返回結果表示是否攔截當前事件。
- onTouchEvent(MotionEvent ev):在 dispatchTouchEvent 方法中呼叫,用來處理點選事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件。
上述三個方法之間的關係可以通過如下偽程式碼表示:
public boolean dispatchTouchEvent(MotionEvent event) { boolean consume = false; if(onInterceptTouchEvent(ev)){//如果攔截 consume = onTouchEvent(ev); } else {//如果不攔截 consume = child.dispatchTouchEvent(ev); } return consume; }
通過上面的虛擬碼,我們可以大致瞭解點選事件的傳遞規則:對一個ViewGroup來說,當一個點選事件傳遞給它時,它的 dispatchTouchEvent 方法會被呼叫,如果這個ViewGroup攔截此事件,那麼事件將會交由該ViewGroup來處理(即呼叫 onTouchEvent 方法);如果它不攔截此事件,那麼事件將會向下傳遞給它的子元素,接著子元素的 dispatchTouchEvent 方法會被呼叫,如此反覆直到時間被最終處理。
關於事件傳遞機制的一些結論:
- 所謂的一個事件序列是指從手指接觸螢幕的那一刻起,到手指離開螢幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序列以 down 事件開始,以 up 事件結束,中間含有多個 move 事件。
- 某個 View 一旦決定攔截,那麼這一個事件序列都只能由它來處理,並且它的 onInterceptTouchEvent 不會再被呼叫。
- 參考第2條,通常而言,一個事件序列只能被一個 View 攔截消耗。但是通過特殊手段,可能出現多個 View 處理的情況,比如一個 View 將本該自己處理的事件通過 onTouchEvent 強行傳遞給其他View處理。
- 某個View一旦開始處理事件(onTouchEvent 開始執行),如果它不消耗 ACTION_DOWN 事件(onTouchEvent 返回 false),那麼同一事件序列中的其他事件都不會再交給它處理,並且事件將重新交由它的父元素去處理(即父元素的 onTouchEvent 會被呼叫)。在這裡,如果所有 View 的 onTouchEvent 返回 false,那麼最終會傳遞給 Activity 處理(即Activity 的 onTouchEvent 會被呼叫)。
- 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那麼這個點選事件會消失,此時父元素的 onTouchEvent 並不會被呼叫,並且當前View可以持續受到後續的事件,最終這些消失的點選事件會傳遞給 Activity 處理。
- ViewGroup 預設不攔截任何事件。
- View 沒有 onInterceptTouchEvent 方法,一旦有點選事件傳遞給它,那麼它的 onTouchEvent 方法就會被呼叫。
- View 的 onTouchEvent 預設都會消耗事件(返回 true),除非它是不可點選的(clickable、longClickable、contextClickable都為 false)非提示框控制元件。View 的 longClickable、contextClickable屬性預設都為false,但 clickable 屬性要視情況:比如 Button 為 true,TextView 為 false。
- View 的 enable 屬性不影響 onTouchEvent 的預設返回值。哪怕一個View是disable狀態的,只要它的 clickable、longClickable、contextClickable不同時為 false,其 onTouchEvent 就預設返回 true。
- onClick 會發生的前提是當前 View 是可點選的,並且它收到了 down 和 up 的事件。
- 事件的傳遞過程是由外向內的。通過 requestDisallowInterceptTouchEvent 方法可以在子元素中干預父元素的事件分發過程,但是 ACTION_DOWN 事件除外。
二、事件分發的原始碼分析
1.點選事件向上傳遞至頂級View
本文最開始有提到,事件總是先傳遞給Activity,然後傳遞給Window,再傳遞給頂級View(Activity→Window→DecorView),最後再按照事件分發機制一層一層向下去分發事件。所以事件最開始是傳遞給當前Activity,由 Activity 的 dispatchTouchEvent 方法進行事件派發,如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看到,Activity拿到事件之後會交給它所屬的 Window 進行分發,如果返回true,那麼整個事件迴圈就結束了,如果返回false,意味著所有 View 的 onTouchEvent 返回了false,最後 Activity 的 onTouchEvent 會被呼叫(上述結論中的第4條)。
接下來我們看下Window是如何處理事件的。值得注意的是 Window 是一個抽象類,其 superDispatchTouchEvent 方法也是一個抽象方法,因此我們需要檢視 Window 的具體實現類的原始碼,即 PhoneWindow 的 superDispatchTouchEvent 方法,如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
可見 PhoneWindow 將事件直接傳遞給了 DecorView。從這裡開始,事件就已經傳遞到頂級View了,也叫根View,通常而言頂級View都是ViewGroup。
2.頂級View對點選事件的分發過程
在上一節中,有說事件分發機制涉及到三個重要的方法,並且對於ViewGroup來說,如果它攔截該點選事件,那麼事件就會交由它來處理,如果它不攔截該事件,那麼則會交由它的子元素處理。我們先來看下 ViewGroup 的 dispatchTouchEvent 方法中有關是否攔截該事件的邏輯程式碼塊:
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {//DOWN事件總會進一步判斷是否攔截,除此之外的其他事件需要看mFirstTouchTarget是否為空
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 {//如果不是DOWN事件,並且mFirstTouchTarget為空
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;//攔截事件
}
上面註釋得比較清楚了,需要說明的有兩點。第一點是 mFirstTouchTarget 是一個指標,指向處理了該事件(DOWN事件)的子元素,換言之如果該 ViewGroup 的所有子元素都沒有處理該事件,那麼 mFirstTouchTarget == null。這樣就會使同一個事件序列中除DOWN以外的其他事件全部被 ViewGroup 攔截。這也就說明了上一節中的第4條結論。第二點是有關子元素請求父元素禁止攔截的,其方法為 requestDisallowInterceptTouchEvent。一旦子元素呼叫了 requestDisallowInterceptTouchEvent 方法,那麼父元素的對應flag就會被置高,ViewGroup 將無法攔截除了DOWN以外的其他事件。這是因為ViewGroup 在分發事件時,如果是 ACTION_DOWN 就會重置上述flag。如下:
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
通過上述程式碼塊的分析,就可以得出上一節中得出的第2條結論,以及第11條結論。
接著再看當ViewGroup不攔截此次事件時,如何分發給子元素處理,如下:
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 (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) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {//判斷條件內部呼叫child.dispatchTouchEvent方法
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);//方法內部完成了mFirstTouchTarget的賦值
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
上述程式碼首先遍歷ViewGroup的所有子元素,然後判斷子元素是否能夠接收到點選事件(判斷條件:1.是否在播放動畫 2.點選事件座標是否落在子元素內),如果子元素可以接收到事件,那麼將會交由它來處理,具體是在dispatchTransformedTouchEvent 方法中呼叫的,如下(注意此時傳入的第三個引數child非空):
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
如果子元素的 dispatchTouchEvent 方法返回 true,那麼 mFirstTouchTarget 就會被賦值並跳出 for 迴圈;如果子元素的 dispatchTouchEvent 方法返回 false,ViewGroup 就會把事件分發給下一個子元素。如果遍歷所有子元素後事件都沒有被合適處理,那麼ViewGroup就會自己處理該事件,如下(注意此時傳入的第三個引數為null):
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
}
3.普通View對點選事件的處理過程
老樣子,先看它的 dispatchTouchEvent 方法,如下:
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)) {//如果有設定onTouchListener並且onTouch返回true
result = true;
}
if (!result && onTouchEvent(event)) {//注意:如果此時result為true,onTouchEvent不執行
result = true;
}
}
......
return result;
}
通過上述程式碼,可以看到,View對點選事件的處理首先會判斷有沒有設定onTouchListener,如果onTouchListener中的onTouch 方法返回 true,那麼 onTouchEvent 就不會被呼叫,可見 onTouchListener 的優先順序高於 onTouchEvent,這樣的好處就是方便在外界處理點選事件。
接著再分析 onTouchEvent 的實現。先來看下上一節中的第9條結論是否正確,如下:
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是 disable 的,其返回值clickable只與三種點選狀態有關,需要同時為false,才返回false。
再看一下 onTouchEvent 中對點選事件的具體處理,如下:
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
......
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
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) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
......
}
mIgnoreNextUpEvent = false;
break;
......
}
return true;
}
從上述程式碼來看,只要 View 不是不可點選的非提示框控制元件,就會進入上面的 if 判斷,最後預設返回 true,消耗此次事件。這就證實了上一節中的第8條結論。然後就是第10條結論,當ACTION_UP事件發生時,會觸發 performClick 方法,如果 View 設定了 OnClickListener,那麼 performClick 方法內部會呼叫它的 onClick 方法。