Android TV 焦點控制邏輯

1. 首先簡單的焦點控制在對應的佈局控件裏設置以下屬性:

android:nextFocusUp="@id/下一個控件的id"
android:nextFocusDown=""
android:nextFocusLeft=""
android:nextFocusRight=""

分別對應該控件按下↑、↓、←、→鍵對應的下一個控件。java

2.焦點控制邏輯:

翻看各大博客,對與AndroidTV焦點控制的理解都大同小異,接下來是我對與焦點控制的理解:android

2.1Event事件機制:

在哪些對象中進行的:

Activity -> Window -> ViewGroup -> View算法

包含攔截、分發、響應:數組

攔截發生在: onInterceptTouchEvent()方法中,當用戶觸發event事件後,由上層傳入,當此方法返回true時,則被攔截不會繼續往子view傳遞,由當前view的 onTouchEvent()來響該事件app

                                                                                                                                                               返回false時,不會被攔截,事件將繼續傳遞  ,由子view調用當前view的 dispatchTouchEvent()  去分發, 最後由具體的控件去消費此事件。ide

分發:函數

   dispatchEvent(MotionEvent event)  負責事件的調度,不少人稱之爲分發和傳遞也一個意思,主要負責將事件交由哪一個控件去處理,若是本身不想處理,則能夠繼續往下傳遞,想處理則觸發自己view的 ontuchEvent(),
    此方法也返回 boolean類型,返回ture表明傳遞,返回false表明不傳遞,和咱們的 事件攔截偏偏相反,對於初學者來講很容易搞糊塗,本事件Activty,ViewGroup,View都擁有處理權,主要將事件負責轉發,不管交由別人處理仍是本身,其實都在充當調度角色,是事件的核心。
 
響應(消費):
 
  安卓中事件具體處理由  onTouchEvent()  來執行,此階段主要 負責事件的消費響應,經過處理完事件後,而後 逐步向上級彙報,若是消費了上級則不會再進行作響應消費處理,只會繼續返回給根佈局。
 
  此方法返回布爾類型,若是消費了此事件,則會調用上級的此方法,默認返回false作處理,若是返回true,則表明不消費此時間 ,讓上級調用本方法去作處理,逐步網上彙報,直到Activity獲得消息爲止。
 
過程:
 

 

 如圖A:表明activity,B:表明ViewGroup(如:佈局),C:表明View(如:button)佈局

點擊屏幕上的C時整個事件將會由A—B --C —B—A這樣的順序進行分發。this

具體狀況以下:spa

 當點擊C (Button)時,首先有A進行分發,而後傳遞到B,若是B不攔截,則繼續分發,傳遞到C ,此時C沒法繼續傳遞 ,則執行事件,消費後繼續向上反饋,
上級則不會進行消費處理,若是不消費,
則由上級B(Layout)進行處理,若是不處理,則繼續交由A(Activity)處理,此時此事件結束。
 

2.2按鍵事件:

KeyEvent:位於android.view下,KeyEvent主要有如下事件類型:

   KeyEvent.KEYCODE_DPAD_UP; 上
   KeyEvent.KEYCODE_DPAD_DOWN; 下
   KeyEvent.KEYCODE_DPAD_LEFT;左
   KeyEvent.KEYCODE_DPAD_RIGHT;右
   KeyEvent.KEYCODE_DPAD_CENTER;肯定鍵
   KeyEvent.KEYCODE_DPAD_RIGHT; 右
   KeyEvent.KEYCODE_XXX:數字鍵 (xx表示你按了數字幾)
   KeyEvent.KEYCODE_BACK; 返回鍵
   KeyEvent.KEYCODE_HOME;房子鍵

   KeyEvent.KEYCODE_A: A-Z,26個字母

   KeyEvent.KEYCODE_MENU菜單鍵。

 

首先看事件分發圖:

 

 

 

如上圖:

首先,KeyEvent會流轉到ViewRootImpl中開始進行處理,具體方法是內部類ViewPostImeInputStage中的processKeyEvent。

代碼以下:

