【Android TV】按鍵事件KeyEvent的分發處理流程解析
這次打算來梳理一下 Android Tv 中的按鍵點選事件 KeyEvent 的分發處理流程。一談到點選事件機制,網上資料已經非常齊全了,像什麼分發、攔截、處理三大流程啊;或者dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 啊;再或者返回 true 表示消費,返回 false 不處理啊;還有說整個流程是個 U 型分發處理,什麼總經理髮布任務到員工處理反饋啊之類的。前輩們早已為我們梳理了一篇篇乾貨,也在儘可能的寫得通俗、易懂。
但是今天這篇的主題是:KeyEvent 的分發處理流程 說得明白點就是:Tv 上的遙控器按鍵的點選事件分發處理流程,也許你還沒反應過來。想想,手機上都是觸屏點選事件,而遙控器則是按鍵點選事件,兩種事件型別的分發處理機制自然有所不同,所以,如果不搞清楚這點,很容易在 Tv 應用開發中將這兩類事件分發機制混淆起來。
最簡單的區別就是,在 Tv 開發中已經不是再像觸屏手機那樣通過 dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent 來分發處理了,取而代之的則是需要使用 dispatchKeyEvent、onKeyDown/Up、onKeyLisenter 等來分發處理。
流程
這次梳理的就只是 KeyEvent 在一個 View 樹內部的分發處理流程,簡單點說,也就是,你在某個 Activity 介面點選了遙控器的某個按鍵,然後這個按鍵事件在當前這個 Activity 裡是如何分發處理的。
流程圖涉及的主要方法和類:
(PhoneWindow$)DecorView -> dispatchKeyEvent()
Activity -> dispatchKeyEvent()
ViewGroup -> dispatchKeyEvent()
View -> dispatchKeyEvent()
KeyEvent -> dispatch()
View -> onKeyDown/Up()
硬體層、框架層那些按鍵事件的獲取、分發、處理太深奧了,啃不透。應用層的一部分事件分發流程也還暫時沒啃透,這次梳理的是在一個 View 樹內部的分發處理流程。
流程解析
當我們在某個 Activity 介面中點選了某個遙控器按鍵時,會有 Action_Down 和 Action_Up 兩個 KeyEvent 進行分發處理,分發流程都一樣,區別就是最後交給 Activity 或 View 的 onKeyDown 或 onKeyUp 處理。
分發流程
當接收到 KeyEvent 事件時,首先是交給 (PhoneWindow$)DecorView 的 dispatchKeyEvent() 分發,而 DecorView 會去呼叫 Activity 的 dispatchKeyEvent(),交給 Activity 繼續分發。
Activity 會先獲取 PhoneWindow 物件,然後呼叫 PhoneWindow 的superDispatchKeyEvent(), PhoneWindow 轉而呼叫 DecorView 的superDispatchKeyEvent(),而 DecorView 則呼叫了 super.dispatchKeyEvent()將事件交給父類分發, DecorView 繼承自 FrameLayout,但 FrameLayout 沒有實現dispatchKeyEvent(),所以實際上是交給 ViewGroup 的dispatchKeyEvent() 來分發。
ViewGroup 分發的邏輯我還不大理解,不過大體上知道 ViewGroup 遞迴尋找當前焦點的子 View,將事件傳給焦點子 View 的 dispatchKeyEvent() 分發,具體是如何遞迴尋找的這部分程式碼待研究。
以上就是一個 KeyEvent 事件的分發流程,跟觸屏手機事件傳遞有些不同的是,如果你沒重寫以上分發事件的相關類的相關分發方法的話,一個 KeyEvent 事件是肯定會從頂層 DecorView 分發到具體的子 View 的,因為它並沒有像 onInterceptTouchEvent 這種在某一層攔截的操作。
處理流程
KeyEvent 事件的處理只有兩個地方,一個是 Activity,另一個則是具體的 View。ViewGroup 只負責分發,不會消耗事件。同 TouchEvent 一樣,返回 true 表示事件已消耗掉,返回 false 則表示事件還在。
當 KeyEvent 事件分到到具體的子 View 的 dispatchKeyEvent() 裡時,View 會先去看下有沒有設定 OnKeyListener 監聽器,有則回撥 OnKeyListener.onKey() 方法來處理事件。
如果 View 沒有設定 OnKeyListener 或者 onKey() 返回 false 時,View 會通過呼叫 KeyEvent
的 dispatch() 方法來回調 View 自己的 onKeyDown/Up() 來處理事件。
如果沒有重寫 View 的 onKeyUp 方法,而且事件是 ok(確認)按鍵的 Action_Up 事件時,View
會再去檢檢視是否有設定 OnClickListener 監聽器,有則呼叫 OnClickListener.onClick()
來消費事件,注意是消費,也就是說如果有對 View 設定 OnClickListener
監聽器的話,而且事件沒有在上面兩個步驟中消費掉的話,那麼就一定會在 onClick()
中被消耗掉,OnClickListener.onClick() 雖然並沒有 boolean 返回值,但是 View 在內部
dispatchKeyEvent() 裡分發事件給 onClick 時已經預設返回 true 表示事件被消耗掉了。如果 View 沒有處理事件,也就是沒有設定 OnKeyListener 也沒有設定 OnClickListener,而且
onKeyDown/Up() 返回的是 false 時,將會通過分發事件的原路返回告知 Activity當前事件還未被消耗,Activity 接收到 ViewGroup 返回的 false 訊息時就會去通過 KeyEvent 的dispatch() 來呼叫 Activity 自己的 onKeyDown/Up() 事件,將事件交給 Activity自己處理。這就是我們常見的在 Activity 裡重寫 onKeyDown/Up()來處理點選事件,但注意,這裡的處 理是最後才會接收到的,所以很有可能事件在到達這裡之前就被消耗掉了。
小結
整體的分發處理流程就如上圖(手抖了,不然是直線的)所示,有些較重要的點我們可以來總結下:
- 如果對 DecorView 不大瞭解,那麼可以只側重我們較常接觸的點,如 Activity、 ViewGroup、 View,基於此。
- 事件分發:Activity 最先拿到 KeyEvent 事件,但沒辦法攔截自己處理(這裡你們肯定有反對意見,我下面解釋),然後將事件分發給 ViewGroup,而 ViewGroup 就只能是遞迴不斷的分發給子 View,事件絕不會在 ViewGroup 中被消耗掉的,最後子 View接收到事件,分發流程結束,開始事件的處理。
- 事件處理:只有 Activity 和 View 能處理事件,View 根據情況選擇是在 OnKeyListener、
OnClickListener 還是在 onKeyDown/Up() 裡處理,Activity 只能在 onKeyDown/Up()
裡處理。 - 事件處理歸納一下其實就是四個地方,按處理順序排列如下: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()—-最常用
舉個栗子:
上面這段程式碼能看懂麼?如果你已經清楚這程式碼是對左右方向按鍵的攔截,那麼你清楚各種 return 的作用麼,為什麼又有 return true,又有 return false,還有 return super.dispatchKeyEvent() 的?
先說結論:這裡的 return true 和 return false 都能起到按鍵攔截的作用,也就是子 View 不會接收到事件的分發或處理,Activity 的 onKeyDown/Up() 也不會收到任何訊息。
要明白這點,先得搞清楚什麼是 return, return 是返回的意思,什麼情況下需要返回,不就是呼叫你的那個方法需要你給個反饋,所以 return 的訊息是給上一級的呼叫者的,所以 return 只會對上一級的呼叫者的行為有影響。呼叫 Activity.dispatchKeyEvent() 的是 DecorView 的 dispatchKeyEvent() 裡,如下圖:
那麼,既然 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 事件。
小結一下:
dispatchKeyEvent(): 比較常見的是在 Activity 或自定義的 ViewGroup 型別控制元件裡面重寫該方法,有時是需要在事件開始分發前預處理一些工作,有時則是需要對特定按鍵進行攔截,注意一下攔截的作用域以及各種 return 值的作用即可。通常情況下,都會含有 return super,因為我們沒有必要對所有按鍵都進行攔截,有些按鍵仍舊需要繼續分發處理,因為 Android 系統預設對很多特殊按鍵都進行了處理。
明確 super 的含義,重寫的方法一般都會執行一下預設的邏輯工作,比如 dispatchKeyEvent 執行事件的分發,重寫的時候注意是否還需要使用父類的邏輯即可。
遺留問題
每次按鍵點選都會有 Action_Down 和 Action_Up 兩次事件,目前遇到這樣的場景,從 Activity A 開啟
Activity B,Action_Down 和 Action_Up 會在 Activity A 中分發處理,然後 Action_Up又會在 Activity B 中分發處理。 最開始的想法 Activity A 將 Action_Up 事件傳遞給 ActivityB 進行處理,但是在 Activity A 中將 Action_Up 先消費掉即返回 true,發現 Activity B中仍然會重新分發處理 Action_Up 事件。因此,目前對於 KeyEvent 事件在兩個 Activity中是如何分發傳遞的還不大瞭解,這部分內容應該是在 ViewRootImpl 和 PhoneWindow 中,計劃下一篇就來梳理這部分內容。Tv 開發中最重要也讓人頭疼的就是焦點問題,通過遙控器方向鍵點選後可以控制焦點的移動,有時需要根據需求來控制焦點,比如我們經常做的就是在焦點到達邊界時重寫dispatchKeyEvent 裡返回 true 來停止焦點的移動,為什麼可以這麼做呢?其實這部分內容也在 DecorView 的dispatchKeyEvent 裡,DecorView 在高的 SDK 裡已經抽出來單獨一個類了,如果沒找到,那麼就去PhoneWindow 裡找,舊的 SDK 裡,DecorView 是 PhoneWindow 的內部類,這部分內容也留著下次一起梳理吧。
參考