View事件分發機制

weixin_34413065發表於2017-05-19

事件這裡指的是一系列的MotionEvent(android.view.MotionEvent)類物件,實際上是一個動作碼和一個座標軸值的集合,動作碼指明瞭在觸控時發生的變化,座標軸值含有位置,時間等運動屬性資訊,常見動作型別即action code如下所示:

ACTION_MASKED 描述
ACTION_DOWN 當手指第一次觸控螢幕的時候產生,是一個事件的開始,包含著初始位置等資訊,該指標的指標資料索引始終為MotionEvent中0
ACTION_MOVE 當手機在螢幕上移動的時候,產生一系列的MOVE,包括座標軸和其他的運動屬性
ACTION_UP 最後一根手指離開螢幕時產生,標誌著事件的結束(或者是ACTION_CANCEL)
ACTION_CANCEL 動作終止,類似於ACTION_UP,但是不執行任何正常狀態下要觸發的動作
ACTION_POINTER_DOWN! 超出第一個進入螢幕的觸控手指,多點觸控的情況,對應的資料由getActionIndex()返回的索引獲取
ACTION_POINTER_UP 非最後一根手指離開螢幕,對應多點觸控的情況

當螢幕接收到點選,就產生了一個事件,緊接著就會觸發一系列特定的方法,一套完整的事件分發機制,從上到下依次是Activity→ViewGroup→View,實際上是按照檢視的層次結構進行分發的。

Activity對事件的分發

事件首先傳遞給當前螢幕上對應的Activity,它是用來和使用者進行交換的視窗,每個Activity都會有一個用於繪製其使用者介面的視窗,視窗通常會充滿螢幕。
Activity要執行的是dispatchTouchEvent(MotionEvent ev)函式,它可以把捕獲到的動作傳遞給根檢視。

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

首先如果是觸控事件的開始,即對應動作是ACTION_DOWN,進入if判斷內部的onUserInteraction(),該回撥函式的意義是表明了使用者早與當前Activity以某種方式進行交動,這些輸入事件可能來自鍵盤,觸控或者軌跡球。與該函式對應的onUserLeaveHint()可以一起過載,用以智慧地管理狀態列通知的活動等。

然後將事件交給Window進行分發,如果返回值不為true(一般因為超出Window邊界之外沒有View去接收觸控事件),則執行Activity本身的onTouchEvent(),這是最後的保障手段,預設返回值為真,表明動作已經被消耗處理。

而getWindow()獲取的Window類物件是一個抽象類,可以控制頂層類的外觀和行為策略,它的唯一實現類是android.view.PhoneWindow 。PhoneWindow實際上把事件傳遞給了DecorView。

DecorView是FrameLayout的子類,是最頂層的檢視,包含標題欄(title)和內容欄(content)。對應於它的唯一一個子檢視結構LinearLayout中的兩個FrameLayout子元素,其中一個是標題欄,會隨著主題的不同而不同,另一個是內容欄,此外內容欄ID:Android.R.id.content是固定的。而我們經常在呼叫的setContentView(View view),就是指的這個名稱為content的檢視。

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

DecorView中該函式的內容為super.dispatchTouchEvent(),而FrameLayout中並沒有這個方法,繼續向上,最終實際上對應的是ViewGroup中的這個方法。如此,整個觸控事件的動作便從Activity傳到頂級View。

ViewGroup對事件的分發

函式dispatchTouchEvent(MotionEvent ev),返回值表示事件是否分發處理。

清除狀態

首先是通過onFilterTouchEventForSecurity(MotionEvent)函式過濾TouchEvent,如果被攔截就直接返回FALSE。之後獲得動作型別,如果是ACTION_DOWN,表示一個新的手勢動作開始了,就取消清除所有之前的TouchTarget記錄,該類是一個單連結串列資料結構;並且重置觸控狀態,例如將FLAG_DISALLOW_INTERCEPT標誌位重置為0,表示允許父檢視中斷事件。

判定攔截

然後是判斷是否攔截事件,ViewGroup在兩種狀態下會攔截事件,當動作為ACTION_DOWN或者mFirstTouchTarget非空,具體程式碼如下:

// 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),必然要判斷是否攔截該事件。另一方面,函式會記錄下哪一個子View消耗了該事件,以便之後把後同一個事件序列的所有動作都交給它處理。如果當前事件被該ViewGroup攔截,那麼mFirstTouchTarget的值就為null,不管後續到來的動作是什麼,判決條件都是FALSE,即始終攔截後續的事件。即沒有觸控目標並且這個動作不是初始按下,就直接攔截該事件。

下一步是是獲取FLAG_DISALLOW_INTERCEPT標誌位的情況,這個標誌位可以通過呼叫requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法設定的,引數值為true,表示不允許攔截事件,不執行onInterceptTouchEvent函式,否則就執行。
在ViewGroup不攔截正常分發MotionEvent時,每一個動作都會經過onInterceptTouchEvent()方法。即onInterceptTouchEvent()函式使父檢視有機會在子檢視接收到事件以前,看到它所要接收處理的事件。

