天天看點

從源碼出發淺析 Android TV 的焦點移動原理 (上篇)

焦點(Focus)可以了解為選中态,在Android TV上起很重要的作用。一個視圖控件隻有在獲得焦點的狀态下,才能響應按鍵的Click事件。

從源碼出發淺析 Android TV 的焦點移動原理 (上篇)

上圖中,外面有一個綠色光圈的視圖,就是目前有焦點的視圖。

相對于手機上用手指點選螢幕産生的Click事件, 在使用Android TV的過程中,遙控器是一個主流的操作工具,通過點選遙控器的方向鍵來控制焦點的移動。當焦點移動到目标控件上之後,按下遙控器的确定鍵,才會觸發一個Click事件,進而去做下一步的處理。焦點的移動如下圖所示。

從源碼出發淺析 Android TV 的焦點移動原理 (上篇)

在處理焦點的時候,有一些基礎的用法需要知道。

首先,isFocusable()需要為true,一個控件才有資格可以擷取到焦點,可以通過setFocusable(boolean)方法來設定。如果想要在觸摸模式下擷取焦點(在我們用手機開發的過程中),需要isFocusableInTouchMode()為true,可以通過setFocusableInTouchMode(boolean)來設定。也可以直接在xml布局檔案中指定:

然後,就是控制焦點的移動了。在谷歌官方文檔中提到:

焦點移動的時候(預設的情況下),會按照一種算法去找在指定移動方向上最近的鄰居。在一些情況下,焦點的移動可能跟開發者的意圖不符,這時開發者可以在布局檔案中使用下面這些XML屬性來指定下一個焦點對象:

在Java代碼中,讓一個指定的View擷取焦點,可以調用它的requestFocus()方法。

盡管有了官方文檔中提到的基礎用法,但是在進行Android TV開發的過程中,還是經常會遇到一些焦點方面的問題或者疑問,如

“明明指定了焦點id,焦點卻跑丢了”

“onKeyDown裡居然截獲不到按鍵事件”

“我沒有做任何焦點處理,焦點是怎麼自己跑到那個View上的”

接下來,帶着這些問題,我們就從源碼的角度出發,簡單分析一下焦點的移動原理。本文以API 23作為參考。

在手機上,當手指觸摸螢幕時,會産生一個的觸摸事件,MotionEvent,進而完成點選,長按,滑動等行為。

而當按下遙控器的按鍵時,會産生一個按鍵事件,就是KeyEvent,包含“上”,“下”,“左”,“右”,“傳回”,“确定”等指令。焦點的處理就在KeyEvent的分發當中完成。

首先,KeyEvent會流轉到ViewRootImpl中開始進行處理,具體方法是内部類ViewPostImeInputStage中的processKeyEvent。(在API 17之前,是deliverKeyEventPostIme這個方法,邏輯大體一緻,本文僅以processKeyEvent作為參考)

從幾處關鍵的代碼,可以看到這裡的邏輯是:

先去執行mView的dispatchKeyEvent

之後會通過focusSearch去找下一個焦點視圖

如果目前本來就沒有焦點View,也會通過focusSearch找一個視圖

ViewRootImpl就是ViewRoot,繼承了ViewParent,但本身并不是一個View,可以看作是View樹的管理者。而這裡的成員變量mView就是DecorView,它指向的對象跟Window和Activity的mDecor指向的對象是同一個對象。所有的View組成了一個View樹,每一個View都是樹中的一個節點,如下圖所示:

從源碼出發淺析 Android TV 的焦點移動原理 (上篇)

最上層的根是DecorView,中間是各ViewGroup,最下層是View。

本文的分析都是基于View樹的。

在processKeyEvent中,首先走了mView的dispatchKeyEvent,也就是從DecorView開始進行KeyEvent的分發。

首先走DecorView的dispatchKeyEvent,之後會依次從Activity->ViewGroup->View的方向分發KeyEvent。

有興趣的話可以通過trace看一下KeyEvent的流轉方向:

從源碼出發淺析 Android TV 的焦點移動原理 (上篇)

對于KeyEvent的分發,之後會另開一篇細講,包括KeyEvent的處理優先級,長按的識别等,這裡隻簡單看一下ViewGroup和View的dispatchKeyEvent。

首先看ViewGroup的dispatchKeyEvent。

通過flag的判斷,有兩個處理路徑,也可以看到在處理keyEvent時,ViewGroup扮演兩個角色:

View的角色,也就是此時keyEvent需要在自己與其他View之間流轉

ViewGroup的角色,此時keyEvent需要在自己的子View之間流轉

當作View的時候,會調用自己View的dispatchKeyEvent。

當作ViewGroup的時候,會調用目前焦點View的dispatchKeyEvent。

其實,從概念上來看,都是調用目前有焦點View的dispatchKeyEvent,隻不過有時是自己本身,有時是他的子View。

再看看View的dispatchKeyEvent

View這裡,會優先處理OnKeyListener的onKey回調。

然後才可能會走KeyEvent的dispatch,最終走到View的OnKeyDown或者OnKeyUp。

将大體的流轉順序總結如下圖

從源碼出發淺析 Android TV 的焦點移動原理 (上篇)

其中任何一步都可以通過return true的方式來消費掉這個KeyEvent,結束這個分發過程。

如果dispatchKeyEvent沒有消費掉這個KeyEvent,會由系統來處理焦點的移動。

通過View的focusSearch方法找到下一個擷取焦點的View,然後調用requestFocus

那focusSearch是如何找到下一個焦點視圖的呢?

View并不會直接去找,而是交給它的parent去找。

判斷是否為頂層布局,若是則執行對應方法,若不是則繼續向上尋找,說明會從内到外的一層層進行判斷,直到最外層的布局為止。

有意思的是,Android提供了設定isRootNamespace的方法,但又hide了起來不讓使用,看來這個邏輯還有待優化。

最後的算法交給了FocusFinder

isRootNamespace()的ViewGroup把自己和目前焦點(View)以及方向傳入。

這裡root是上面isRootNamespace()為true的ViewGroup,focused是目前焦點視圖

優先找開發者指定的下一個focus的視圖 ,就是在xml或者代碼中指定NextFocusDirection Id的視圖

其次,根據算法去找,原理就是找在方向上最近的視圖

首先執行View的findUserSetNextFocus方法

比如,按了“左”方向鍵,如果設定了mNextFocusLeftId,則會通過findViewInsideOutShouldExist去找這個View。

mNextFocusLeftId一般是在xml裡面設定的,比如

也可以在java代碼裡設定

來看看findViewInsideOutShouldExist做了什麼。

ViewGroup的findViewByPredicateTraversal

可以看到,findViewInsideOutShouldExist這個方法從目前指定視圖去尋找指定id的視圖。首先從自己開始向下周遊,如果沒找到則從自己的parent開始向下周遊,直到找到id比對的視圖為止。

這裡要注意的是,也許存在多個相同id的視圖(比如ListView,RecyclerView,ViewPager等場景),但是這個方法隻會傳回在View樹中節點範圍最近的一個視圖,這就是為什麼有時候看似指定了focusId,但實際上焦點卻丢失的原因,因為焦點跑到了另一個“意想不到”的相同id的視圖上。

接《從源碼出發淺析Android TV的焦點移動原理(下篇)》