1. 程式人生 > >【Android TV】按鍵事件KeyEvent的分發處理流程解析

【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 處理。

分發流程

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

    這裡寫圖片描述

  2. 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 則表示事件還在。

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

    處理流程

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

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

    這裡寫圖片描述

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

小結

這裡寫圖片描述

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

  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()—-最常用
舉個栗子:
這裡寫圖片描述

上面這段程式碼能看懂麼?如果你已經清楚這程式碼是對左右方向按鍵的攔截,那麼你清楚各種 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 執行事件的分發,重寫的時候注意是否還需要使用父類的邏輯即可。

遺留問題

  1. 每次按鍵點選都會有 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 中,計劃下一篇就來梳理這部分內容。

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

    參考