Android TV開發-按鍵焦點了解與應用

下面將圍繞下面幾點展開講解按鍵焦點:

1. 寫在前面

歡迎大家入坑.
大家好,我是冰雪情緣,已經在 Android TV開發爬坑多年,也是一名TV開發開源愛好者.

Android TV 開源社區 https://gitee.com/kumei/Android_tv_libs
Android TV 開源論壇 www.androidtvdev.com
Android TV 文章專題 https://www.jianshu.com/c/3f0ab61a1322
Android TV QQ羣1:522186932 QQ羣2:468357191

工欲善其事必先利其器,瞭解按鍵的流程,焦點的搜索,請求過程等等
對於我們在開發中遇到的問題,可以去思考以及解決一些刺手問題.
下面我將和分享下按鍵焦點過程以及它的一些實際應用場景.
如果不喜歡看分析原理的小夥伴,可以直接去看3~5.
由於本人經驗有限,有問題還請大家多多指教,互相討論.

參考的 源碼 android-24,圖片截圖爲小米電視.

2. 按鍵焦點過程瞭解

假設Activity 的界面佈局如下(activity_test.xml):

<FrameLayout ... ...>
    <Button1 
        android:focusableInTouchMode="true"
        ... .../> 
    <Button2 ... .../>
</FrameLayout>

帶着3個問題,圍繞 activity_test.xml 和大家一起學習討論:

  • 按鍵的事件流程是如何跑的?
  • 第一次進入界面,button 是如何獲取焦點的?
  • 按鍵的時候,button的下一個焦點是如何查找,並且請求的?

2.1 dispatchKeyEvent 過程瞭解

如果想深入瞭解 按鍵過程的,建議看看 《Android內核剖析》,這裏不過多進行大篇幅的講解.

圖1:
在這裏插入圖片描述

ViewRootlmpl 的 processKeyEvent 函數:

private int processKeyEvent(QueuedInputEvent q) {
    .. ...
    // 1. dispatchKeyEvent 處理.
    // 返回 true,事件消耗,不往下執行焦點搜索與請求,返回 false,繼續往下執行.
    // mView 是 DecorView,DecorView爲整個Window界面的最頂層View, 
    // 繼承FrameLayout,它包含了 ActionBarOv...,還有 content... ...
    // 想深入瞭解 DecorView,具體可以搜索瞭解下.
    if (mView.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    // 2. 下面焦點搜索以及請求的代碼,後面會講解
    ... ...
    if (direction != 0) {
        View focused = mView.findFocus();
        if (focused != null) {
            View v = focused.focusSearch(direction);
    ... ...
}

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);
}

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);
    }

DecorView 的 superDispatchKeyEvent 函數:

public boolean superDispatchKeyEvent(KeyEvent event) {
    ... ...
    // DecorView 繼承的 FrameLayout
    // 調用的是 ViewGroup.dispatchKeyEvent
    return super.dispatchKeyEvent(event);
}

ViewGroup 的 dispatchKeyEvent 函數:

Override
public boolean dispatchKeyEvent(KeyEvent event) {
    ... ...
    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        // 調用 view.dispatchKeyEvent
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS) {
        // 調用 mFocused 的 dispatchKeyEvent,參考 圖2
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }
    ... ...
    return false;
}

mFocused 爲空的時候,最後 按照 圖1 調用的過程返回(view排除).
如果 mFocused 不爲空的時候,假設 Button 已經獲取焦點
流程參考 圖1(ViewRootlmpl->DecorView->PhoneWindow…) + 圖2.

圖2:
在這裏插入圖片描述

Button 最後也調用了 View 的 dispatchKeyEvent:

public boolean dispatchKeyEvent(KeyEvent event) {
    ... ...
    // onKey 的回調,如果這裏也沒有消耗事件,繼續往下面執行.
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }
    // 主要是處理一些回調,比如 onKeyDown,onKeyLongPress,onKeyUp等等,具體看代碼.
    // 沒有消耗事件繼續往下執行.
    if (event.dispatch(this, mAttachInfo != null
            ? mAttachInfo.mKeyDispatchState : null, this)) {
        return true;
    }
    ... ...
    return false;
}

