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