private int processKeyEvent(QueuedInputEvent q) {
        final KeyEvent event = (KeyEvent)q.mEvent;
        ...
        // Deliver the key to the view hierarchy.
        // 1. 先去執行mView的dispatchKeyEvent
        if (mView.dispatchKeyEvent(event)) {
            return FINISH_HANDLED;
        }
        ...
        // Handle automatic focus changes.
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            int direction = 0;
            ...
            if (direction != 0) {
                View focused = mView.findFocus();
                if (focused != null) {
                    // 2. 以後會經過focusSearch去找下一個焦點視圖
                    View v = focused.focusSearch(direction);
                    if (v != null && v != focused) {
                        ...
                        if (v.requestFocus(direction, mTempRect)) {
                            ...
                            return FINISH_HANDLED;
                        }
                    }

                    // Give the focused view a last chance to handle the dpad key.
                    if (mView.dispatchUnhandledMove(focused, direction)) {
                        return FINISH_HANDLED;
                    }
                } else {
                    // find the best view to give focus to in this non-touch-mode with no-focus
                    // 3. 若是當前原本就沒有焦點視圖,也會經過focusSearch找一個視圖
                    View v = focusSearch(null, direction);
                    if (v != null && v.requestFocus(direction)) {
                        return FINISH_HANDLED;
                    }
                }
            }
        }
        return FORWARD;
    }

看上面的代碼能夠了解:

      先執行mView的dispatchKeyEvent()方法,再經過focusSearch()去找下一個焦點視圖,若是當前沒由焦點視圖也會執行focusSearch()找一個視圖。

2.2.1 dispatchKeyEvent()執行流程

    DecorView →Activity→ViewGroup→view。

DecorView 的 dispatchKeyEvent ():

public boolean dispatchKeyEvent(KeyEvent event) {
    ... ...
    if (!mWindow.isDestroyed()) {
        // Activity實現了Window.Callback接口,具體能夠參考 Activity.java 源碼.
        final Window.Callback cb = mWindow.getCallback();
        // mFeatureId < 0,表示爲 application 的 DecorView.
        // cb.dispatchKeyEven 調用的是 Activity 的 dispatchKeyEven.
        final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
                : super.dispatchKeyEvent(event);
        // 是否消耗掉事件.
        if (handled) {
            return true;
        }
    }
    return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
            : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
}

這裏將會調用Activty的dispatchKeyEvent();

Activity 的 dispatchKeyEvent ():

// 補充知識點:
// 這就是爲什麼在 Activity 直接 return true,事件被消耗,就不執行焦點搜索等等操做了.
// 因此這裏也是能夠作 焦點控制的,最好是在 event.getAction() == KeyEvent.ACTION_DOWN 進行.
// 由於android 的 ViewRootlmpl 的 processKeyEvent 焦點搜索與請求的地方 進行了判斷
// if (event.getAction() == KeyEvent.ACTION_DOWN)

public boolean dispatchKeyEvent(KeyEvent event) {
        ... ...
        Window win = getWindow();
        // 調用 PhoneWindow 的 superDispatchKeyEvent
        // 裏面又調用 mDecor.superDispatchKeyEvent(event)
        // mDecor 爲 DecorView.
        if (win.superDispatchKeyEvent(event)) {
            return true;
        }
        View decor = mDecor;
        if (decor == null) decor = win.getDecorView();
        // onKeyDown,onKeyUp,onKeyLongPress 等等回調的處理.
        // 只有 onKeyDown return true 能夠進行焦點控制,
        // 由於android 的 ViewRootlmpl 的 processKeyEvent 焦點搜索與請求的地方 進行了判斷
        // if (event.getAction() == KeyEvent.ACTION_DOWN)
        return event.dispatch(this, decor != null
                ? decor.getKeyDispatcherState() : null, this);
    }

 

 

ViewGroup的dispatchKeyEvent():

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
    ...
    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        // 1.1 以View的身份處理KeyEvent
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS) {
        // 1.2 以ViewGroup的身份把KeyEvent交給mFocused處理
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }
    ...
    return false;
}

經過flag的判斷,有兩個處理路徑,也能夠看到在處理keyEvent時,ViewGroup扮演兩個角色:

       View的角色,也就是此時keyEvent須要在本身與其餘View之間流轉。:調用自身的dispathKeyEvent()。

       ViewGroup的角色,此時keyEvent須要在本身的子View之間流轉 。:調用當前焦點子View的dispatchKeyEvent()。

再來看看view的dispatchKeyEvent():

