目录

Android 焦点

最近在做 Android TV,和手机 APP 不同的是,TV 大都需要依赖遥控器来改变焦点而不是 Touch 事件(除非你的屏幕可以触控~~)

Leanback

当时做 TV 项目搜到的资料都是它,但是没几个人细讲,我也不打算细讲···
因为这个库需要你自己去源码编译,有很多东西写死了,或者不是 public 的,而且不一定符合你们设计的审美~~
我们最后就是用源码编译搞的,只用了一个 VerticalGridView, 还改了一些东西。

如何自定义我的焦点呢?

项目中有很多神奇的 UI 上的要求,要我去实现一些自定义的效果。比如,进入播放页,要按确认键或者下键,呼出一些菜单?比如,跳转 tab 栏,焦点永远先跑到第二个 tab 上(因为第一个 tab 是历史记录)等等。这就不得不研究一下,焦点是如何分发寻找的。

事件起源

如果非要讲···不知道有没有人说的清···反正我是说不清。要从硬件说起,从驱动程序说起,从操作系统说起,中断程序说起~~

但是对于我等渣渣,还是从 ViewRootImpl 看起来吧~~

private int processKeyEvent(QueuedInputEvent q) {
            final KeyEvent event = (KeyEvent)q.mEvent;

            if (mUnhandledKeyManager.preViewDispatch(event)) {
                return FINISH_HANDLED;
            }

            // Deliver the key to the view hierarchy.
            if (mView.dispatchKeyEvent(event)) {
                return FINISH_HANDLED;
            }

            if (shouldDropInputEvent(q)) {
                return FINISH_NOT_HANDLED;
            }

            // This dispatch is for windows that don't have a Window.Callback. Otherwise,
            // the Window.Callback usually will have already called this (see
            // DecorView.superDispatchKeyEvent) leaving this call a no-op.
            if (mUnhandledKeyManager.dispatch(mView, event)) {
                return FINISH_HANDLED;
            }

            int groupNavigationDirection = 0;

            if (event.getAction() == KeyEvent.ACTION_DOWN
                    && event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
                if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
                    groupNavigationDirection = View.FOCUS_FORWARD;
                } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
                        KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
                    groupNavigationDirection = View.FOCUS_BACKWARD;
                }
            }

            // If a modifier is held, try to interpret the key as a shortcut.
            if (event.getAction() == KeyEvent.ACTION_DOWN
                    && !KeyEvent.metaStateHasNoModifiers(event.getMetaState())
                    && event.getRepeatCount() == 0
                    && !KeyEvent.isModifierKey(event.getKeyCode())
                    && groupNavigationDirection == 0) {
                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) {
                if (groupNavigationDirection != 0) {
                    if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                        return FINISH_HANDLED;
                    }
                } else {
                    if (performFocusNavigation(event)) {
                        return FINISH_HANDLED;
                    }
                }
            }
            return FORWARD;
        }

代码相对于事件分发还不算太长~~ 可以看到前面都是对于 dispatchKeyEvent 相关的拦截,如果拦截了,那么就不会走后续的流程了,所以重写 dispatchKeyEvent 可以帮忙处理一些按键需求。

之后,如果不返回 true,还会有一些特殊按键处理,之后就到了 performFocusNavigation,这个就是一个默认的自动寻找焦点的逻辑了。

private boolean performFocusNavigation(KeyEvent event) {
            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 true;
                        }
                    }

                    // Give the focused view a last chance to handle the dpad key.
                    if (mView.dispatchUnhandledMove(focused, direction)) {
                        return true;
                    }
                } else {
                    if (mView.restoreDefaultFocus()) {
                        return true;
                    }
                }
            }
            return false;
        }

代码其实很好懂,判断方向,寻找焦点,请求焦点。核心方法,focusSearch 和 requestFocus,这两个都是 View 的方法,给了我们重写来实现自定义效果的可能。这里 requestFocus 如果是 ViewGroup 还会进入 onRequestFocusInDescendants 这个方法,会见到熟悉的 getDescendantFocusability。

经常会在网上看到文章说给 ViewGroup 添加 descendantFocusability 这个属性等等,其实就是在这类进行判断的。这个方法也可以重写,但是意义不大。

理一理思路

所以说,现在我们知道了,焦点事件或者说遥控器的按键事件的分发我们可以在 dispatchKeyEvent 拦截,如果拦截了事件被截断。如果不拦截,那么一般会走到寻找焦点的逻辑,这时候 focusSearchrequestFocusonRequestFocusInDescendants 就出现了。可以重写 focusSearch 来返回当前情况下你想让哪个 view 获取到焦点,如果不重写,focusSearch 会一直到 isRootNamespace 返回 true,走FocusFinder.getInstance().findNextFocus(this, focused, direction)。FocusFinder这里就会有一套自己根据方向寻找焦点 view 的默认逻辑,这也是为什么,你不重写任何方法,有时候焦点也能达到你想要的效果。

那么来看看之前说的两个需求

  1. 进入播放页,要按确认键或者下键,呼出一些菜单
    这个就很简单了,dispatchKeyEvent 中判断如果是确认键和下键,就做一个动画,return true,消耗掉这个事件就 OK

  2. 跳转 tab 栏,焦点永远先跑到第二个 tab 上
    这个也很简单,核心就是焦点本来要跑到这个 tab 的 ViewGroup 时,第一个焦点永远在 第二个子 View 上。onRequestFocusInDescendants,这个方法就发生在 ViewGroup 找焦点的时候,你可以设置 ViewGroup 为 FOCUS_AFTER_DESCENDANTS,并重写这个方法,当这个方法被调用时,寻找第二个 View,调用其 requestFocus 方法并返回 true 即可。

所以,想不让焦点分发下来,或者焦点变化中做一些动画,只需要 dispatchKeyEvent,想定制一些焦点逻辑则需要 focusSearch。

简单来说,这就是整个焦点分发的核心逻辑!当然,细节还有很多很多,但是对于一个应用工程师来说,做需求这些够用了,深入研究源码的每一个细节,有时候意义并不大。