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

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)的前提

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

  1. 改變焦點(diǎn)標(biāo)志位, 此時(shí)View#isFocused就會(huì)返回true
  2. 通過(guò)ViewParent#requestChildFocus通知父控件即將獲取焦點(diǎn)
  3. 通知其他部件焦點(diǎn)狀態(tài)發(fā)生變化(略, 本文不關(guān)心)
  4. 觸發(fā)OnGlobalFocusChangeListener的回調(diào)
  5. 觸發(fā)OnFocusChangeListener回調(diào)
  6. 重繪, 結(jié)束流程

清除焦點(diǎn)

調(diào)用View#clearFocus主動(dòng)放棄焦點(diǎn)

如果控件本身沒(méi)有焦點(diǎn), 則什么都不會(huì)發(fā)生

如果控件持有焦點(diǎn)

  1. 改變焦點(diǎn)標(biāo)志位
  2. 通過(guò)ViewParent#clearChildFocus通知父控件, 當(dāng)前控件放棄焦點(diǎn)
  3. 觸發(fā)OnFocusChangeListener回調(diào)
  4. 調(diào)用當(dāng)前控件的根控件(rootView)的requestFocus方法
  5. 如果步驟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

  1. FOCUS_BLOCK_DESCENDANTS: 攔截焦點(diǎn), 直接自己嘗試獲取焦點(diǎn)
  2. FOCUS_BEFORE_DESCENDANTS: 首先自己嘗試獲取焦點(diǎn), 如果自己不能獲取焦點(diǎn), 則嘗試讓子控件獲取焦點(diǎn)
  3. 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看作ViewView#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)清除掉, 大致流程如下

  1. 如果焦點(diǎn)分發(fā)策略為FOCUS_BLOCK_DESCENDANTS則什么也不干
  2. 如果父控件自身有焦點(diǎn), 通過(guò)View#unFocus清除焦點(diǎn)
  3. 如果父控件當(dāng)前已經(jīng)有焦點(diǎn)控件, 并且和新的控件不一致, 那么通過(guò)View#unFocus清除舊焦點(diǎn)控件的焦點(diǎn)
  4. 向上傳遞這個(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)切換的大致流程是

  1. 新焦點(diǎn)控件獲取焦點(diǎn)
  2. 新焦點(diǎn)控件通知父控件
  3. 父控件清除舊焦點(diǎn)控件的焦點(diǎn)
  4. 舊焦點(diǎn)控件回調(diào)OnFocusChangeListener
  5. 觸發(fā)OnGlobalFocusChangeListener的回調(diào)
  6. 新焦點(diǎn)控件回調(diào)OnFocusChangeListener

focusableViewAvailable

通知父控件, 子控件的狀態(tài)發(fā)生改變, 從不能獲取焦點(diǎn), 變成可能可以獲取焦點(diǎn).

有兩種情況會(huì)被調(diào)用

  1. 子控件從unFocusable變?yōu)閒ocusable
  2. 子控件從不可見(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ì)算
  1. 獲取所有可以獲取焦點(diǎn)的控件的集合
  2. 計(jì)算相對(duì)當(dāng)前焦點(diǎn)控件的坐標(biāo)
  3. 根據(jù)方向選擇合適的控件

總結(jié)

  1. 分析的過(guò)程要注意區(qū)分ViewViewGroup的差異和新焦點(diǎn)和舊焦點(diǎn)控件的方法調(diào)用.
  2. ViewParent是一個(gè)接口, 其中一些方法應(yīng)該看作是回調(diào), 子控件通過(guò)這些回調(diào)通知父控件焦點(diǎn)狀態(tài)發(fā)生了變化, 提醒父控件進(jìn)行相關(guān)處理, 確保只有一個(gè)焦點(diǎn)存在
  3. 某個(gè)控件獲取焦點(diǎn)的同時(shí), 舊焦點(diǎn)控件也會(huì)失去焦點(diǎn), 這個(gè)動(dòng)作是在requestChildFocus中發(fā)生的.
  4. 焦點(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ò)程.
最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 上一篇文章, 一點(diǎn)見(jiàn)解: 焦點(diǎn)那點(diǎn)事(一), 了解了焦點(diǎn)相關(guān)的一些基本知識(shí), 提到焦點(diǎn)切換的關(guān)鍵方法ViewPar...
    AssIstne閱讀 2,906評(píng)論 7 6
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,716評(píng)論 25 709
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程,因...
    小菜c閱讀 7,294評(píng)論 0 17
  • 相信每個(gè)人都有時(shí)間過(guò)的好快的感覺(jué)。平時(shí)事務(wù)繁忙的人這種感覺(jué)會(huì)更快一些,每個(gè)時(shí)間點(diǎn)都安排著事情,他們的感覺(jué)是時(shí)間太快...
    不萊梅閱讀 277評(píng)論 0 2
  • >Linker中主要的兩個(gè)源點(diǎn)是dlopen和dlsym。 * dlopen傳入兩個(gè)參數(shù),返回一個(gè)文件句柄。傳入的...
    sakuradream閱讀 824評(píng)論 0 0

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