public boolean dispatchKeyEvent(KeyEvent event) {
    ...
    ListenerInfo li = mListenerInfo;
    // 1.3 若是設置了mOnKeyListener,則優先走onKey方法
    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }
    // 1.4 把View本身看成參數傳入,調用KeyEvent的dispatch方法
    if (event.dispatch(this, mAttachInfo != null
            ? mAttachInfo.mKeyDispatchState : null, this)) {
        return true;
    }
    ...
    return false;
}

View這裏,會優先處理OnKeyListener的onKey回調。而後纔可能會走KeyEvent的dispatch,最終走到View的OnKeyDown或者OnKeyUp。

大致的流轉順序總結以下圖:

 

 其中任何一步均可以經過return true的方式來消費掉這個KeyEvent,結束這個分發過程。

 

 按鍵事件分發結束,接下來讓咱們看看如和查找焦點。

3.焦點查找方法。

若是dispatchKeyEvent沒有消耗掉KeyEvent,會由系統來處理焦點移動。

經過view的focusSearch方法找到下一個獲取焦點的View,而後調用requestFocus設置焦點。

 

3.1:focusSearch()

// View.java
public View focusSearch(@FocusRealDirection int direction) {
    if (mParent != null) {
        return mParent.focusSearch(this, direction);
    } else {
        return null;
    }
}

由上面的代碼能夠看出,View不會直接去查找,而是會交給其parent的focusSearch方法去查找,也就是ViewGroup的focusSearch()方法去查找。

 ViewGroup的focusSearch()方法:
// ViewGroup.java
public View focusSearch(View focused, int direction) {
    if (isRootNamespace()) {
        // root namespace means we should consider ourselves the top of the
        // tree for focus searching; otherwise we could be focus searching
        // into other tabs.  see LocalActivityManager and TabHost for more info
        return FocusFinder.getInstance().findNextFocus(this, focused, direction);
    } else if (mParent != null) {
        return mParent.focusSearch(focused, direction);
    }
    return null;
}

 

這裏會判斷是否爲根佈局,也就是頂層佈局,若是是則最後交給FocusFinder去查找

 若是不是則會接調用上層parent的focusSearch()。

 

isRootNamespace的()

/**
 * {@hide}
 *
 * @param isRoot true if the view belongs to the root namespace, false
 *        otherwise
 */
public void setIsRootNamespace(boolean isRoot) {
    if (isRoot) {
        mPrivateFlags |= PFLAG_IS_ROOT_NAMESPACE;
    } else {
        mPrivateFlags &= ~PFLAG_IS_ROOT_NAMESPACE;
    }
}

3.2:findNextFocus():

位於頂層的ViewGroup把本身和當前焦點(View)以及方向傳入。

findNextFocus()代碼:

// FocusFinder.java

public final View findNextFocus(ViewGroup root, View focused, int direction) {
    return findNextFocus(root, focused, null, direction);
}

private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
    View next = null;
    if (focused != null) {
        // 2.1 優先從xml或者代碼中指定focusid的View中找
        next = findNextUserSpecifiedFocus(root, focused, direction);
    }
    if (next != null) {
        return next;
    }
    ArrayList<View> focusables = mTempList;
    try {
        focusables.clear();
        root.addFocusables(focusables, direction);
        if (!focusables.isEmpty()) {
            // 2.2 其次,根據算法去找,原理就是找在方向上最近的View
            next = findNextFocus(root, focused, focusedRect, direction, focusables);
        }
    } finally {
        focusables.clear();
    }
    return next;
}

這裏root是上面isRootNamespace()爲true的ViewGroup,focused是當前焦點視圖

優先找開發者指定的下一個focus的視圖 ,就是在xml或者代碼中指定NextFocusDirection Id的視圖。

其次,根據算法去找,原理就是找在方向上最近的視圖。

 

4.按鍵焦點查找流程

4.1界面第一次進入的時候,是如何獲取到焦點的

先看下DecoreView的流程圖:

 

 

 上圖ViewRootImpl類中 performTraversals方法:

