本篇主要阅读Android
源码讲解TV
的按键事件分发原理和焦点查找原理,源码基于Android9.0
,首先思考几个问题:
- 当遥控器按下一个按键时按键事件是如何一步一步分发处理的
- 为什么有的设备长按遥控器第一次会先
onKeyDown
、onKeyUp
,之后才是正常的一直onKeyDown
直到松手才onKeyUp
- 当给
View
设置setOnKeyListener
时,会先走View
的onKeyDown
回调还是OnKeyListener
回调 Activity
的onBackPressed
方法什么情况下会调用- 当按键按下方向键时焦点时如果未控制下一个获取焦点的时候,系统是如何知道该让哪一个控件获取焦点的
带着这些问题,我们一起来撸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
对应的onKeyDown
,onKeyUp
方法来处理。
接下来我们按照这个派发顺序依次来看看相关方法的实现,这里先看看Activity
的dispatchKeyEvent
实现:
/**
* 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);
}
此时,再来看下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)) {
// 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;
}
接着看下View
的dispatchKeyEvent
方法:
/**
* Dispatch a key event to the next view on the focus path. This path runs