Android TV开发-按键焦点

2,662 阅读10分钟
原文链接: blog.csdn.net

下面将围绕下面几点展开讲解按键焦点:

文章目录

1. 写在前面

工欲善其事必先利其器,了解按键的流程,焦点的搜索,请求过程等等
对于我们在开发中遇到的问题,可以去思考以及解决一些刺手问题.
下面我将和分享下按键焦点过程以及它的一些实际应用场景.
由于本人经验有限,有问题还请大家多多指教,互相讨论.

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 按键焦点

按键按下后,上面讲解的过程 没有消耗 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. 焦点控制

几个焦点控制的函数,也可以配合 FocusFinder.getInstance().findNextFocus 一起使用.
在这里插入图片描述

敬请期待,完善中… …

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

5. 应用场景

多级菜单,上下,左右等等结构,等等 焦点错乱,焦点需要控制. 敬请期待,完善中… …

6. 参考资料