一點(diǎn)見解: 焦點(diǎn)那點(diǎn)事(二)

上一篇文章, 一點(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ī)制竟是這樣的?.

概括起來就是

  1. ViewRootImpl通過一個(gè)Receiver接收硬件發(fā)送過來的事件(包括觸摸事件和鍵盤事件)
  2. 然后ViewRootImpl會(huì)把這些事件放在隊(duì)列中
  3. 然后再按順序取出這些事件通過InputStage相關(guān)類分發(fā)出去, 最后會(huì)執(zhí)行InputStage#onProcess()方法
  4. 其中在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è)方法里

  1. 如果這個(gè)ViewGroup持有焦點(diǎn), 那么就會(huì)直接調(diào)用View#dispatchKeyEvent
  2. 如果是它的子控件持有焦點(diǎn), 那么就會(huì)調(diào)用子控件View#dispatchKeyEvent

View#dispatchKeyEvent里面

  1. 詢問OnKeyListener是否消費(fèi)這個(gè)事件
  2. 消費(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í)

  1. 如果當(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è)方法.
  2. 如果不存在焦點(diǎn), 那么就會(huì)調(diào)用ViewRootImpl#focusSearch, 這個(gè)方法直接調(diào)用了FocusFinder#findNextFocus來查找合適的控件
  3. 當(dāng)找到具體的控件后, 就會(huì)調(diào)用該控件的requestFocus方法

這個(gè)過程說明

  1. 按下方向鍵時(shí), 如果沒有控件持有焦點(diǎn), 那么我們不能控制候選控件的選擇
  2. 按下方向鍵時(shí), 如果有控件持有焦點(diǎn), 那么可以通過重寫這個(gè)控件的父控件ViewParent#focusSearch來控制候選控件的選擇
  3. 無論是如何得到候選控件, 這個(gè)控件是通過requestFocus來獲取焦點(diǎn)的, 后續(xù)流程參考一點(diǎn)見解: 焦點(diǎn)那點(diǎn)事(一)

焦點(diǎn)控件失去焦點(diǎn)資格

上一篇文章提到控件要獲取焦點(diǎn)必須符合

  1. View#isFocusable返回true, 如果在觸摸模式, 則View#isFocusableInTouchMode也要返回true
  2. 控件必須可見
  3. 控件相關(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)

  1. 如果當(dāng)前沒有焦點(diǎn)控件, 那么就會(huì)調(diào)用這個(gè)新獲得焦點(diǎn)資格的控件的requestFocus方法
  2. 如果當(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é)

  1. 頁面第一次刷新布局時(shí)會(huì)通過根控件的requestFocus來尋找第一個(gè)焦點(diǎn)控件
  2. 當(dāng)鍵盤輸入方向事件時(shí), 頁面會(huì)通過ViewParent#focusSearch來尋找下一個(gè)焦點(diǎn)控件, 并調(diào)用它的requestFocus方法
  3. 當(dāng)焦點(diǎn)控件的可見性或者focusable屬性發(fā)生變化, 導(dǎo)致該控件不能繼續(xù)持有焦點(diǎn), 那么就會(huì)清除焦點(diǎn), 并重新通過根控件的requestFocus來分發(fā)焦點(diǎn)
  4. 當(dāng)控件從不能持有焦點(diǎn)變?yōu)榭梢猿钟薪裹c(diǎn), 會(huì)觸發(fā)ViewParent#focusableViewAvailable, 并在兩種情況下會(huì)替換舊焦點(diǎn)控件.
  5. 當(dāng)焦點(diǎn)控件從布局中移除, 會(huì)重新通過根控件的requestFocus來分發(fā)焦點(diǎn)
  6. 當(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)的解決方案.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android開發(fā)使用的手機(jī)一般處于觸摸模式, 因此默認(rèn)情況下并不會(huì)有焦點(diǎn), 所以之前一直對(duì)焦點(diǎn)不是很熟悉. 但是...
    AssIstne閱讀 8,388評(píng)論 1 18
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,045評(píng)論 25 709
  • 前言 相信很多剛接觸AndroidTV開發(fā)的開發(fā)者,都會(huì)被各種焦點(diǎn)問題給折磨的不行。不管是學(xué)技術(shù)還是學(xué)習(xí)其他知識(shí),...
    礪雪凝霜閱讀 5,769評(píng)論 15 25
  • 還行吧,畢竟沒怎么學(xué)好c。
    實(shí)在想不出昵稱丶閱讀 200評(píng)論 0 0
  • 三天就走過我的二十年 事出有因,某月某一天在一起逛沃爾瑪,三人竟然深深被自助燒烤工具吸引久久不愿離去,更萌生了眾籌...
    無處留歡喜閱讀 352評(píng)論 0 1

友情鏈接更多精彩內(nèi)容