Android開發使用的手機一般處于觸摸模式, 因此默認情況下并不會有焦點, 所以之前一直對焦點不是很熟悉. 但是在電視端開發上, 焦點的處理可以說直接影響了用戶體驗, 因此借此熟悉下焦點處理的流程.
本文著重介紹焦點相關的一些關鍵方法, 先從局部了解下焦點的一些基礎規則和行為特點.
獲取焦點的前提
-
View#isFocusable
返回true
, 如果在觸摸模式, 則View#isFocusableInTouchMode
也要返回true
- 控件必須可見
- 控件相關的父控件, 包括祖父控件等,
ViewGroup#getDescendantFocusability()
不能為ViewGroup#FOCUS_BLOCK_DESCENDANTS
View
獲取焦點
調用View#requestFocus
系列方法
進入View#requestFocusNoSearch
在該方法中會對控件的當前狀態進行判斷, 如果不符合獲取焦點的前提則直接返回false
告知調用方, 控件不會獲取焦點
只要符合前提就會繼續執行, 最終必定返回true
, 不論當前控件的焦點狀態是否有改變
符合前提則進入 View#handleFocusGainInternal
如果控件已經持有焦點, 則不會做任何事情, 直接結束流程
如果沒有焦點,
- 改變焦點標志位, 此時
View#isFocused
就會返回true
了 - 通過
ViewParent#requestChildFocus
通知父控件即將獲取焦點 - 通知其他部件焦點狀態發生變化(略, 本文不關心)
- 觸發
OnGlobalFocusChangeListener
的回調 - 觸發
OnFocusChangeListener
回調 - 重繪, 結束流程
清除焦點
調用View#clearFocus
主動放棄焦點
如果控件本身沒有焦點, 則什么都不會發生
如果控件持有焦點
- 改變焦點標志位
- 通過
ViewParent#clearChildFocus
通知父控件, 當前控件放棄焦點 - 觸發
OnFocusChangeListener
回調 - 調用當前控件的根控件(
rootView
)的requestFocus
方法 - 如果步驟4中沒有找到新的焦點控件, 則觸發
OnGlobalFocusChangeListener
的回調, 注: 如果找到新的焦點控件, 那么新的控件獲取焦點的過程中就會回調OnGlobalFocusChangeListener
, 所以這里只有沒找到才進行步驟5
注: 由上流程可以知道, 如果根控件查找控件的時候找到的控件還是這個控件, 那么OnFocusChangeListener
就會被調用兩次, 先失去焦點, 然后又獲取到焦點
ViewGroup
焦點分發策略DescendantFocusability
-
FOCUS_BLOCK_DESCENDANTS
: 攔截焦點, 直接自己嘗試獲取焦點 -
FOCUS_BEFORE_DESCENDANTS
: 首先自己嘗試獲取焦點, 如果自己不能獲取焦點, 則嘗試讓子控件獲取焦點 -
FOCUS_AFTER_DESCENDANTS
: 首先嘗試把焦點給子控件, 如果所有子控件都不要, 則自己嘗試獲取焦點
獲取焦點
根據焦點分發策略決定下面兩個方法的調用順序
通過View#requestFocus
自己獲取焦點
把ViewGroup
看作View
, 直接走View
獲取焦點的流程來獲取焦點
進入onRequestFocusInDescendants
可以傳入方向來改變遍歷的順序, 默認是從0遞增
遍歷子控件, 調用子控件的View#requestFocus
來嘗試把焦點給可見的子控件, 某個子控件成功獲取到焦點后, 停止遍歷
注: 重寫該方法可以改變ViewGroup
分發焦點給子控件的行為, 例如遍歷順序
清除焦點
如果焦點控件不是它的子控件, 那么直接把當前的ViewGroup
看作View
走View#clearFocus
流程, 反之則調用焦點控件的View#clearFocus
.
注: 區別在于重新分發焦點時的選擇范圍.
ViewParent
ViewParent
是一個接口, 表示了一個父控件應該具備的功能, ViewGroup
實現了該接口.
與焦點相關的接口有4個
clearChildFocus
當子控件主動放棄焦點的時候會通過這個方法通知父控件.
在ViewGroup
的默認實現中, 會置空當前焦點控件, 表示該父控件下沒有子控件獲取焦點, 接著把這個事件通知給上級父控件.
注1: 這個方法名有點讓人誤解, 應該把這個方法看作一個回調, 表明了一個狀態, 在這個方法中并沒有做清除焦點的操作, 實際的清除動作是在View#clearFocus
中完成的, 這個方法也是在這個流程中被調用的. 而且是在子控件已經放棄焦點后調用.
注2: 區分主動放棄和因為其他控件獲取了焦點而被動丟失焦點的情況
requestChildFocus
當子控件獲取了焦點后, 通過這個方法通知父控件. 同clearChildFocus
類似, 應該把這個方法看作是一個回調.
在ViewGroup
的默認實現中, 因為同時只會有一個焦點, 因此在這里應該把舊焦點清除掉, 大致流程如下
- 如果焦點分發策略為
FOCUS_BLOCK_DESCENDANTS
則什么也不干 - 如果父控件自身有焦點, 通過
View#unFocus
清除焦點 - 如果父控件當前已經有焦點控件, 并且和新的控件不一致, 那么通過
View#unFocus
清除舊焦點控件的焦點 - 向上傳遞這個事件
內部清除焦點View#unFocus
這個方法和View#clearFocus
相同點在于都會執行View#clearFocusInternal
方法, 區別在于unFocus
只會執行clearFocus
中, 上文清除焦點中提到的1, 3步驟, 因此不會通知父控件, 不會觸犯requestChildFocus
回調, 因為這個方法是在子控件被動失去焦點時調用的, 所以也不會觸發焦點分發.
因此新舊焦點切換的大致流程是
- 新焦點控件獲取焦點
- 新焦點控件通知父控件
- 父控件清除舊焦點控件的焦點
- 舊焦點控件回調
OnFocusChangeListener
- 觸發
OnGlobalFocusChangeListener
的回調 - 新焦點控件回調
OnFocusChangeListener
focusableViewAvailable
通知父控件, 子控件的狀態發生改變, 從不能獲取焦點, 變成可能可以獲取焦點.
有兩種情況會被調用
- 子控件從unFocusable變為focusable
- 子控件從不可見變為可見, 即使它不是focusable也會調用, 因此它的子控件可能可以獲取焦點.
而ViewGroup
中的默認實現只是在符合條件的情況下把這個事件向上傳遞給自己的父控件.
focusSearch(View, int)
查找指定方向中最近的, 想要獲取焦點的控件.
這個方法直接決定了焦點的移動規則, 非常重要.
在ViewGroup
的默認實現中, 會一直向上傳遞, 直到根控件, 接著調用FocusFinder#findNextFocus
方法查找合適的控件. 稍后再分析這個方法.
View
中有一個同名的方法focusSearch(int)
, 該方法直接調用了父控件的focusSearch(View, int)
來查找下一個焦點控件
findNextFocus
查找步驟大致如下
手動指定
如果有通過android:nextFocusDown
等手動指定控件, 則返回對應方向的控件
動態計算
- 獲取所有可以獲取焦點的控件的集合
- 計算相對當前焦點控件的坐標
- 根據方向選擇合適的控件
總結
- 分析的過程要注意區分
View
和ViewGroup
的差異和新焦點和舊焦點控件的方法調用. -
ViewParent
是一個接口, 其中一些方法應該看作是回調, 子控件通過這些回調通知父控件焦點狀態發生了變化, 提醒父控件進行相關處理, 確保只有一個焦點存在 - 某個控件獲取焦點的同時, 舊焦點控件也會失去焦點, 這個動作是在
requestChildFocus
中發生的. - 焦點移動的關鍵方法是
focusSearch(View, int)
, 下一篇文章一點見解: 焦點那點事(二)接著分析焦點移動的發起點和過程.