上一篇文章, 一點(diǎn)見解: 焦點(diǎn)那點(diǎn)事(一), 了解了焦點(diǎn)相關(guān)的一些基本知識(shí), 提到焦點(diǎn)切換的關(guān)鍵方法ViewParent#focusSearch, 本文接著看, 焦點(diǎn)是從什么時(shí)候產(chǎn)生的, 又是如何在控件間切換的, 當(dāng)控件被移除或者新增進(jìn)布局時(shí)焦點(diǎn)又會(huì)發(fā)生什么變化.
焦點(diǎn)產(chǎn)生
頁面創(chuàng)建出來后, 什么時(shí)候開始分發(fā)焦點(diǎn)?
關(guān)于頁面創(chuàng)建流程的和繪制過程的文章有很多, 這里不再累述, 通過這些文章, 我們可以知道頁面控件的繪制入口是ViewRootImpl#performTraversals方法.
在這個(gè)方法中, 如果是第一次執(zhí)行這個(gè)方法, 同時(shí), ViewRoot相關(guān)聯(lián)的DecorView沒有焦點(diǎn)控件, 那么就會(huì)調(diào)用DecorView#requestFocus, 實(shí)際上也就是調(diào)用了ViewGroup#requestFocus, 上一篇文章一點(diǎn)見解: 焦點(diǎn)那點(diǎn)事(一)介紹過, 在這個(gè)方法里, 會(huì)遍歷子控件, 執(zhí)行View#requestFocus直到某個(gè)控件持有焦點(diǎn).
疑問: home鍵退出頁面, 然后返回時(shí), 如果當(dāng)前頁面沒有焦點(diǎn), 還會(huì)走一次
requestFocus, 這種情況是哪里觸發(fā)的?
焦點(diǎn)切換
雖然在觸摸模式也能產(chǎn)生焦點(diǎn), 但是一般不會(huì)用到, 因此這里著重分析通過鍵盤操作來切換焦點(diǎn)的情況.
起點(diǎn)
既然是通過鍵盤切換焦點(diǎn), 因此從鍵盤事件開始入手.
關(guān)于輸入事件的處理流程已經(jīng)有很多文章了, 這個(gè)也不是本文關(guān)注的重點(diǎn), 因此不再累述, 可以參考原來Android觸控機(jī)制竟是這樣的?.
概括起來就是
-
ViewRootImpl通過一個(gè)Receiver接收硬件發(fā)送過來的事件(包括觸摸事件和鍵盤事件) - 然后
ViewRootImpl會(huì)把這些事件放在隊(duì)列中 - 然后再按順序取出這些事件通過
InputStage相關(guān)類分發(fā)出去, 最后會(huì)執(zhí)行InputStage#onProcess()方法 - 其中在
ViewPostImeInputStage類中, 如果輸入的事件是鍵盤事件, 那么就會(huì)調(diào)用ViewPostImeInputStage#processKeyEvent()方法
processKeyEvent()
在這個(gè)方法里, 會(huì)先把事件傳遞給ViewGroup#dispatchKeyEvent()方法, 如果這個(gè)方法沒有消費(fèi)掉這個(gè)事件, 并且這個(gè)事件是方向事件的按下事件, 例如KeyEvent.KEYCODE_DPAD_LEFT等, 那么就會(huì)觸發(fā)焦點(diǎn)切換, 也就是focusSearch方法.
ViewGroup#dispatchKeyEvent()
首先看這個(gè)方法, 因?yàn)樵?code>ViewRootImpl中持有的是DecorView, 它本質(zhì)上是一個(gè)FrameLayout, 因此分發(fā)鍵盤事件時(shí)實(shí)際調(diào)用的會(huì)是ViewGroup#dispatchKeyEvent().
在這個(gè)方法里
- 如果這個(gè)
ViewGroup持有焦點(diǎn), 那么就會(huì)直接調(diào)用View#dispatchKeyEvent - 如果是它的子控件持有焦點(diǎn), 那么就會(huì)調(diào)用子控件的
View#dispatchKeyEvent
在View#dispatchKeyEvent里面
- 詢問
OnKeyListener是否消費(fèi)這個(gè)事件 - 消費(fèi)確認(rèn)相關(guān)的按鍵事件, 例如
KeyEvent.KEYCODE_DPAD_CENTER等
由上可以知道, 一般情況下, ViewGroup#dispatchKeyEvent()只會(huì)消費(fèi)確認(rèn)事件, 方向事件是會(huì)繼續(xù)執(zhí)行下一步的.
觸發(fā)焦點(diǎn)切換
方向事件的按下事件表明, 在按下的時(shí)候就會(huì)觸發(fā)焦點(diǎn)切換了, 這解釋了為什么長按方向鍵會(huì)一直切換焦點(diǎn).
焦點(diǎn)切換時(shí)
- 如果當(dāng)前已經(jīng)存在焦點(diǎn), 那么就調(diào)用當(dāng)前焦點(diǎn)控件的
View#focusSearch(int), 這個(gè)方法又會(huì)馬上調(diào)用ViewParent#focusSearch(View, int)方法, 注意區(qū)分這兩個(gè)方法, 雖然同名, 但不是同一個(gè)方法. - 如果不存在焦點(diǎn), 那么就會(huì)調(diào)用
ViewRootImpl#focusSearch, 這個(gè)方法直接調(diào)用了FocusFinder#findNextFocus來查找合適的控件 - 當(dāng)找到具體的控件后, 就會(huì)調(diào)用該控件的
requestFocus方法
這個(gè)過程說明
- 按下方向鍵時(shí), 如果沒有控件持有焦點(diǎn), 那么我們不能控制候選控件的選擇
- 按下方向鍵時(shí), 如果有控件持有焦點(diǎn), 那么可以通過重寫這個(gè)控件的父控件的
ViewParent#focusSearch來控制候選控件的選擇- 無論是如何得到候選控件, 這個(gè)控件是通過
requestFocus來獲取焦點(diǎn)的, 后續(xù)流程參考一點(diǎn)見解: 焦點(diǎn)那點(diǎn)事(一)
焦點(diǎn)控件失去焦點(diǎn)資格
上一篇文章提到控件要獲取焦點(diǎn)必須符合
View#isFocusable返回true, 如果在觸摸模式, 則View#isFocusableInTouchMode也要返回true- 控件必須可見
- 控件相關(guān)的父控件, 包括祖父控件等,
ViewGroup#getDescendantFocusability()不能為ViewGroup#FOCUS_BLOCK_DESCENDANTS
unFocusable和unVisibility
改變控件的這兩個(gè)狀態(tài), 最終會(huì)調(diào)用View#setFlags方法, 在該方法中, 如果焦點(diǎn)控件是變?yōu)榱瞬豢梢娀蛘卟豢色@取焦點(diǎn), 那么就會(huì)調(diào)用View#clearFocus來清除焦點(diǎn), 跟手動(dòng)清除焦點(diǎn)流程一樣.
FOCUS_BLOCK_DESCENDANTS
如果父控件突然變?yōu)榱?code>FOCUS_BLOCK_DESCENDANTS, 不會(huì)影響當(dāng)前焦點(diǎn)控件的狀態(tài), 只會(huì)影響下一次焦點(diǎn)分發(fā)/查找的流程.
焦點(diǎn)控件被移除
控件被移除, 最終都會(huì)調(diào)用ViewGroup#removeViewInternal方法, 在這個(gè)方法中, 首先會(huì)調(diào)用View#unFocus來清除焦點(diǎn), 具體參考上一篇文章的介紹, 因?yàn)?code>View#unFocus方法不會(huì)調(diào)用ViewParent#clearChildFocus, 因此ViewGroup會(huì)主動(dòng)調(diào)用自己的clearChildFocus方法, 緊接著會(huì)調(diào)用View#rootViewRequestFocus方法, 在這個(gè)方法中會(huì)調(diào)用getRootView()#requestFocus, 然后就會(huì)遍歷一次控件樹來重新分發(fā)焦點(diǎn).
控件獲得焦點(diǎn)資格
和失去焦點(diǎn)資格類似, 最終會(huì)調(diào)用View#setFlags方法, 然后調(diào)用ViewParent#focusableViewAvailable方法, 默認(rèn)實(shí)現(xiàn)中會(huì)一直向上級(jí)父控件傳遞, 最終就會(huì)調(diào)用ViewRootImpl#focusableViewAvailable方法, 在這個(gè)方法中, 兩種情況下這個(gè)新控件可以獲得焦點(diǎn)
- 如果當(dāng)前沒有焦點(diǎn)控件, 那么就會(huì)調(diào)用這個(gè)新獲得焦點(diǎn)資格的控件的
requestFocus方法 - 如果當(dāng)前有焦點(diǎn)控件, 同時(shí)新的這個(gè)控件是當(dāng)前焦點(diǎn)控件的子控件, 而這個(gè)焦點(diǎn)控件的焦點(diǎn)分發(fā)策略為
FOCUS_AFTER_DESCENDANTS, 那么還是會(huì)調(diào)用requestFocus來把焦點(diǎn)給這個(gè)新的控件
新增控件(有焦點(diǎn)資格)
通過addView方式添加控件, 都會(huì)調(diào)用ViewGroup#addViewInner方法, 在這個(gè)方法中, 如果新增的控件的hasFocus方法為true, 那么就會(huì)調(diào)用父控件的ViewParent#requestChildFocus, 參考上一篇文章可以知道, 在這個(gè)方法里會(huì)把現(xiàn)有的焦點(diǎn)控件的焦點(diǎn)清除掉. 也就是說, 新增的控件如果持有焦點(diǎn), 那么就會(huì)替換現(xiàn)有的控件成為焦點(diǎn)控件.
如果新增的控件沒有持有焦點(diǎn), 即使它有焦點(diǎn)資格, 也不會(huì)有任何焦點(diǎn)相關(guān)的回調(diào)
注意: 新增(addView)控件時(shí), 無論這個(gè)控件會(huì)不會(huì)獲得焦點(diǎn),
ViewParent#focusableViewAvailable都不會(huì)被調(diào)用.
總結(jié)
- 頁面第一次刷新布局時(shí)會(huì)通過根控件的
requestFocus來尋找第一個(gè)焦點(diǎn)控件 - 當(dāng)鍵盤輸入方向事件時(shí), 頁面會(huì)通過
ViewParent#focusSearch來尋找下一個(gè)焦點(diǎn)控件, 并調(diào)用它的requestFocus方法 - 當(dāng)焦點(diǎn)控件的可見性或者focusable屬性發(fā)生變化, 導(dǎo)致該控件不能繼續(xù)持有焦點(diǎn), 那么就會(huì)清除焦點(diǎn), 并重新通過根控件的
requestFocus來分發(fā)焦點(diǎn) - 當(dāng)控件從不能持有焦點(diǎn)變?yōu)榭梢猿钟薪裹c(diǎn), 會(huì)觸發(fā)
ViewParent#focusableViewAvailable, 并在兩種情況下會(huì)替換舊焦點(diǎn)控件. - 當(dāng)焦點(diǎn)控件從布局中移除, 會(huì)重新通過根控件的
requestFocus來分發(fā)焦點(diǎn) - 當(dāng)可以獲取焦點(diǎn)的控件新增進(jìn)布局時(shí), 不會(huì)調(diào)用
ViewParent#focusableViewAvailable, 如果該控件被加入布局前已經(jīng)持有焦點(diǎn), 那么就會(huì)替換舊焦點(diǎn)控件, 否則就不會(huì)觸發(fā)焦點(diǎn)相關(guān)方法.
RecyclerView是一個(gè)非常常用的控件, 其中列表中的子控件會(huì)復(fù)用/移除/新增等, 因此焦點(diǎn)的處理也比較特殊, 下一篇會(huì)詳細(xì)分析RecyclerView的焦點(diǎn)處理邏輯, 以此得到移除焦點(diǎn)控件后重新分發(fā)焦點(diǎn)的解決方案.