Android 按键事件分发流程

版本:Android11

前言:

最近TV开发中遇到这么一个需求,添加一键进入谷歌浏览器,并进入指定的网址中。最开始在PhoneWindowManager中进行添加,但是添加完成后发现存在问题。每次进入浏览器都会打开一个浏览器窗口,按下次数多了会变得异常卡顿,后续将按键响应的流程放在PhoneFallbackEventHandler中进行处理之后便能解决这个问题,觉得较为奇怪,便准备查看一遍按键事件的分发流程,以理清整个的逻辑。

按键处理流程:

首先从PhoneWindowManager开始,按键从遥控器按下之后,经过一系列底层向上的传递工作之后,会来到PhoneWindowManager的interceptKeyBeforeDispatching方法和interceptKeyBeforeQueueing方法,在这里可以对按键进行初步处理,例如音量键,Home键,都是在这里处理。代码较长,这里就不贴了,大家可以自行去查看源码。在PhoneWindowManager中如果没有处理按键事件,那么按键会传入到ViewRootImpl。

frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java

在ViewRootImpl有三个内部类用于处理按键事件,分别是:
ViewPreImeInputStage,EarlyPostImeInputStage,ViewPostImeInputStage
这三个内部类的区别是ViewPreImeInputStage会在界面有输入法,且事件尚未发送给输入法的时候调用,EarlyPostImeInputStage会在发送给输入法之后调用,而ViewPostImeInputStage则是发送给View处理,我们的需求中没有出现输入法的地方,所以我们直接来看ViewPostImeInputStage类:

frameworks\base\core\java\android\view\ViewRootImpl.java
    /**
     * 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) {
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);
            } else {
                final int source = q.mEvent.getSource();
                if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                    return processPointerEvent(q);
                } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                    return processTrackballEvent(q);
                } else {
                    return processGenericMotionEvent(q);
                }
            }
        }
        ...
    }

在事件发下来后首先会调用onProcess方法,在这个方法中判断event属于KeyEvent之后就会调用processKeyEvent方法,我们继续看processKeyEvent方法:

        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.
            //这里会将按键事件传递给mView的dispatchKeyEvent方法
            //这个mView便是我们Activity的最顶层布局DecorView对象
            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;
			//此处会判断一下方向键以及TAB键,进行方向和焦点的处理
            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.
            // 这里便是我们添加按键处理的地方
            //可以看到这里是在mView没有处理消耗按键之后才能接收到事件
            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;
        }

从上面这段代码我们可以看出,按键消息会先发送给Activity所在的DecorView对象,如果DecorView没有对按键进行处理,才会继续下发到PhoneFallbackEventHandler对象。而我们第一次按下按键时由于在Launcher界面,DecorView没有处理此按键事件,便将按键事件分发到PhoneFallbackEventHandler中,而我们在PhoneFallbackEventHandler中onKeyUp方法才能正确获取到消息并进行处理。而当进入了对应网页之后,大部分按键都在mView.dispatchKeyEvent中被拦截了,我们继续查看DecorView的dispatchKeyEvent方法。

frameworks\base\core\java\com\android\internal\policy\DecorView.java
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        final int action = event.getAction();
        final boolean isDown = action == KeyEvent.ACTION_DOWN;

        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;
                }
            }
        }
		//这里进行判断,当window未销毁,并且Callback存在,并且是应用程序时
		//就会发送到PhoneWindow.Callback 的dispatchKeyEvent方法,否则就发往父类
        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;
            }
        }

        return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
                : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
    }

从这里可以看到当window未销毁时,就会去获取Callback对象,而这个callback对象实际上就是我们应用的Activity,因为Activity是实现了Callback接口的,通过打印也可以能看到在此处调用完之后就是调用的Activity的dispatchKeyEvent方法,大家可以自行实验。
而mFeatureId变量表示小于零表示为应用程序。

结论

经过这一步骤后,按键便传递到了Activity中,而谷歌浏览器中做了什么处理我们看不到代码,只能根据打印猜测一番。在进入了指定页面之后输入的一些按键都会在mView.dispatchKeyEvent中被拦截,只有Back键,Menu键等特殊按键有响应,应当是此页面直接拦截了按键事件,所以进入页面之后在ViewRootImpl中只会走到mView.dispatchKeyEvent便return,我们加在PhoneFallbackEventHandler的处理便不会再执行,以此达到我们的目的。

你可能感兴趣的:(android,java)