圖2中,mFocused 不爲空的情況下,dispatchKeyEvent 一層層的調用下去(因爲每一層的ViewGroup都保存了mFocused的,請求焦點會講解),
如果這個過程沒有事件消耗,直到盡頭 Button_View return false,
最後一層層的返回上去(圖1 + 圖2 如何一層層調用下來的,那就如何原路返回),
直到回到 ViewRootlmpl 的 processKeyEvent 函數 上次執行的地方,
接下去執行,後面繼續走 就是 焦點查找與請求相關的代碼.

2.2 焦點查找請求過程瞭解

2.2.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
基本上,ActionBarOverlayLayout 和 FrameLayout(andorid:id/content),FrameLayout(activity_test.xml) 基本步驟是一致的.

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();
}

第一次請求的過程基本告一個段落,下面將分析遙控器按鍵後,
焦點是如何搜索並且請求的.

2.2.2 按鍵焦點

大概簡單的講解下按鍵焦點的搜索過程
在這裏插入圖片描述
當 focusView(2) 按下右鍵後,它經歷了什麼鬼?
在這裏插入圖片描述
focusSearch 一層層上去,調用 FocusFinder.getInstance().findNextFocus… … 後,在 …addFocusables 下,將所有 帶 焦點屬性的 view 全部加到數組裏面去,然後通用 方向,位置等 查找相近的view. 最後找到的是 3.

繼續我們上次的DEMO講解

按鍵按下後,上面講解的過程 沒有消耗 dispatchKeyEvent,
那麼就到了 KeyEvent.ACTION_DOWN 按鍵根據方向查找以及請求焦點view.
#假設 direction = 66,右鍵

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(android:id/content)_ViewGroup->focusSearch(View focused, int direction)
… …
DecoreView_ViewGroup->FocusFinder.getInstance().findNextFocus(this, focused, direction)

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);
}

最後 button2 請求焦點 的過程 與 button1最後的掙扎 是一致的.
總結,button1,右鍵,焦點搜索 focusSearch,
根佈局 調用 FocusFinder.getInstance().findNextFocus,
然後父佈局調用 addFocusables 將 button1, button2 添加到 views 數組,
最後根據 button1 的按鍵方向,搜索最近的 button2,最後button2請求焦點.

3. 焦點控制

在xml中,可以使用 nextFocusRight, …Left,…Top,…B 等等來簡單控制界面的某些元素.
還有幾個焦點控制的函數,也可以配合 FocusFinder.getInstance().findNextFocus 一起使用.
在這裏插入圖片描述

dispatchKeyEvent,可以在 Activity,繼承 View的所有控件可以使用,下面搞個小demo.

@Override
public boolean dispatchKeyEvent(KeyEvent event) {
	if (event.getAction() == KeyEvent.ACTION_DOWN) {
		// 因爲 return true,前面講過,事件會被消耗. 
		// 界面上的 焦點搜索與請求 不繼續執行了,執行我們下面的代碼.
		// 1. 強制請求
		findViewById(R.id.xxxx).requestFocus(); 
		// 2. 配合 FocusFinder.getInstance().findNextFocus
		ViewGroup contentView = findViewById(android.R.id.content);
		View focusView = FocusFinder.getInstance().findNextFocus(contentView, contentView.findFocus(), View.FOCUS_RIGHT);
		if (null != focusView) {
      		focusView.requestFocus();
		}
		return true;
	}
	return super.dispatchKeyEvent(event);
}

上面舉了個小demo,onKeyDown, okKey 就不在這裏過多描述了,可以自己寫寫demo實驗下.

這裏重點介紹下 focusSearch 這個函數,前面分析過 ,最終 focusSearch 會返回搜索到的焦點view.
這裏舉一個 focusSearch 控制的焦點的例子:

@Override
public View focusSearch(View focused, int direction) {
	View focusView = super.focusSearch(focused, direction);
    // 這裏判斷是否爲標題欄的view,不是就強制返回標題欄的view,
    // 最後getTitleView會進行requestFocus...,前面有focusSearch的分析過程.
	if (getTitleView() != null && focused != getTitleView() &&
                    direction == View.FOCUS_UP) {
		return getTitleView();
	}      
	return focusView;
}

