一點見解: 焦點那點事(一)

Android開發使用的手機一般處于觸摸模式, 因此默認情況下并不會有焦點, 所以之前一直對焦點不是很熟悉. 但是在電視端開發上, 焦點的處理可以說直接影響了用戶體驗, 因此借此熟悉下焦點處理的流程.

本文著重介紹焦點相關的一些關鍵方法, 先從局部了解下焦點的一些基礎規則和行為特點.

獲取焦點的前提

  1. View#isFocusable返回true, 如果在觸摸模式, 則View#isFocusableInTouchMode也要返回true
  2. 控件必須可見
  3. 控件相關的父控件, 包括祖父控件等, ViewGroup#getDescendantFocusability()不能為ViewGroup#FOCUS_BLOCK_DESCENDANTS

View

獲取焦點

調用View#requestFocus系列方法

進入View#requestFocusNoSearch

在該方法中會對控件的當前狀態進行判斷, 如果不符合獲取焦點的前提則直接返回false告知調用方, 控件不會獲取焦點

只要符合前提就會繼續執行, 最終必定返回true, 不論當前控件的焦點狀態是否有改變

符合前提則進入 View#handleFocusGainInternal

如果控件已經持有焦點, 則不會做任何事情, 直接結束流程

如果沒有焦點,

  1. 改變焦點標志位, 此時View#isFocused就會返回true
  2. 通過ViewParent#requestChildFocus通知父控件即將獲取焦點
  3. 通知其他部件焦點狀態發生變化(略, 本文不關心)
  4. 觸發OnGlobalFocusChangeListener的回調
  5. 觸發OnFocusChangeListener回調
  6. 重繪, 結束流程

清除焦點

調用View#clearFocus主動放棄焦點

如果控件本身沒有焦點, 則什么都不會發生

如果控件持有焦點

  1. 改變焦點標志位
  2. 通過ViewParent#clearChildFocus通知父控件, 當前控件放棄焦點
  3. 觸發OnFocusChangeListener回調
  4. 調用當前控件的根控件(rootView)的requestFocus方法
  5. 如果步驟4中沒有找到新的焦點控件, 則觸發OnGlobalFocusChangeListener的回調, 注: 如果找到新的焦點控件, 那么新的控件獲取焦點的過程中就會回調OnGlobalFocusChangeListener, 所以這里只有沒找到才進行步驟5

注: 由上流程可以知道, 如果根控件查找控件的時候找到的控件還是這個控件, 那么OnFocusChangeListener就會被調用兩次, 先失去焦點, 然后又獲取到焦點

ViewGroup

焦點分發策略DescendantFocusability

  1. FOCUS_BLOCK_DESCENDANTS: 攔截焦點, 直接自己嘗試獲取焦點
  2. FOCUS_BEFORE_DESCENDANTS: 首先自己嘗試獲取焦點, 如果自己不能獲取焦點, 則嘗試讓子控件獲取焦點
  3. FOCUS_AFTER_DESCENDANTS: 首先嘗試把焦點給子控件, 如果所有子控件都不要, 則自己嘗試獲取焦點

獲取焦點

根據焦點分發策略決定下面兩個方法的調用順序

通過View#requestFocus自己獲取焦點

ViewGroup看作View, 直接走View獲取焦點的流程來獲取焦點

進入onRequestFocusInDescendants

可以傳入方向來改變遍歷的順序, 默認是從0遞增

遍歷子控件, 調用子控件的View#requestFocus來嘗試把焦點給可見的子控件, 某個子控件成功獲取到焦點后, 停止遍歷

注: 重寫該方法可以改變ViewGroup分發焦點給子控件的行為, 例如遍歷順序

清除焦點

如果焦點控件不是它的子控件, 那么直接把當前的ViewGroup看作ViewView#clearFocus流程, 反之則調用焦點控件的View#clearFocus.

注: 區別在于重新分發焦點時的選擇范圍.

ViewParent

ViewParent是一個接口, 表示了一個父控件應該具備的功能, ViewGroup實現了該接口.

與焦點相關的接口有4個

clearChildFocus

當子控件主動放棄焦點的時候會通過這個方法通知父控件.

ViewGroup的默認實現中, 會置空當前焦點控件, 表示該父控件下沒有子控件獲取焦點, 接著把這個事件通知給上級父控件.