.. ...
if (mFirst) {
    if (mView != null) {
        if (!mView.hasFocus()) {
            // 調用 View 的 requestFocus(int direction)
 mView.requestFocus(View.FOCUS_FORWARD);
        }
        ... ...
    }
... ...

總體的過程:

 

 

 

ViewRootlmpl.performTraversals→DecoreView.requestFocus→ActionBarOverlayLayout.requestFocus→FrameLayout(android:id/content).requestFocus→FrameLayout(activity_test.xml).requestFocus→Button1(activity_test.xml).requestFocus

代碼步驟:

View.java
public final boolean requestFocus(int direction) {
    // 由於 DecoreView 繼承 ViewGroup
    // ViewGroup 重寫了此函數,
    // 會調用 ViewGroup 的 requestFocus(int direction, Rect previouslyFocusedRect)
    return requestFocus(direction, null);
}

ViewGroup.java public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    // 關注內容:
    // 處理 DescendantFocusabilit
    // 1)FOCUS_AFTER_DESCENDANTS 先分發給Child View進行處理,若是全部的Child View都沒有處理,則本身再處理
    // 2)FOCUS_BEFORE_DESCENDANTS ViewGroup先對焦點進行處理,若是沒有處理則分發給child View進行處理
    // 3)FOCUS_BLOCK_DESCENDANTS ViewGroup自己進行處理,不論是否處理成功,都不會分發給ChildView進行處理
    // setDescendantFocusability 能夠設置.
    int descendantFocusability = getDescendantFocusability();
    switch (descendantFocusability) {
        case FOCUS_BLOCK_DESCENDANTS:
            return super.requestFocus(direction, previouslyFocusedRect);
        case FOCUS_BEFORE_DESCENDANTS: { 
            // 其它的 ActionBarOverlayLayout,Content等繼承ViewGroup
            // 默認進入 FOCUS_BEFORE_DESCENDANTS,由於 ViewGroup 初始化的時候設置了
            // setDescendantFocusability(FOCUS_BEFORE_DESCENDANTS);
            
            // mViewFlags 判斷 FOCUSABLE_MASK,FOCUSABLE_IN_TOUCH_MODE.
            // Button 以上的父佈局,不知足以上條件判斷,所有都是 直接 return false.
            final boolean took = super.requestFocus(
            direction, previouslyFocusedRect);
            // took=false, 調用 onRequestFocusInDescendants 遍歷子控件進行請求
            return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
        }
        case FOCUS_AFTER_DESCENDANTS: { 
            // DecoreView 進入這裏,由於 PhoneWindow 給 DecoreView 初始化 設置
            // setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            // setIsRootNamespace(true);
            // 像 RecyclerView, Leanback 也會進入這裏.
            // 遍歷子控件進行請求
            final boolean took = onRequestFocusInDescendants(
            direction, previouslyFocusedRect);
            // took=true,子控件有焦點,不調用 super.request...,反之.
            return took ? took : super.requestFocus(
            direction, previouslyFocusedRect);
        }
        ... ...
    }
}

View.java public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
        return requestFocusNoSearch(direction, previouslyFocusedRect);
}

ViewGroup.java
// 補充知識點: onRequestFocusInDescendants 是能夠作焦點記憶控制的.
protected boolean onRequestFocusInDescendants(int direction, 
Rect previouslyFocusedRect) {
    .. ...
    for (int i = index; i != end; i += increment) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
            // 
            if (child.requestFocus(direction, previouslyFocusedRect)) {
                return true;
            }
        }
    }
    return false;
}

Button1獲取焦點:

 

 

 關鍵代碼是 View.java 的函數 handleFocusGainInternal : mPrivateFlags |= PFLAG_FOCUSED 和 mParent.requestChildFocus(this, this)

View.java
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
    // need to be focusable
    // Button 默認 android:focusable="true"
    // button1 以上的父佈局都沒有設置此類屬性,進入這裏,直接就 return false.
    if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
            (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
        return false;
    }

    // need to be focusable in touch mode if in touch mode
    // 當 button1 沒有設置 android:focusableInTouchMode="true" 的時候,
    // 直接 return false,那麼界面上是沒有任何控件獲取到焦點的.
    // 鼠標|觸摸支持的屬性.
    if (isInTouchMode() &&
        (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
           return false;
    }

    // need to not have any parents blocking us
    if (hasAncestorThatBlocksDescendantFocus()) {
        return false;
    }
    // 關鍵函數
    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;
}

