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