Android KeyEvent 點選事件分發處理流程(一)

weixin_33670713發表於2017-08-21

本篇文章已授權微信公眾號 安卓巴士Android開發者門戶 獨家釋出

這次打算來梳理一下 Android Tv 中的按鍵點選事件 KeyEvent 的分發處理流程。一談到點選事件機制,網上資料已經非常齊全了,像什麼分發、攔截、處理三大流程啊;或者
dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 啊;再或者返回 true 表示消費,返回 false 不處理啊;還有說整個流程是個 U 型分發處理,什麼總經理髮布任務到員工處理反饋啊之類的。前輩們早已為我們梳理了一篇篇乾貨,也在儘可能的寫得通俗、易懂。

但是今天這篇的主題是:KeyEvent 的分發處理流程
說得明白點就是:Tv 上的遙控器按鍵的點選事件分發處理流程,也許你還沒反應過來。想想,手機上都是觸屏點選事件,而遙控器則是按鍵點選事件,兩種事件型別的分發處理機制自然有所不同,所以,如果不搞清楚這點,很容易在 Tv 應用開發中將這兩類事件分發機制混淆起來。

最簡單的區別就是,在 Tv 開發中已經不是再像觸屏手機那樣通過 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 來分發處理了,取而代之的則是需要使用 dispatchKeyEvent、onKeyDown/Up、onKeyLisenter 等來分發處理。

流程

1924341-38bcb57fa4617d95.jpg
dispatchKeyEvent事件分發傳遞流程圖.jpg

這次梳理的就只是 KeyEvent 在一個 View 樹內部的分發處理流程,簡單點說,也就是,你在某個 Activity 介面點選了遙控器的某個按鍵,然後這個按鍵事件在當前這個 Activity 裡是如何分發處理的。

流程圖涉及的主要方法和類:

  1. (PhoneWindow$)DecorView -> dispatchKeyEvent()
  2. Activity -> dispatchKeyEvent()
  3. ViewGroup -> dispatchKeyEvent()
  4. View -> dispatchKeyEvent()
  5. KeyEvent -> dispatch()
  6. View -> onKeyDown/Up()

硬體層、框架層那些按鍵事件的獲取、分發、處理太深奧了,啃不透。應用層的一部分事件分發流程也還暫時沒啃透,這次梳理的是在一個 View 樹內部的分發處理流程。

流程解析

ps:當我們在某個 Activity 介面中點選了某個遙控器按鍵時,會有 Action_Down 和 Action_Up 兩個 KeyEvent 進行分發處理,分發流程都一樣,區別就是最後交給 Activity 或 View 的 onKeyDown 或 onKeyUp 處理。

分發流程

  1. 當接收到 KeyEvent 事件時,首先是交給 (PhoneWindow$)DecorView 的 dispatchKeyEvent() 分發,而 DecorView 會去呼叫 Activity 的 dispatchKeyEvent(),交給 Activity 繼續分發。

    1924341-7a0b2f1a106bc1b5.png
    DecorView_dispatchKeyEvent.png

  2. Activity 會先獲取 PhoneWindow 物件,然後呼叫 PhoneWindow 的 superDispatchKeyEvent(),PhoneWindow 轉而呼叫 DecorView 的 superDispatchKeyEvent(),而 DecorView 則呼叫了 super.dispatchKeyEvent() 將事件交給父類分發, DecorView 繼承自 FrameLayout,但 FrameLayout 沒有實現 dispatchKeyEvent(),所以實際上是交給 ViewGroup 的 dispatchKeyEvent() 來分發。

    1924341-e8cc5298a223bc27.png
    activity_dispatchKeyEvent.png

    1924341-c9aa9cc3b8e47f80.png
    PhoneWindow_superDispatchKeyEvent.png

    1924341-5b9e0fe8a8010f4b.png
    DecorView_superDispatchKeyEvent.png

  3. ViewGroup 分發的邏輯我還不大理解,不過大體上知道 ViewGroup 遞迴尋找當前焦點的子 View,將事件傳給焦點子 View 的 dispatchKeyEvent() 分發,具體是如何遞迴尋找的這部分程式碼待研究。

  4. 以上就是一個 KeyEvent 事件的分發流程,跟觸屏手機事件傳遞有些不同的是,如果你沒重寫以上分發事件的相關類的相關分發方法的話,一個 KeyEvent 事件是肯定會從頂層 DecorView 分發到具體的子 View 的,因為它並沒有像 onInterceptTouchEvent 這種在某一層攔截的操作。