onInterceptTouchEvent()返回值含義

返回值 描述
true 攔截MotionEvent事件,這表示它不會被傳遞給子View,先前正在消耗處理事件的子檢視會收到ACTION_CANCEL,並且從該點開始的所有後續事件將傳送到父節點的onTouchEvent()方法
false 簡單地監視事件,事件依舊沿著檢視層次結構傳播到通常的目標,使用目標的onTouchEvent()方法處理事件
6112189-bc20667987ef32fe.png
**攔截判斷**

事件向下分發

如果該ViewGroup沒有攔截事件的時候,事件繼續向下分發,遍歷所有子檢視,找到一個子檢視來接收事件。

首先判斷檢視是否可以接收點事件,當檢視是可見的,檢視正在或者計劃播放動畫效果都是可以接收點選事件的,同時判斷事件對應的座標點是否在子檢視的區域內,不滿足條件的跳過,進行下一個。

if (!canViewReceivePointerEvents(child)
        || !isTransformedTouchPointInView(x, y, child, null)) {
    ev.setTargetAccessibilityFocus(false);
    continue;
}

最終在dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desirePointerIdBits)函式中,呼叫了子檢視的dispatchTouchEvent方法,這個過程中,對觸控事件的位置進行了轉換操作,實際上就是根據Scroll計算了位置偏移。這樣就將事件分發給了子檢視。

如果子檢視返回值為true,那麼就可以確定分發物件,跳出for迴圈。在此之前呼叫addTouchTarget方法,函式中對mFirstTouchTarget(TouchTarget類物件)賦值,它影響著ViewGroup的分發攔截方式,不為null時,當後邊的一系列動作到來時,就可以直接傳遞給相應的檢視,否則會攔截同一手勢序列中的所有觸控事件。

如果遍歷結束以後,都沒有找到消耗事件的子檢視,那麼ViewGroup會自己處理該事件,呼叫dispatchTransformeTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)函式,把子檢視引數設定為null,即把自身當做一個普通的View,呼叫父類View的dispatchTouchEvent方法。

View對事件的處理

View是一個單獨的檢視,沒有子檢視需要分發,所有直接由自身處理。

呼叫onTouch方法

首先是判斷View有沒有設定觸控監聽(View預設情況下是ENABLE的),以及是否設定OnTouchListener介面的回撥函式onTouch(View v, MotionEvent event),如果設定了,則呼叫該函式並取得處理之後的返回值。為true就不會呼叫後續的onTouchEvent方法,顯然onTouch方法優先順序高於onTouchEvent方法;如果返回值是false,則呼叫onTouchEvent方法。

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;
}

呼叫onTouchEvent方法

onTouchEvent方法進行分析,首先會涉及到檢視的狀態,檢視狀態有很多,常用的檢視狀態有如下:

名稱 描述
enabled 表示當前檢視可用狀態。可以通過setEnable方法進行位運算,改變檢視的狀態。如果對應的為為0,表示不可用,就無法響應onTouch事件,正常情況下(mViewFlags & ENABLED_MASK) == ENABLED
focused 檢視是否獲得焦點。判斷方式(mViewFlags & FOCUSABLE_MASK) == FOCUSABLE,類似於打遊戲通過手柄的上下左右鍵切焦點,requestFocus方法可以改變焦點
pressed 檢視是否處於按下狀態,按下物件,必須為Clickable。呼叫setPressed方法來對這一狀態進行改變,判斷方式(mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED
clickable 檢視是否可以點選,CLICKABLE對應的是setClickable函式,在設定點選的響應函式setOnClickListener時,自動會把檢視設定為可點選狀態

當View處於不可用狀態,即·(viewFlags & ENABLED_MASK) == DISABLED·時,函式的返回值為:

return (((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);

即只要是可以點選的,不論是長按LONG_CLICKABLE還是短按CLICKABLE,還是內容(用於觸控筆按鈕或滑鼠右鍵單擊)是可點選的CONTEXT_CLICKABLE,都會消耗點選事件,儘管處於不可用狀態,View只是不作出相應的響應。

接下來判斷檢視代理TouchDelegate,用於想要檢視具有比其實際檢視邊界更大的觸控面積。觸控區域被更改的檢視稱為委託檢視。mTouchDelegate的使用機制和mTouchListener,mOnClickListener等介面類似。

if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
    }
}

動作處理

最後是對具體的動作進行處理,當View是可點選檢視的時候,最後就一定會返回true,View的CLICKABLE屬性狀態要分情況看待,實質就是檢視是否可以點選,而View的LONG_CLICKABLE屬性狀態預設是關閉的。

通過switch-case語句,對不同的動作狀態進行不同的響應

ACTION_DOWN

初始化長按狀態,即把mHasPerformedLongPress賦值為false,尚未執行長按動作;
判斷View是否在一個可以滾動的容器中,比如ListView,進行延時,防止當使用者實際上是要滑動容器時,出現按下的狀態。
如果是在一個這樣的容器中,把mPrivateFlagsPREPRESSED標識位置1;通過postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout())函式短時間推延這個動作按下的反饋,設定推延的時長為 ViewConfiguration.getTapTimeout(預設115ms),如果在這個時間段內,該Message沒有從訊息佇列中取出,那麼等到時間導到以後就執行CheckForTap類內的run函式,內容包括:

  1. 設定檢視的PREPRESSED狀態位為0;
  2. 呼叫setPressed方法,把View的PRESSED狀態位設定為1;
  3. 執行checkForLongClick函式,檢測View的LONG_CLICKABLE標誌位,為1就把長按檢測的函式通過postDelayed(mPendingCheckForLongPress,ViewConfiguration.getLongPressTimeout()-delayOffset)加入MessageQueue中,設定延時的時間為長按的檢測時間(預設500ms)- 之前延時檢測按下狀態的時間(預設為115ms),即385ms。

