Android TV按键焦点原理浅谈

本文深入探讨了Android TV的按键事件分发和焦点导航流程。从按键事件的入口开始,详细分析了按键事件如何分发,如何处理长按事件,以及焦点查找策略。在焦点导航部分,解释了系统如何决定下一个获取焦点的控件,涉及findFocus方法的实现。文章通过源码解析,帮助读者理解系统处理按键和焦点问题的机制。
摘要由CSDN通过智能技术生成

本篇主要阅读Android源码讲解TV的按键事件分发原理和焦点查找原理,源码基于Android9.0,首先思考几个问题:

  • 当遥控器按下一个按键时按键事件是如何一步一步分发处理的
  • 为什么有的设备长按遥控器第一次会先onKeyDownonKeyUp,之后才是正常的一直onKeyDown直到松手才onKeyUp
  • 当给View设置setOnKeyListener时,会先走ViewonKeyDown回调还是OnKeyListener回调
  • ActivityonBackPressed方法什么情况下会调用
  • 当按键按下方向键时焦点时如果未控制下一个获取焦点的时候,系统是如何知道该让哪一个控件获取焦点的

带着这些问题,我们一起来撸Android源码吧!了解了系统是如何处理的有便于我们解决TV上一些按键和焦点的问题。

一、按键事件入口

首先我们看下按键事件的入口ViewRootImpl类中的ViewPostImeInputStage内部类:

    /**
     * Delivers post-ime input events to the view hierarchy.
     */
    final class ViewPostImeInputStage extends InputStage {
        public ViewPostImeInputStage(InputStage next) {
            super(next);
        }

        @Override
        protected int onProcess(QueuedInputEvent q) {
          	// 1.判断为按键事件则执行processKeyEvent方法
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);
            } else {
                final int source = q.mEvent.getSource();
              	// 2.判断为触摸事件则执行processPointerEvent方法
                if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                    return processPointerEvent(q);
                // 3.判断为轨迹球事件则执行processTrackballEvent方法
                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                    return processTrackballEvent(q);
                // 4.判断为运动事件则执行processGenericMotionEvent方法
                } else {
                    return processGenericMotionEvent(q);
                }
            }
        }

可以看到注释1,2,3,4分别判断不同事件执行不同方法,本篇主要讨论的TV焦点事件,主要看下processKeyEvent方法:

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

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

          	// 1.分发按键,如果有消费返回true不继续往下执行
            // 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 {
                 		// 2.如果按下按键则执行焦点导航逻辑
                    if (performFocusNavigation(event)) {
                        return FINISH_HANDLED;
                    }
                }
            }
            return FORWARD;
        }

二、按键事件分发流程

可以看到在该方法中执行了mView.dispatchKeyEvent方法,这里的View其实是DecorView,接着看下该方法:

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        final int action = event.getAction();
        final boolean isDown = action == KeyEvent.ACTION_DOWN;

        // 1.如果是第一次按下则处理panel的快捷键
        if (isDown && (event.getRepeatCount() == 0)) {
            // First handle chording of panel key: if a panel key is held
            // but not released, try to execute a shortcut in it.
            if ((mWindow.mPanelChordingKey > 0) && (mWindow.mPanelChordingKey != keyCode)) {
                boolean handled = dispatchKeyShortcutEvent(event);
                if (handled) {
                    return true;
                }
            }

            // If a panel is open, perform a shortcut on it without the
            // chorded panel key
            if ((mWindow.mPreparedPanel != null) && mWindow.mPreparedPanel.isOpen) {
                if (mWindow.performPanelShortcut(mWindow.mPreparedPanel, keyCode, event, 0)) {
                    return true;
                }
            }
        }

     		// 2.当Window没destroy且其Callback非空的话,交给其Callback处理
        if (!mWindow.isDestroyed()) {
            final Window.Callback cb = mWindow.getCallback();
            final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
                    : super.dispatchKeyEvent(event);
            if (handled) {
                return true;
            }
        }

      	// 3.如果上面还没处理,则分发到PhoneWindow到onKeyDown、onKeyUp事件处理
        return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
                : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
    }

上面首先判断了如果是第一次按下则处理panel的快捷键,如果处理了则不往下走,否则继续判断当窗口未销毁且回调非空则回调处理,如果处理了则不往下走,否则让PhoneWindow对应的onKeyDownonKeyUp方法来处理。

接下来我们按照这个派发顺序依次来看看相关方法的实现,这里先看看ActivitydispatchKeyEvent实现:

    /**
     * Called to process key events.  You can override this to intercept all
     * key events before they are dispatched to the window.  Be sure to call
     * this implementation for key events that should be handled normally.
     *
     * @param event The key event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchKeyEvent(KeyEvent event) {
        onUserInteraction();

        // Let action bars open menus in response to the menu key prioritized over
        // the window handling it
        final int keyCode = event.getKeyCode();
        if (keyCode == KeyEvent.KEYCODE_MENU &&
                mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
            return true;
        }

        Window win = getWindow();
        // 1.从这里事件的处理交给了与之相关的window对象,实质是派发到了view层次结构
        if (win.superDispatchKeyEvent(event)) {
            return true;
        }
        View decor = mDecor;
        if (decor == null) decor = win.getDecorView();
        // 2.如果view层次结构没处理则交给KeyEvent本身的dispatch方法,Activity的各种回调方***被触发
        return event.dispatch(this, decor != null
                ? decor.getKeyDispatcherState() : null, this);
    }

我们看第1点superDispatchKeyEvent方法,可以看到该方法为一个抽象方法,而它的实现是实现它的子类PhoneWindow:

    @Override
    public boolean superDispatchKeyEvent(KeyEvent event) {
        return mDecor.superDispatchKeyEvent(event);
    }

该方法又回调用DecorView中的superDispatchKeyEvent方法:

    public boolean superDispatchKeyEvent(KeyEvent event) {
        // Give priority to closing action modes if applicable.
        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            final int action = event.getAction();
            // Back cancels action modes first.
            if (mPrimaryActionMode != null) {
                if (action == KeyEvent.ACTION_UP) {
                    mPrimaryActionMode.finish();
                }
                return true;
            }
        }

      	// 1.如果ViewGroup的dispatchKeyEvent方法消费掉了,返回true不走下面
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
      
      	// 2.如果ViewRootImpl不为空且被ViewRootImpl的dispatchUnhandledKeyEvent方法消费了,则返回true
        return (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event);
    }

此时,再来看下ViewGroupdispatchKeyEvent方法:

    @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)) {
          	// 1.如果ViewGroup当前是获焦状态或者有边界,分发给View处理
            if (super.dispatchKeyEvent(event)) {
                return true;
            }
        } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
                == PFLAG_HAS_BOUNDS) {
         		// 2.如果ViewGroup中有获取焦点的View并且ViewGroup有边界,则交给mFocused处理
            if (mFocused.dispatchKeyEvent(event)) {
                return true;
            }
        }

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
        }
        return false;
    }

接着看下ViewdispatchKeyEvent方法:

    /**
     * Dispatch a key event to the next view on the focus path. This path runs
 
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值