處理流程

ps:KeyEvent 事件的處理只有兩個地方,一個是 Activity,另一個則是具體的 View。ViewGroup 只負責分發,不會消耗事件。同 TouchEvent 一樣,返回 true 表示事件已消耗掉,返回 false 則表示事件還在。

  1. 當 KeyEvent 事件分到到具體的子 View 的 dispatchKeyEvent() 裡時,View 會先去看下有沒有設定 OnKeyListener 監聽器,有則回撥 OnKeyListener.onKey() 方法來處理事件。

    1924341-d63452ae450624d4.png
    view_dispatchKeyEvent.png

  2. 如果 View 沒有設定 OnKeyListener 或者 onKey() 返回 false 時,View 會通過呼叫 KeyEvent 的 dispatch() 方法來回撥 View 自己的 onKeyDown/Up() 來處理事件。

    1924341-1b8369d45a89999c.png
    keyEvent_dispatch.png

  3. 如果沒有重寫 View 的 onKeyUp 方法,而且事件是 ok(確認)按鍵的 Action_Up 事件時,View 會再去檢檢視是否有設定 OnClickListener 監聽器,有則呼叫 OnClickListener.onClick() 來消費事件,注意是消費,也就是說如果有對 View 設定 OnClickListener 監聽器的話,而且事件沒有在上面兩個步驟中消費掉的話,那麼就一定會在 onClick() 中被消耗掉,OnClickListener.onClick() 雖然並沒有 boolean 返回值,但是 View 在內部 dispatchKeyEvent() 裡分發事件給 onClick 時已經預設返回 true 表示事件被消耗掉了。

    1924341-ea943b9f519aecea.png
    View_onKeyUp.png

  4. 如果 View 沒有處理事件,也就是沒有設定 OnKeyListener 也沒有設定 OnClickListener,而且 onKeyDown/Up() 返回的是 false 時,將會通過分發事件的原路返回告知 Activity 當前事件還未被消耗,Activity 接收到 ViewGroup 返回的 false 訊息時就會去通過 KeyEvent 的 dispatch() 來呼叫 Activity 自己的 onKeyDown/Up() 事件,將事件交給 Activity 自己處理。這就是我們常見的在 Activity 裡重寫 onKeyDown/Up() 來處理點選事件,但注意,這裡的處理是最後才會接收到的,所以很有可能事件在到達這裡之前就被消耗掉了。

小結

1924341-7ae1780f9b29d3da.jpg
dispatchKeyEvent事件分發傳遞流程圖_LI.jpg

整體的分發處理流程就如上圖(手抖了,不然是直線的)所示,有些較重要的點我們可以來總結下:

  1. 如果對 DecorView 不大瞭解,那麼可以只側重我們較常接觸的點,如 Activity、 ViewGroup、 View,基於此:

  2. 事件分發:Activity 最先拿到 KeyEvent 事件,但沒辦法攔截自己處理(這裡你們肯定有反對意見,我下面解釋),然後將事件分發給 ViewGroup,而 ViewGroup 就只能是遞迴不斷的分發給子 View,事件絕不會在 ViewGroup 中被消耗掉的,最後子 View 接收到事件,分發流程結束,開始事件的處理。

  3. 事件處理:只有 Activity 和 View 能處理事件,View 根據情況選擇是在 OnKeyListener、 OnClickListener 還是在 onKeyDown/Up() 裡處理,Activity 只能在 onKeyDown/Up() 裡處理。

  4. 事件處理歸納一下其實就是四個地方,按處理順序排列如下:View 的 OnKeyListener.onKey()、onKeyDown/Up()、 OnClickListener.onClick()、 Activity 的 onKeyDown/Up()。一旦在四個地方的某處,事件被消耗了,也就是返回 true 了,事件將不會傳遞到後面的處理方法中去了。

