Android進階知識(十):View的事件分發機制

Android進階知識(十):View的事件分發機制

  這一篇章中,筆者將介紹View的事件分發機制,需要提及的是,View的事件分發機制是View事件體系中極其重要的一點。以下對View的事件分發機制的解析,都是基於源代碼的基礎上進行的總結與分析,若有興趣的讀者可以通過《Android開發藝術探索》一書閱讀任玉剛老師是如何通過源碼來解釋以下內容的,筆者就不完全照搬了。

一、點擊事件的傳遞規則

  此處以點擊事件的傳遞規則作爲View事件分發機制的介紹,點擊事件的傳遞規則分析的對象是MotionEvent,即點擊事件。所謂的點擊事件的事件分發,其實就是對MotionEvent事件的分發過程,即當一個MotionEvent產生後,系統需要把這個事件傳遞給具體View,這個傳遞過程就是事件分發過程
  點擊事件的分發過程由三個很重要的方法來共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent。

  1. dispatchTouchEvent(MotionEvent ev)

  用來進行事件的分發。如果事件能夠傳遞給當前View,那麼此方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件

  1. onInterceptTouchEvent(MotionEvent event)

  在上述方法內部調用,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。

  1. onTouchEvent(Motion event)

  在dispatchTouchEvent方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一事件序列中,當前View無法再次接收到事件

  這三個方法的關係可以用如下的僞代碼來表示。

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean consume = false;  // 事件是否被消費
    if (onInterceptTouchEvent(ev)) {
        consume = onTouchEvent(ev);  // 攔截,調用自身的onTouchEvent方法
    } else {
        consume = child.dispatchTouchEvent(ev);  // 不攔截,調用子View的dispatchTouchEvent方法
    }
    return consume;
}

  點擊事件的傳遞規則:如果頂級ViewGroup攔截事件即onInterceptTouchEvent返回true,則事件由ViewGroup處理,這時如果ViewGroup的mOnTouchListener被設置,則onTouch會被調用,onTouch返回true則onTouchEvent不會被調用,反之則會被調用,在onTouchEvent方法中,如果設置了OnClickListener,則可以通過performClick()調用onClick;如果ViewGroup不攔截事件,則當前事件傳遞給其子元素,直到事件被處理
  從上述中可以看到,onTouch、onTouchEvent以及onClick的優先級關係爲:onTouch>onTouchEvent>onClick。以上點擊事件的傳遞規則用流程圖可以表示爲如下所示。
在這裏插入圖片描述
  對於上圖,需要提出的一點是,當onTouchEvent或者返回false,即事件不消費時,後續的流程需要根據所處理的事件進行分類,具體情況可以參照後面的結論4和結論5。
  當一個點擊事件產生之後,其傳遞過程遵循如下順序:Activity->Window->View,即事件總是先傳遞給Activity,Activity再傳遞給Window,最後Window再傳遞給頂級View。頂級View接收到事件後,就會按照事件分發機制去分發事件

二、事件傳遞機制結論

  爲了更好的理解整個事件分發傳遞機制,根據源碼可以得出對於down事件,View事件分發流程如下圖所示(這裏不設置onTouchListener和onClickListener,僅僅以onTouchEvent來討論)。
在這裏插入圖片描述

  值得一提的是,Activity的dispatchTouchEvent源碼如下所示。事件一開始交給了Activity所附屬的Window進行分發,返回true則事件結束,否則交給Activity的onToucEvent處理。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

  從以上對down事件的View事件分發流程圖可以得到如下結論。

  1. 同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束

  在手指接觸到離開屏幕的這一過程中,其所產生的一系列事件序列以down事件開始,中間含有數量不定的move事件,最終以up事件結束。

  1. 正常情況下,一個事件序列只能被一個View攔截且消耗

  一旦一個元素攔截了某此事件,那麼同一個事件序列內的所有事件都會直接交給它處理,因此同一事件序列中的事件不能分別由兩個View同時處理。從源碼上去理解的話就是,某View攔截了Down事件,那麼該View的上級ViewGroup會持有指向該View的TouchTarget
在這裏插入圖片描述

  1. 某個View一旦決定攔截,那麼這一個事件序列都只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的onInterceptTouchEvent不會再被調用

  值得一提的是,這裏提到的View指的是ViewGroup,因爲最底層的View是沒有onInterceptTouchEvent方法的。這個結論可以從源碼上去理解,如圖爲ViewGroup的dispatchTouchEvent的部分源碼。其中,mFirstTouchTarget指向攔截事件的子元素,如果該值爲null,即ViewGroup攔截事件,從中可以看出當Down事件時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;
}
  1. 某個View一旦開始處理事件,如果它不消耗ACTION_DOWN事件,那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交由它的父元素去處理,即父元素的onTouchEvent會被調用

  這一結論可以參照上面提到的流程圖。其原因在於由於View不消耗Down事件,因此頂層ViewGroup並不會持有指向View的TouchTarget對象。用動畫表示如下。
在這裏插入圖片描述

  1. 如果View不消耗除ACTION_DOWN以外的其他事件,那麼這個點擊事件就會消失,此時父元素的onTouchEvent並不會被調用,並且當前View可以持續收到後續的事件,最終這些消失的點擊事件會傳遞給Activity處理

  這一結論的原因在於,該View消耗了Down事件,那麼該View的上級ViewGroup會持有指向該View的TouchTarget對象,因此後續事件都會直接交付給該View。但是由於後續事件不消耗,在Activity的dispatchTouchEvent則會調用Activity的onTouchEvent方法(見上方源碼)。
  需要注意的是,這一結論的前提是,在ViewGroup中事件默認不攔截
在這裏插入圖片描述

  1. ViewGroup默認不攔截任何事件

  Android源碼中ViewGroup的onInterceptTouchEvent方法默認返回false

  1. View沒有onInterceptTouchEvent方法,一旦有點擊事件傳遞給它,那麼它的onTouchEvent方法就會被調用

在這裏插入圖片描述

  1. View的onTouchEvent默認都會消耗事件(返回true),除非它是不可點擊的(clickable和longClickable同時爲false)

  View的longClickable屬性默認都爲false,clickable屬性視情況而定

  1. View的enable屬性不影響onTouchEvent的默認返回值

  一個View哪怕是disable狀態,只要其clickable或者longClickable有一個爲true,那麼它的onTouchEvent就會返回true。

  1. onClick會發生的前提是當前View是可點擊的,並且它收到了down和up的事件

在這裏插入圖片描述

  1. 事件傳遞過程是由外向內,即事件總是先傳遞給父元素,然後再由父元素分發給子View

  這裏值得一提的是,通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是ACTION_DOWN事件除外。這也是解決滑動衝突的一種方式(具體使用方式見:Android進階知識(十一):View的滑動衝突
)。
  之所以ACTION_DOWN事件除外,原因在於對於一個事件序列,ViewGroup在分發事件時,如果是事件的開始即ACTION_DOWN事件,那麼將進行初始化,這導致了標誌爲FLAG_DISALLOW_INTERCEPT的重置。而requestDisallowInterceptTouchEvent方法恰恰就是對該標誌位的設置。
在這裏插入圖片描述

  以上就是對View事件分發機制的結論,如果不是很多的讀者,建議還是看看源碼,畢竟源碼纔是王道,其他都是胡扯。

參考資料:《Android開發藝術探索》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章