void handleFocusGainInternal(@FocusRealDirection int direction, 
Rect previouslyFocusedRect) {
    if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
        // 關鍵代碼,設置 有焦點的標誌位. 
        // 這個時候 button1 已經標誌上焦點
        mPrivateFlags |= PFLAG_FOCUSED;
        // 獲取父佈局的老焦點.
        View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;
        // 調用此函數,告訴上一層父佈局,讓它作一些事情.
        if (mParent != null) {
            mParent.requestChildFocus(this, this);
        }
        // 此函數是全局焦點監聽的回調.
        // 調用方式: View.getViewTreeObserver().addOnGlobalFocusChangeListener
        if (mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
        }
        // 回調處理.
        onFocusChanged(true, direction, previouslyFocusedRect);
        // 刷新按鍵的 selector drawable state狀態
        refreshDrawableState();
    }
}

ViewGroup.java
public void requestChildFocus(View child, View focused) {
    if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
        return;
    }

    // Unfocus us, if necessary
    super.unFocus(focused);

    // We had a previous notion of who had focus. Clear it.
    if (mFocused != child) {
        if (mFocused != null) {
            mFocused.unFocus(focused);
        }
        // 保存上一級的焦點view.
        mFocused = child;
    }
    // 一層層調用回去父佈局,至關於 
    // FrameLayout(activity_test.xml) 的 mFocused 是 Button1.
    // FrameLayout(android:id/content) 的 mFocused 是 FrameLayout(activity_test.xml)
    // ActionBarOverlayLayout 的 mFocused 是 FrameLayout(android:id/content)
    // 最後 DecoreView 的 mFocused 是 ActionBarOverlayLayout
    // 在最後的後面,ViewRootImpl 會調用 
    // requestChildFocus,又會再次調用 
    // performTraversals刷新界面.(再執行 layout, draw)
    // 造成了一個關聯, dispatchKeyEvent 的 mFocused 也在使用.
    if (mParent != null) {
        mParent.requestChildFocus(this, focused);
    }
 }

// ViewRootImpl.java
@Override
public void requestChildFocus(View child, View focused) {
    checkThread();
    scheduleTraversals();
}

初步獲取焦點已經瞭解,接下來看看焦點是如何從 view2 →view2的。

4.2按鍵焦點的搜索過程

 

 

 focusView(2) 按下右鍵後:由上面的3.焦點查找方法能夠得出下圖:

 

 

在沒有消耗 dispatchKeyEvent的狀況下: 

FocusSearch 一層層上去,調用 FocusFinder.getInstance().findNextFocus… … 後,在 …addFocusables 下,將全部帶焦點屬性的 view 所有加到數組裏面去,而後通用方向,位置等查找相近的view. 最後找到的是  focusView(3).

 

private int processKeyEvent(QueuedInputEvent q) {
    ... ...
    // 以上代碼不消耗事件.
    // 判斷 action 爲 ACTION_DOWN 才處理焦點搜索以及請求.
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
    // 根據按鍵判斷,設置 direction 屬性.
    if (direction != 0) {
        // 一層層查找(根據mFocused),最後獲取到 button1.
        View focused = mView.findFocus();
        if (focused != null) {
            // button1_view 調用 focusSearch(), 右鍵,direction=66
            View v = focused.focusSearch(direction);
            // 最終返回 v = button2
            if (v != null && v != focused) {
                // do the math the get the interesting rect
                // of previous focused into the coord system of
                // newly focused view
                focused.getFocusedRect(mTempRect);
                if (mView instanceof ViewGroup) {
                    ((ViewGroup) mView).offsetDescendantRectToMyCoords(
                            focused, mTempRect);
                    ((ViewGroup) mView).offsetRectIntoDescendantCoords(
                            v, mTempRect);
                }
                // button2 View 調用 requestFocus
                // 這裏的過程 和 第一次獲取焦點button1請求是同樣的.
                if (v.requestFocus(direction, mTempRect)) {
                    // 播放音效
                    playSoundEffect(SoundEffectConstants
                            .getContantForFocusDirection(direction));
                    return FINISH_HANDLED;
                }
            }
            // 進行最後的垂死掙扎,
            // 這裏其實能夠處理一些焦點問題或者滾動翻頁問題.
            // 滾動翻頁的demo能夠參考 原生 Launcher 的 Workspace.java
            // Give the focused view a last chance to handle the dpad key.
            if (mView.dispatchUnhandledMove(focused, direction)) {
                return FINISH_HANDLED;
            }
        } else {
            // 這裏處理第一次無焦點 view 的狀況.
            // 基本上和有焦點view 的狀況差很少.
            View v = focusSearch(null, direction);
            if (v != null && v.requestFocus(direction)) {
                return FINISH_HANDLED;
            }
        }
    }
    }
    ... ...
}