為什麼我說 Activity 不能攔截事件交由自己處理呢?
在觸屏的 TouchEvent 點選事件機制中,我們可以通過重寫 onInterceptTouchEvent() 返回 true 來停止攔截事件的分發並自己處理事件,但在 KeyEvent 中並沒有這個方法,所以如果 dispatchKeyEvent() 只幹事件分發的事,事件處理都在 onKeyDwon/Up、onKey()、onClick() 中完成,這樣的話,Activity 確實沒辦法攔截事件分發交由自己的 onKeyDown/Up() 來處理。

但誰規定 dispatchKeyEvent() 只能幹事件傳遞的事呢,所以理論上按標準來說,Activity 無法攔截事件分發自己處理,但實際程式設計中,我經常碰見有人在 Activity 裡重寫 dispatchKeyEvent() 來處理事件,然後讓其返回 true 或 false,停止事件的分發。

使用場景

KeyEvent 事件的分發處理流程大體上知道是怎麼走的就行了,有興趣的可以再去看看原始碼,然後自己畫畫流程圖,就會更明白了。先把分發處理流程梳理清楚了,我們才知道該怎麼用,怎麼去重寫分發處理的方法,下面就講些使用場景:

1. 在 Activity 裡重寫 dispatchKeyEvent()----最常用
舉個栗子:

1924341-47ef9644e3e7017e.png
homeActivity_dispatchKeyEvent.png

這在 Tv 開發中是很常見的,經常會在 Activity 裡重寫 dispatchKeyEvent(),然後要麼去預先處理一些工作,要麼就是對特定的按鍵進行攔截。

上面這段程式碼能看懂麼?如果你已經清楚這程式碼是對左右方向按鍵的攔截,那麼你清楚各種 return 的作用麼,為什麼又有 return true,又有 return false,還有 return super.dispatchKeyEvent() 的?

先說結論:這裡的 return true 和 return false 都能起到按鍵攔截的作用,也就是子 View 不會接收到事件的分發或處理,Activity 的 onKeyDown/Up() 也不會收到任何訊息。

要明白這點,先得搞清楚什麼是 return, return 是返回的意思,什麼情況下需要返回,不就是呼叫你的那個方法需要你給個反饋,所以 return 的訊息是給上一級的呼叫者的,所以 return 只會對上一級的呼叫者的行為有影響。呼叫 Activity.dispatchKeyEvent() 的是 DecorView 的 dispatchKeyEvent() 裡,如下圖:

1924341-7e4a605ee70b57ce.png
DecorView_dispatchKeyEvent行為.png

那麼,既然 Activity 返回 true 或 false 都只對 DecorView 的行為有影響,那麼為什麼都能起到攔截事件分發的作用呢

這是因為,事件的分發邏輯其實是在 Activity.java 的 dispatchKeyEvent() 裡實現的,如果你重寫了 Activity 的 dispatchKeyEvent() 方法,那麼根據
Java 的特性程式就會執行你寫的 dispatchKeyEvent(),而不會執行基類 Activity.java 的方法,因此你在重寫的方法裡沒有自己實現事件的分發邏輯,事件當然就停止分發了啊。這也是為什麼返回 super.dispatchKeyEvent() 時事件會繼續分發,因為這最終會呼叫到基類 Activity.java 的 dispatchKeyEvent() 方法來執行事件分發的邏輯。

既然在 Activity 裡返回 true 或 false 都表示攔截,那麼有什麼區別麼?

當然有,因為會影響 DecorView 的行為,比如我們點選遙控器的方向鍵時介面上的焦點會跟隨著移動,這部分邏輯其實是在 DecorView 的上一級呼叫者中實現的,Activity 返回 true 的話,會導致 DecorView 也返回 true,那麼上一級將根據 DecorView 返回 true 的結果停止焦點的移動,這就是我們常見的在 Activity 裡重寫 dispatchKeyEvent() 返回 true 來實現停止焦點移動的原理。那麼,如果 Activity 返回的是 false,DecorView 也跟隨著返回 false,那麼上一級會繼續執行焦點移動的邏輯,表現出來的效果就是,介面上的焦點仍然會移動,但不會觸發 Activity 和 View 的事件分發和處理方法,因為已經被 Activity 攔截掉了。

最後,還有一個問題,在 View 或 ViewGroup 裡面重寫 dispatchKeyEvent() 作用會跟 Activity 一樣麼?

