版本: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的处理便不会再执行,以此达到我们的目的。