button1下一個焦點搜索流程圖:

 

 

View v = focused.focusSearch(direction); # focused=>button1 direction=>66
Button1_View→focusSearch(int direction)→FrameLayout(activity_test.xml)_ViewGroup→focusSearch(View focused, int direction)→。。。→FrameLayout(activity_test.xml)_ViewGroup→

focusSearch(View focused, int direction)→DecoreView_ViewGroup→FocusFinder.getInstance().findNextFocus(this, focused, direction)→FocusFinder.findNextFocus()→ViewGroup.addFocusables()->。。。



代碼流程:

View.java
public View focusSearch(@FocusRealDirection int direction) {
    if (mParent != null) {
        // button1 的父佈局ViewGroup調用 focusSearch
        return mParent.focusSearch(this, direction);
    } else {
        return null;
    }
}

ViewGroup.java
// 像 RecyclerView 會重寫 focusSearch 進行焦點搜索.
// 也是調用的 FocusFinder.getInstance().findNextFocus
// leanback 的 GridLayoutmanger 也重寫了 onAddFocusables.
public View focusSearch(View focused, int direction) {
    // 只有 DecoreView 設置了 setIsRootNamespace
    // 最終由 DecoreView 進入這裏.
    if (isRootNamespace()) {
        // 傳入參數(this: DecoreView focused: button1 direction: 66)
        return FocusFinder.getInstance().findNextFocus(this, focused, direction);
    } else if (mParent != null) {
        return mParent.focusSearch(focused, direction);
    }
    return null;
}

FocusFinder.java
findNextFocus(ViewGroup root, View focused, int direction)->findNextFocus(root, focused, null, direction)->
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) {
    View next = null;
    if (focused != null) {
        // 關於XML佈局中的 android:nextFocusRight 等等的查找.
        next = findNextUserSpecifiedFocus(root, focused, direction);
    }
    if (next != null) {
        return next;
    }
    ArrayList<View> focusables = mTempList;
    try {
        focusables.clear();
        // 要進行 findNextFocus,關鍵在於 addFocusables,一層層調用下去.
        // DecorView_View.addFocusables
        // DecorView_ViewGroup.addFocusables
        // ActionBarOverlayLayout_ViewGroup.addFocusables
        // FrameLayout(android:id/content)_ViewGroup.addFocusables
        // FrameLayout(activity_test.xml)_ViewGroup.addFocusables
        // 到最後 button1, button2 添加到 views 數組中,也就是 focusables .
        root.addFocusables(focusables, direction);
        if (!focusables.isEmpty()) {
             // 關鍵函數 findNextFocus,想深刻了解是如何查找到下一個焦點的,
            // 能夠去看看源碼,這裏不進行過多篇幅的講解.
            // focusables 數組有 button1, button2
            // 內部調用 findNextFocusInAbsoluteDirection,這裏進行了一些判斷,查找某個方向比較近的view.
            next = findNextFocus(root, focused, focusedRect, direction, focusables);
        }
    } finally {
        focusables.clear();
    }
    return next;
}

ViewGroup.java
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
    final int focusableCount = views.size();
    final int descendantFocusability = getDescendantFocusability();
    ... ...
        for (int i = 0; i < count; i++) {
            final View child = children[i];
            // 循環 child view 調用 addFocusables,一層層調用下去,將知足條件的添加進 views 數組.
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                child.addFocusables(views, direction, focusableMode);
            }
        }
    }
    if ... ...
        // 調用 view 的 addFocusables,父佈局是不知足條件的,直接返回了.
        super.addFocusables(views, direction, focusableMode);
    }
}

View.java
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
    if (views == null) {
        return;
    }
    if (!isFocusable()) {
        return;
    }
    if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
            && isInTouchMode() && !isFocusableInTouchMode()) {
        return;
    }
    // button1 以上條件知足,加入views數組.
    // button2 以上條件也知足,加入views數組.
    // 同理,焦點記憶的原理就很簡單了,後續會講解.
    views.add(this);
}
相關文章
相關標籤/搜索