return true 或 false 或 super 的含義還是一樣的,但這裡要明白一個層次結構。上層:Activity,中層:ViewGroup,下層:View。

不管在哪一層重寫 dispatchKeyEvent(),如果返回 true 或 false,那麼它下層包括它本層都不會接收到事件的分發處理,但是它的上層會接收。因為攔截的效果只作用於該層及下層,而上層只會根據你返回的值,行為受到影響。

比如在 ViewGroup 中返回 true,Activity 的 onKeyDown/Up() 就不會被觸發,因為被消費了;如果返回 false,那麼事件就交由 Activity 處理。但不管返回 true 或 false,子 View 的 dispatchKeyEvent()、各種 onClick() 等事件處理方法都不會被觸發到了。

2. 在 Activity 裡重寫 onKeyDown/Up()----最常用
事件能走到這裡表示沒有被子 View 消費掉,這裡是我們能接觸到的層次裡面最後對事件進行處理的地方。而且就算我們在這裡做了一些工作,也沒有必要一定要返回 true。比如如果是方向鍵事件的話,你在這裡返回 true 會影響到上級停止焦點的移動,所以視情況而定。

3. 為某個具體的 View (如 TextView) 設定 OnKeyListener()----一般常用
這個應該也挺常見的,在 Activity 裡獲取某個控制元件的物件,然後設定點選事件監聽,然後去做一些事。

4. 為某個具體的 View (如 Button) 設定 OnClickListener()----一般常用
這個應該是更常見的了,setOnClickListener,很多場景都需要監聽某個控制元件的點選事件,明確一點就是:該監聽器監聽的是 ok(確認)鍵的 Action_Up 事件。

小結一下:

  1. dispatchKeyEvent(): 比較常見的是在 Activity 或自定義的 ViewGroup 型別控制元件裡面重寫該方法,有時是需要在事件開始分發前預處理一些工作,有時則是需要對特定按鍵進行攔截,注意一下攔截的作用域以及各種 return 值的作用即可。通常情況下,都會含有 return super,因為我們沒有必要對所有按鍵都進行攔截,有些按鍵仍舊需要繼續分發處理,因為 Android 系統預設對很多特殊按鍵都進行了處理。

  2. 明確 super 的含義,重寫的方法一般都會執行一下預設的邏輯工作,比如 dispatchKeyEvent 執行事件的分發,重寫的時候注意是否還需要使用父類的邏輯即可。

遺留問題

  1. 每次按鍵點選都會有 Action_Down 和 Action_Up 兩次事件,目前遇到這樣的場景,從 Activity A 開啟 Activity B,Action_Down 和 Action_Up 會在 Activity A 中分發處理,然後 Action_Up 又會在 Activity B 中分發處理。
    最開始的想法 Activity A 將 Action_Up 事件傳遞給 Activity B 進行處理,但是在 Activity A 中將 Action_Up 先消費掉即返回 true,發現 Activity B 中仍然會重新分發處理 Action_Up 事件。因此,目前對於 KeyEvent 事件在兩個 Activity 中是如何分發傳遞的還不大瞭解,這部分內容應該是在 ViewRootImpl 和 PhoneWindow 中,計劃下一篇就來梳理這部分內容。

  2. Tv 開發中最重要也讓人頭疼的就是焦點問題,通過遙控器方向鍵點選後可以控制焦點的移動,有時需要根據需求來控制焦點,比如我們經常做的就是在焦點到達邊界時重寫 dispatchKeyEvent 裡返回 true 來停止焦點的移動,為什麼可以這麼做呢?其實這部分內容也在 DecorView 的 dispatchKeyEvent 裡,DecorView 在高的 SDK 裡已經抽出來單獨一個類了,如果沒找到,那麼就去 PhoneWindow 裡找,舊的 SDK 裡,DecorView 是 PhoneWindow 的內部類,這部分內容也留著下次一起梳理吧。

參考

Android View框架總結(九)KeyEvent事件分發機制
Android按鍵事件傳遞流程(二)


1924341-35fed4659c556352.jpg
QQ圖片20180316094923.jpg

最近剛開通了公眾號,想激勵自己堅持寫作下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的可以點一波關注,謝謝支援~~

相關文章