4. 焦點記憶

設置 需要焦點記憶 並且繼承 viewGroup 的控件 2~3 個屬性
android:descendantFocusability=“afterDescendants” // FOCUS_AFTER_DESCENDANTS
android:focusable=“true”
android:focusableInTouchMode=“true” // 可選
關鍵函數 addFocusables,onRequestFocusInDescendants
因爲前面我們分析過焦點搜索以及請求過程,瞭解到:
前一個焦點view focusSearch,然後 findNextFocus 裏面調用 addFocusables 添加 相關控件,用於搜索最近的焦點.
如果搜索到的是 LinearLayout ,那麼將調用 ViewGroup_requestFocus,因爲是 FOCUS_AFTER_DESCENDANTS 屬性.
首先調用的是 onRequestFocusInDescendants,上一次的保存的焦點view 再次喚醒,請求一次,搞定,大概的邏輯就是這樣.

這裏用 LinearLayout 寫了一個小小的demo,關於焦點記憶的:

@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
    if (mFocudView != null) {
        boolean result = mFocudView.requestFocus(direction, previouslyFocusedRect);
        return result;
    }
    return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}

View mFocudView;

@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
    if (hasFocus()) {
        mFocudView = getFocusedChild();
    } else {
        if (isFocusable()) {
            views.add(this);
            return;
        }
    }
    super.addFocusables(views, direction, focusableMode);
}

前幾天一個朋友問我,但是不想用Leanback的焦點記憶這麼辦?
我給他提供了兩種解決方案:
第一種:使用Leanback的 setFocusScrollStrategy
在這裏插入圖片描述
第二種:如果你想保持中間滾動,又不想用焦點記錄,這個就要改源碼:

BaseGridView.onRequestFocusInDescendants
@Override
public boolean onRequestFocusInDescendants(
int direction, Rect previouslyFocusedRect) {
	return mLayoutManager.gridOnRequestFocusInDescendants(this, direction, previouslyFocusedRect);
}

Leanback的GridLayoutManger.gridOnRequestFocusInDescendants -> gridOnRequestFocusInDescendantsAligned

boolean gridOnRequestFocusInDescendants(
RecyclerView recyclerView, int direction, Rect previouslyFocusedRect) {
        switch (mFocusScrollStrategy) {
        case BaseGridView.FOCUS_SCROLL_ALIGNED:
        default:
        	// 下面這行代碼去掉
            return gridOnRequestFocusInDescendantsAligned(recyclerView,
                    direction, previouslyFocusedRect);
        case BaseGridView.FOCUS_SCROLL_PAGE:
        case BaseGridView.FOCUS_SCROLL_ITEM:
            return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
                    direction, previouslyFocusedRect);
        }
}
    
private boolean gridOnRequestFocusInDescendantsAligned(
RecyclerView recyclerView, int direction, Rect previouslyFocusedRect) {
	View view = findViewByPosition(mFocusPosition);
	if (view != null) {
		boolean result = view.requestFocus(direction, previouslyFocusedRect);
        return result;
    }
	return false;
}

至於爲何Leanback如何做焦點記憶的,建議看看 GridLayoutManger.java.

@Override
public boolean onAddFocusables(
RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) {
	... ...
	if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
		... ...
    	if (recyclerView.isFocusable()) {
        	views.add(recyclerView);
    	}
    }
    return true;
}

5. 應用場景

多級菜單,上下,左右等等結構,等等 焦點錯亂,焦點需要控制.

場景一:Leanback 或者 RecyclerView,到達右邊邊緣的時候,按右鍵,焦點需要跑到下面的一行焦點上.
在這裏插入圖片描述

ExecutorService mExecutorService = Executors.newFixedThreadPool(2);
@Override
public View focusSearch(View focused, int direction) {
	View focusView = super.focusSearch(focused, direction);
	if ((focusView== null) && (direction == View.FOCUS_RIGHT)) {
    	// 到達最右邊,焦點下移.(注意:建議放到Executors的Runnable裏面執行哈,這裏簡化代碼)
    	new Instrumentation().sendKeyDownUpSync(KEYCODE_DPAD_DOWN);
    } else if ((focusView == null) && (direction == View.FOCUS_LEFT)) {
    	// 到達最左邊,焦點下移.
        new Instrumentation().sendKeyDownUpSync(KeyEvent.KEYCODE_DPAD_UP);
    }
	return focusView;
}

