Android Tv 中的按鍵事件 KeyEvent 分發處理流程


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

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

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

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

流程

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 繼續分發。
    DecorView_dispatchKeyEvent.png

  2. Activity 會先獲取 PhoneWindow 對象,然后調用 PhoneWindow 的 superDispatchKeyEvent(),PhoneWindow 轉而調用 DecorView 的 superDispatchKeyEvent(),而 DecorView 則調用了 super.dispatchKeyEvent() 將事件交給父類分發, DecorView 繼承自 FrameLayout,但 FrameLayout 沒有實現 dispatchKeyEvent(),所以實際上是交給 ViewGroup 的 dispatchKeyEvent() 來分發。
    activity_dispatchKeyEvent.png
    PhoneWindow_superDispatchKeyEvent.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() 方法來處理事件。
    view_dispatchKeyEvent.png

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

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

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

小結

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()----最常用
舉個栗子:

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() 里,如下圖:

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按鍵事件傳遞流程(二)


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM