View事件分發機制

事件這里指的是一系列的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()方法處理事件
**攔截判斷**

事件向下分發

如果該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

上述內容參照了網上一些博客的描述。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容