同樣如果在這個時間段以內沒有從訊息佇列中取出該Message,執行以下內容:

  1. 執行performLongClick函式,即檢測檢視的OnLongClickListener介面,如果有定義,就呼叫onLongClick函式,根據它的返回結果,確定是否把mHasPerformedLongPress狀態設為已執行。

當檢視不在滾動容器內時,就立即顯示按下的反饋,直接呼叫setPressed方法,並且發出一個檢測長按的延遲事件為0毫秒的任務checkForLongClick(0, x, y)

總之就是檢測Tap和LongClick:把mPendingCheckForTapmPendingCheckForLongPress對應的run新增到Message佇列中

ACTION_MOVE

呼叫drawableHotspotChanged(x, y),表明View的熱點hotspot發生變化,並將變化傳播到檢視管理的Drawable物件或子檢視時。
然後用pointInView方法判斷確定給定觸控點(在區域性座標中)是否在檢視內,其中檢視的邊界都擴充套件了一個最小滑動距離TOUCH_SLOP的大小。如果移出了範圍:

  1. removeTapCallback函式,把PFLAG_PREPRESSED標誌位置0,並把之前CheckForTap物件mPendingCheckForTap要延時執行的Runnable通過removeCallbacks函式從訊息佇列中取消(如果還未執行的話);
  2. 檢視PRESSED狀態位,為1,說明已經過了檢測Tap的115ms,第一條中的Runnable已經執行了,要移除在run函式中新增的檢測長按的CheckForLongPress類物件mPendingCheckForLongPress;並且執行setPressed,把PRESSED標誌位置為0。

即只要使用者移出了對應的檢視的座標範圍,就將所有關於輕觸(tap)和長按(long press)的狀態全部取消。

ACTION_UP:

  1. 動作結束了,對之前的所有標識進行一個總的判斷,檢視PREPRESSEDPRESSED狀態位,不管哪一個是真,都進入下一階段;
  2. 為檢視請求焦點,並且進入觸控模式。如果View可以獲得焦點,並且還沒有獲得焦點,就請求焦點;
  3. 把之前為預按下狀態(PFLAG_PREPRESSED)的設定為按下,確保使用者可以看到按下狀態的出現。即如果 prepressed 值為true,呼叫 setPressed(true, x, y),把 PRESSED**標誌位設定為1,對應於下邊的第6點;
  4. 如果表示長按狀態的mHasPerformedLongPress為false,並且忽略下一次ACTION_UP事件的mIgnoreNextUpEvent狀態標識為false,就移除長按的檢測,因為手勢已經到此結束了,不可能再有長按了;
  5. 判斷mPerformClick,如果為null,初始化一個例項,該類實現了一個Runable介面,然後呼叫post,通過非同步處理Handler傳送run函式到訊息佇列尾部,如果新增Message失敗則直接執行performClick函式,確保執行,不直接呼叫performClick函式,可以讓View的其他視覺狀態在點選動作開始之前更新。
  6. 如果之前獲取的prepressed值為true,64毫秒(ViewConfiguration.getPresedStateDuration)後執行UnsetPressedState類物件mUnsetPresedState,否則立即執行mUnsetPresedState;最後無論如何mUnsetPresedState.run()都會執行,其內部呼叫了·setPresed(false)·,把的PRESSED標誌位重置為0,這樣實際上是為了保證之前處於預按下狀態的View,變為按下狀態,有一個足夠的延時(預設為64ms),來讓使用者觀察到。
  7. 最後呼叫·removeTapCallback·函式,目的是移除PFLAG_PREPRESSED狀態位,並且撤銷在訊息佇列中對應的tap延時執行內容 。

總結

這就是Android的事件分發機制的主要流程,關鍵方法如下

方法 呼叫位置 描述
dispatchTouchEvent A, VG, V 分發事件到子檢視
onInterceptTouchEvent VG 在傳遞到子檢視前攔截事件
onTouchEvent V 處理觸控事件

A代表Activity,VG代表ViewGroup, V 代表View

上述內容參照了網上一些部落格的描述。

相關文章