Android——按鍵事件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 樹內部的分發處理流程。

流程解析

ps:當我們在某個 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() 來分發。
在這裏插入圖片描述在這裏插入圖片描述在這裏插入圖片描述
1、ViewGroup 分發的邏輯我還不大理解,不過大體上知道 ViewGroup 遞歸尋找當前焦點的子 View,將事件傳給焦點子 View 的 dispatchKeyEvent() 分發,具體是如何遞歸尋找的這部分代碼待研究。

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

處理流程

ps: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() 來處理點擊事件,但注意,這裏的處理是最後纔會接收到的,所以很有可能事件在到達這裏之前就被消耗掉了。

小結

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

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()----最常用

舉個栗子:
在這裏插入圖片描述
這在 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() 裏,如下圖:
在這裏插入圖片描述
那麼,既然 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 事件傳遞給 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按鍵事件KeyEvent的分發處理流程解析

發佈了200 篇原創文章 · 獲贊 100 · 訪問量 53萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章