場景二:上下結構的標題欄與內容
對面下面圖片的這種結構,可能需要做的就是焦點記憶,還有焦點控制.
焦點記憶可以參考我上面的demo或者使用 Leanback的 HorizontalGridView.
焦點控制可以在 FocusSearch 進行處理.
在這裏插入圖片描述

場景三: 左右結構的多級類似的菜單
這種的場景,處理方式一般在 focusSearch 去控制焦點,處理焦點錯亂的問題
在這裏插入圖片描述

場景四: 按鍵速度控制,在Activity或者主佈局添加都可以.

  	private final static long KEY_OUT_TIME = 150L;
    long mTimeLast = 0;
    long mTimeDelay = 0;
    long mTimeSpace = 0;

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            long nowTime = System.currentTimeMillis();
            this.mTimeDelay = nowTime - this.mTimeLast;
            this.mTimeLast = nowTime;
            if (this.mTimeSpace <= KEY_OUT_TIME && this.mTimeDelay <= KEY_OUT_TIME) {
                this.mTimeSpace += this.mTimeDelay;
                return true;
            }
            this.mTimeSpace = 0L;
        }
        return super.dispatchKeyEvent(event);
    }

場景五:
在這裏插入圖片描述
VideoView播放器佔滿窗口並且有焦點(focusableInTouchMode=true,focusable=true),佈局中的下方一個選集. 按鍵焦點下不去這麼辦?(這裏原因是因爲addFocusables已經將button添加到數據中,但是findNextFocus找不到下一個焦點)
當下面的顯示出來的時候,如下代碼:

button的xml設置:
android:focusableInTouchMode="true"
android:focusable="true"
代碼中:
button.requestFocusFromTouch();

場景六:
進入界面的第一個焦點如何設置?

  • 在xml中設置,想那個有焦點就設置focusableInTouchMode=true
 <Button
	android:focusableInTouchMode="true"
	android:focusable="true"/>

<Button android:focusable="true" />
  • 在代碼中請求或者延時請求
testBtn.requestFocusFromTouch... ...

6. 注意

Leanback的焦點記憶問題
在這裏插入圖片描述

// Leanback的GridLayoutManger的 onAddFocusables 代碼 !!
if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
	... ...
} else {
	// 這裏沒有進行判斷你的ItemView是否隱藏或者無效
	// 比如 你之前 是 pos=2 的位置,現在更新了數據,pos=2的位置已經隱藏或者無效拉
	// 那麼焦點將會出現在該位置上... ...
	// 導致焦點丟失(其實也沒有丟失,只不過焦點跑到無效的位置上去拉)!!
	View view = findViewByPosition(mFocusPosition);
	if (view != null) {
		view.addFocusables(views, direction, focusableMode);
	}
}

解決方案
在你的ItemView的 addFocusables 進行處理.

public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
	if ((!isEnabled() || getVisibility() == View.GONE) 
		&& null != prentView && prentView instanceof RecyclerView) {
		// 處理你的邏輯,
		// 1.獲取當前的位置
		// 2. 根據位置向前,向後查找有效的ItemView.
		RecyclerView tv = (RecyclerView) prentView;
        int position = tv.getChildLayoutPosition(this);
        // 向前查找
        for (int i = position; i >= 0; i--) {
			final View tempView = tv.getChildAt(i);
            if (tempView != null && tempView.isEnabled() 
            && tempView.getVisibility() == View.VISIBLE) {
				nextView = tempView;
				break;
            }
		}
		// 向後查找
		for (int i = position; i < getChildCount(); i++) {
		}
		// 如果只剩下一個也是無效的,那就停留在原來的位置.
		// 等等的邏輯,具體看自己的場景吧.
	} else {
		super.addFocusables(views, direction, focusableMode);
	}
}

7. 參考資料

<<Android內核剖析>>

8. 感謝支持

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章