美文网首页Android TVAndroid TV开发Android TV
Android TV 焦点原理源码解析

Android TV 焦点原理源码解析

作者: 砺雪凝霜 | 来源:发表于2017-04-02 18:35 被阅读1396次
    • 前言

    相信很多刚接触AndroidTV开发的开发者,都会被各种焦点问题给折磨的不行。不管是学技术还是学习其他知识,都要学习和理解其中原理,碰到问题我们才能得心应手。下面就来探一探Android的焦点分发的过程。

    • Android焦点分发,拦截过程的实现

    Android焦点事件的分发是从ViewRootImpl的processKeyEvent开始的,源码如下:

            private int processKeyEvent(QueuedInputEvent q) {
                final KeyEvent event = (KeyEvent)q.mEvent;
    
                // Deliver the key to the view hierarchy.
                if (mView.dispatchKeyEvent(event)) {
                    return FINISH_HANDLED;
                }
    
                if (shouldDropInputEvent(q)) {
                    return FINISH_NOT_HANDLED;
                }
    
                // If the Control modifier is held, try to interpret the key as a shortcut.
                if (event.getAction() == KeyEvent.ACTION_DOWN
                        && event.isCtrlPressed()
                        && event.getRepeatCount() == 0
                        && !KeyEvent.isModifierKey(event.getKeyCode())) {
                    if (mView.dispatchKeyShortcutEvent(event)) {
                        return FINISH_HANDLED;
                    }
                    if (shouldDropInputEvent(q)) {
                        return FINISH_NOT_HANDLED;
                    }
                }
    
                // Apply the fallback event policy.
                if (mFallbackEventHandler.dispatchKeyEvent(event)) {
                    return FINISH_HANDLED;
                }
                if (shouldDropInputEvent(q)) {
                    return FINISH_NOT_HANDLED;
                }
    
                // Handle automatic focus changes.
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    int direction = 0;
                    switch (event.getKeyCode()) {
                        case KeyEvent.KEYCODE_DPAD_LEFT:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_LEFT;
                            }
                            break;
                        case KeyEvent.KEYCODE_DPAD_RIGHT:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_RIGHT;
                            }
                            break;
                        case KeyEvent.KEYCODE_DPAD_UP:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_UP;
                            }
                            break;
                        case KeyEvent.KEYCODE_DPAD_DOWN:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_DOWN;
                            }
                            break;
                        case KeyEvent.KEYCODE_TAB:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_FORWARD;
                            } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                                direction = View.FOCUS_BACKWARD;
                            }
                            break;
                    }
                    if (direction != 0) {
                        View focused = mView.findFocus();
                        if (focused != null) {
                            View v = focused.focusSearch(direction);
                            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);
                                }
                                if (v.requestFocus(direction, mTempRect)) {
                                    playSoundEffect(SoundEffectConstants
                                            .getContantForFocusDirection(direction));
                                    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
                            View v = focusSearch(null, direction);
                            if (v != null && v.requestFocus(direction)) {
                                return FINISH_HANDLED;
                            }
                        }
                    }
                }
                return FORWARD;
            }
    
    

    源码比较长,下面我就慢慢来讲解一下具体的每一个细节。

    • (1) 首先由dispatchKeyEvent进行焦点的分发,如果dispatchKeyEvent方法返回true,那么下面的焦点查找步骤就不会继续了。

    dispatchKeyEvent方法返回true代表焦点事件被消费了。

      // Deliver the key to the view hierarchy.
                if (mView.dispatchKeyEvent(event)) {
                    return FINISH_HANDLED;
                }
    
    • 首先会执行mView的dispatchKeyEvent方法,估计大家会好奇这个mView是个什么鬼?其实它就是Activity的顶层容器DecorView,它是一FrameLayout。所以这里的dispatchKeyEvent方法应该执行的是ViewGroup的dispatchKeyEvent()方法,而不是View的dispatchKeyEvent方法。

    ViewGroup的dispatchKeyEvent()方法的源码如下:

     @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onKeyEvent(event, 1);
            }
    
            if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
                    == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
                if (super.dispatchKeyEvent(event)) {
                    return true;
                }
            } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                    == PFLAG_HAS_BOUNDS) {
                if (mFocused.dispatchKeyEvent(event)) {
                    return true;
                }
            }
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
            }
            return false;
        }
    

    (2)ViewGroup的dispatchKeyEvent执行流程

    • 首先ViewGroup会一层一层往上执行父类的dispatchKeyEvent方法,如果返回true那么父类的dispatchKeyEvent方法就会返回true,也就代表父类消费了该焦点事件,那么焦点事件自然就不会往下进行分发。
    • 然后ViewGroup会判断mFocused这个view是否为空,如果为空就会return false,焦点继续往下传递;如果不为空,那就会return mFocused的dispatchKeyEvent方法返回的结果。这个mFocused是什么呢?其实
      是ViewGroup中当前获取焦点的子View,这个可以从requestChildFocus方法中得到答案。requestChildFocus()的源码如下:
     @Override
        public void requestChildFocus(View child, View focused) {
            if (DBG) {
                System.out.println(this + " requestChildFocus()");
            }
            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);
                }
    
                mFocused = child;
            }
            if (mParent != null) {
                mParent.requestChildFocus(this, focused);
            }
        }
    
    

    (3)下面再来瞧瞧view的dispatchKeyEvent方法的具体的执行过程

      public boolean dispatchKeyEvent(KeyEvent event) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onKeyEvent(event, 0);
            }
    
            // Give any attached key listener a first crack at the event.
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
                return true;
            }
    
            if (event.dispatch(this, mAttachInfo != null
                    ? mAttachInfo.mKeyDispatchState : null, this)) {
                return true;
            }
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
            }
            return false;
        }
    

    惊奇的发现执行了onKeyListener中的onKey方法,如果onKey方法返回true,那么dispatchKeyEvent方法也会返回true

    可以得出结论:如果想要修改ViewGroup焦点事件的分发,可以这么干:

    • 重写view的dispatchKeyEvent方法
    • 给某个子view设置onKeyListener监听

    注意:实际开发中,理论上所有焦点问题都可以通过给dispatchKeyEvent方法增加监听来来拦截来控制。

    • **回到ViewRootImpl中,焦点没有被dispatchKeyEvent拦截的情况下的处理过程 **

    (1)dispatchKeyEvent方法返回false后,先得到按键的方向direction值,这个值是一个int类型参数。这个direction值是后面来进行焦点查找的。

      // Handle automatic focus changes.
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    int direction = 0;
                    switch (event.getKeyCode()) {
                        case KeyEvent.KEYCODE_DPAD_LEFT:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_LEFT;
                            }
                            break;
                        case KeyEvent.KEYCODE_DPAD_RIGHT:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_RIGHT;
                            }
                            break;
                        case KeyEvent.KEYCODE_DPAD_UP:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_UP;
                            }
                            break;
                        case KeyEvent.KEYCODE_DPAD_DOWN:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_DOWN;
                            }
                            break;
                        case KeyEvent.KEYCODE_TAB:
                            if (event.hasNoModifiers()) {
                                direction = View.FOCUS_FORWARD;
                            } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
                                direction = View.FOCUS_BACKWARD;
                            }
                            break;
                    }
    

    (2)接着会调用DecorView的findFocus()方法一层一层往下查找已经获取焦点的子View。
    ViewGroup的findFocus方法如下:

     @Override
        public View findFocus() {
            if (DBG) {
                System.out.println("Find focus in " + this + ": flags="
                        + isFocused() + ", child=" + mFocused);
            }
    
            if (isFocused()) {
                return this;
            }
    
            if (mFocused != null) {
                return mFocused.findFocus();
            }
            return null;
        }
    

    View的findFocus方法

     public View findFocus() {
            return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null;
        }
    

    说明:判断view是否获取焦点的isFocused()方法, (mPrivateFlags & PFLAG_FOCUSED) != 0 和view 的isFocused()方法是一致的。

      @ViewDebug.ExportedProperty(category = "focus")
        public boolean isFocused() {
            return (mPrivateFlags & PFLAG_FOCUSED) != 0;
        }
    

    其中isFocused()方法的作用是判断view是否已经获取焦点,如果viewGroup已经获取到了焦点,那么返回本身即可,否则通过mFocused的findFocus()方法来找焦点。mFocused其实就是ViewGroup中获取焦点的子view,如果mView不是ViewGourp的话,findFocus其实就是判断本身是否已经获取焦点,如果已经获取焦点了,返回本身。

    (3)回到processKeyEvent方法中,如果findFocus方法返回的mFocused不为空,说明找到了当前获取焦点的view(mFocused),接着focusSearch会把direction(遥控器按键按下的方向)作为参数,找到特定方向下一个将要获取焦点的view,最后如果该view不为空,那么就让该view获取焦点。

     if (direction != 0) {
                        View focused = mView.findFocus();
                        if (focused != null) {
                            View v = focused.focusSearch(direction);
                            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);
                                }
                                if (v.requestFocus(direction, mTempRect)) {
                                    playSoundEffect(SoundEffectConstants
                                            .getContantForFocusDirection(direction));
                                    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
                            View v = focusSearch(null, direction);
                            if (v != null && v.requestFocus(direction)) {
                                return FINISH_HANDLED;
                            }
                        }
                    }
    

    (4)focusSearch方法的具体实现。

    focusSearch方法的源码如下:

      @Override
        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;
        }
    
    

    可以看出focusSearch其实是一层一层地网上调用父View的focusSearch方法,直到当前view是根布局(isRootNamespace()方法),通过注释可以知道focusSearch最终会调用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦点view是通过FocusFinder来找到的。

      @Override
        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;
        }
    

    (5)FocusFinder是什么?

    它其实是一个实现 根据给定的按键方向,通过当前的获取焦点的View,查找下一个获取焦点的view这样算法的类。焦点没有被拦截的情况下,Android框架焦点的查找最终都是通过FocusFinder类来实现的。

    (6)FocusFinder是如何通过findNextFocus方法寻找焦点的。

    下面就来看看FocusFinder类是如何通过findNextFocus来找焦点的。一层一层往下看,后面会执行findNextUserSpecifiedFocus()方法,这个方法会执行focused(即当前获取焦点的View)的findUserSetNextFocus方法,如果该方法返回的View不为空,且isFocusable = true && isInTouchMode() = true的话,FocusFinder找到的焦点就是findNextUserSpecifiedFocus()返回的View。

       private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) {
            // check for user specified next focus
            View userSetNextFocus = focused.findUserSetNextFocus(root, direction);
            if (userSetNextFocus != null && userSetNextFocus.isFocusable()
                    && (!userSetNextFocus.isInTouchMode()
                            || userSetNextFocus.isFocusableInTouchMode())) {
                return userSetNextFocus;
            }
            return null;
        }
    

    (7)findNextFocus会优先根据XML里设置的下一个将获取焦点的View ID值来寻找将要获取焦点的View。

    看看View的findUserSetNextFocus方法内部都干了些什么,OMG不就是通过我们xml布局里设置的nextFocusLeft,nextFocusRight的viewId来找焦点吗,如果按下Left键,那么便会通过nextFocusLeft值里的View Id值去找下一个获取焦点的View。

     View findUserSetNextFocus(View root, @FocusDirection int direction) {
            switch (direction) {
                case FOCUS_LEFT:
                    if (mNextFocusLeftId == View.NO_ID) return null;
                    return findViewInsideOutShouldExist(root, mNextFocusLeftId);
                case FOCUS_RIGHT:
                    if (mNextFocusRightId == View.NO_ID) return null;
                    return findViewInsideOutShouldExist(root, mNextFocusRightId);
                case FOCUS_UP:
                    if (mNextFocusUpId == View.NO_ID) return null;
                    return findViewInsideOutShouldExist(root, mNextFocusUpId);
                case FOCUS_DOWN:
                    if (mNextFocusDownId == View.NO_ID) return null;
                    return findViewInsideOutShouldExist(root, mNextFocusDownId);
                case FOCUS_FORWARD:
                    if (mNextFocusForwardId == View.NO_ID) return null;
                    return findViewInsideOutShouldExist(root, mNextFocusForwardId);
                case FOCUS_BACKWARD: {
                    if (mID == View.NO_ID) return null;
                    final int id = mID;
                    return root.findViewByPredicateInsideOut(this, new Predicate<View>() {
                        @Override
                        public boolean apply(View t) {
                            return t.mNextFocusForwardId == id;
                        }
                    });
                }
            }
            return null;
        }
    
    

    可以得出以下结论:

    1. 如果一个View在XML布局中设置了focusable = true && isInTouchMode = true,那么这个View会优先获取焦点。

    2. 通过设置nextFocusLeft,nextFocusRight,nextFocusUp,nextFocusDown值可以控制View的下一个焦点。

    Android焦点的原理实现就这些。总结一下:

    • **首先DecorView会调用dispatchKey一层一层进行焦点的分发,如果dispatchKeyEvent方法返回true的话,那么焦点就不会往下分发了。 **

    • 中途可以给某个子View设置OnKeyListener进行焦点的拦截。

    • **如果焦点没有被拦截的话,那么焦点就会交给系统来处理 **

    • Android底层先会记录按键的方向,后面DecorView会一层一层往下调用findFocus方法找到当前获取焦点的View

    • 后面系统又会根据按键的方向,执行focusSearch方法来寻找下一个将要获取焦点的View

    • focusSearch内部其实是通过FocusFinder来查找焦点的。FocusFinder会优先通过View在XML布局设置的下一个焦点的ID来查找焦点。

    • 最终如果找到将要获取焦点的View,就让其requestFocus。

    为了方便同志们学习,我这做了张导图,方便大家理解~


    Android焦点事件分发机制.png

    相关文章

      网友评论

      • CaoJiaming:特意登录拜谢博主
        (1) 首先由dispatchKeyEvent进行焦点的分发,如果dispatchKeyEvent方法返回true,那么下面的焦点查找步骤就不会继续了。
        这句话让我修复了一个焦点给到A又到B的棘手bug 。
        砺雪凝霜:@CaoJiaming 有收获就好
      • 请叫我大苏:有个疑问,DecorView不是有自己实现dispatchKeyEvent()么,那为什么还是调用父类ViewGroup的dispatchKeyEvent(),不应该是调用自己实现的么
      • xtl1889:很受用!
        遇到一个问题请教楼主,我的一个activity 里面有标题栏,标题栏下面是一个viewpager 加载fragment.fragment 中是recyclerview.当item 跳转到别的页面,再返回时。有时候焦点还在点击的item 上(我希望的效果)。有时候焦点会发生变化,标题栏的第一个标题会获取焦点。这是什么原因,返回时,我什么操作都没有做,为什么会引起焦点改变。
      • JS_WANGSHA:左侧一个listview,右侧一个recycleview,右侧用语音控制(非遥控器控制)让它滑动,调用smoothscrollby()让滑动一定的距离,滑动时,左侧会抢右侧的焦点状态,怎么解决?
        砺雪凝霜: @YHHLWTV 看看我重写的RecyclerView是有的
        JS_WANGSHA: @砺雪凝霜 recycleview没有setselection方法吧?
        砺雪凝霜: @YHHLWTV 滑动前先屏蔽listView的焦点,滑动完成后恢复焦点,滑动可以调用setSelection来完成,具体可以看看我的怎么重写RecyclerView的文章
      • 砺雪凝霜:估计重写recyclerView的requestFocuse方法了,看看是不是保留了之前的焦点View?
      • e0505ef573ca:写得很好,很详细!
        遇到一个问题,不处理按键事件,RecyclerView获取到焦点,会自动往上滑,然后之前的item获取到焦点,有什么好的解决办法吗?
        砺雪凝霜:@深藏孤与独 按照我刚刚提的,检查下RecyclerView的requestFocus方法是不是重写了?
        e0505ef573ca:@砺雪凝霜 嗯,拦截的话 getChildAt(0) 也是获取到的已经滑动至不见的item,不知道怎样获取当前显示的第一个item的位置或者 view
        砺雪凝霜: @深藏孤与独 实在不行就对dispatchKeyEvent方法进行拦截处理

      本文标题:Android TV 焦点原理源码解析

      本文链接:https://www.haomeiwen.com/subject/gnbgottx.html