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