注1: 這個方法名有點讓人誤解, 應該把這個方法看作一個回調, 表明了一個狀態, 在這個方法中并沒有做清除焦點的操作, 實際的清除動作是在View#clearFocus中完成的, 這個方法也是在這個流程中被調用的. 而且是在子控件已經放棄焦點后調用.
注2: 區分主動放棄和因為其他控件獲取了焦點而被動丟失焦點的情況

requestChildFocus

當子控件獲取了焦點后, 通過這個方法通知父控件. 同clearChildFocus類似, 應該把這個方法看作是一個回調.

ViewGroup的默認實現中, 因為同時只會有一個焦點, 因此在這里應該把舊焦點清除掉, 大致流程如下

  1. 如果焦點分發策略為FOCUS_BLOCK_DESCENDANTS則什么也不干
  2. 如果父控件自身有焦點, 通過View#unFocus清除焦點
  3. 如果父控件當前已經有焦點控件, 并且和新的控件不一致, 那么通過View#unFocus清除舊焦點控件的焦點
  4. 向上傳遞這個事件

內部清除焦點View#unFocus

這個方法和View#clearFocus相同點在于都會執行View#clearFocusInternal方法, 區別在于unFocus只會執行clearFocus中, 上文清除焦點中提到的1, 3步驟, 因此不會通知父控件, 不會觸犯requestChildFocus回調, 因為這個方法是在子控件被動失去焦點時調用的, 所以也不會觸發焦點分發.

因此新舊焦點切換的大致流程是

  1. 新焦點控件獲取焦點
  2. 新焦點控件通知父控件
  3. 父控件清除舊焦點控件的焦點
  4. 舊焦點控件回調OnFocusChangeListener
  5. 觸發OnGlobalFocusChangeListener的回調
  6. 新焦點控件回調OnFocusChangeListener

focusableViewAvailable

通知父控件, 子控件的狀態發生改變, 從不能獲取焦點, 變成可能可以獲取焦點.

有兩種情況會被調用

  1. 子控件從unFocusable變為focusable
  2. 子控件從不可見變為可見, 即使它不是focusable也會調用, 因此它的子控件可能可以獲取焦點.

ViewGroup中的默認實現只是在符合條件的情況下把這個事件向上傳遞給自己的父控件.

focusSearch(View, int)

查找指定方向中最近的, 想要獲取焦點的控件.

這個方法直接決定了焦點的移動規則, 非常重要.

ViewGroup的默認實現中, 會一直向上傳遞, 直到根控件, 接著調用FocusFinder#findNextFocus方法查找合適的控件. 稍后再分析這個方法.

View中有一個同名的方法focusSearch(int), 該方法直接調用了父控件的focusSearch(View, int)來查找下一個焦點控件

findNextFocus

查找步驟大致如下

手動指定

如果有通過android:nextFocusDown等手動指定控件, 則返回對應方向的控件

動態計算
  1. 獲取所有可以獲取焦點的控件的集合
  2. 計算相對當前焦點控件的坐標
  3. 根據方向選擇合適的控件

總結

  1. 分析的過程要注意區分ViewViewGroup的差異和新焦點和舊焦點控件的方法調用.
  2. ViewParent是一個接口, 其中一些方法應該看作是回調, 子控件通過這些回調通知父控件焦點狀態發生了變化, 提醒父控件進行相關處理, 確保只有一個焦點存在
  3. 某個控件獲取焦點的同時, 舊焦點控件也會失去焦點, 這個動作是在requestChildFocus中發生的.
  4. 焦點移動的關鍵方法是focusSearch(View, int), 下一篇文章一點見解: 焦點那點事(二)接著分析焦點移動的發起點和過程.
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 上一篇文章, 一點見解: 焦點那點事(一), 了解了焦點相關的一些基本知識, 提到焦點切換的關鍵方法ViewPar...
    AssIstne閱讀 2,830評論 7 6
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,841評論 25 708
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,497評論 0 17
  • 相信每個人都有時間過的好快的感覺。平時事務繁忙的人這種感覺會更快一些,每個時間點都安排著事情,他們的感覺是時間太快...
    不萊梅閱讀 258評論 0 2
  • >Linker中主要的兩個源點是dlopen和dlsym。 * dlopen傳入兩個參數,返回一個文件句柄。傳入的...
    sakuradream閱讀 779評論 0 0