开发中需要用到遥控器,各种上下左右菜单音量飞鼠OK按键满天飞...
对于key事件的捕获仅限于BACK/MENU/HOME按键的我来说,这完全是在搞事情啊!
因此,决定深刻认识一下,从源码角度(android-23)来理解key事件的传递机制。
首先从入口开始,直接看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
if (event.getKeyCode() == KeyEvent.KEYCODE_MENU &&
mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
return true;
}
Window win = getWindow();
if (win.superDispatchKeyEvent(event)) {
return true;
}
View decor = mDecor;
if (decor == null) decor = win.getDecorView();
return event.dispatch(this, decor != null
? decor.getKeyDispatcherState() : null, this);
}
分析源码可知,事件传递的优先顺序如下:
actionBar——>Window——>event.dispatch,下面按照这个顺序依次分析
一、actionBar
keyCode如果是KEYCODE_MENU,且actionBar存在,
则交给了actionBar处理:使用其onMenuKeyEvent方法,对菜单按键进行响应;
如果actionBar不消费,就交给Window处理
二、Window
Window对key事件的处理与Touch事件有些类似,都是View树中的传递,
不过Touch事件是由父到子层层往下传递,而key事件的传递是由焦点位置决定的。
下面按层级从Window——>View逐个分析
1、Window.superDispatchKeyEvent
如果actionBar不消费,事件传递给Window,
window直接调用了上述代码中的Window.superDispatchKeyEvent
方法,
Window.superDispatchKeyEvent源码如下:
/**
* Used by custom windows, such as Dialog, to pass the key press event
* further down the view hierarchy. Application developers should
* not need to implement or call this.
*
*/
public abstract boolean superDispatchKeyEvent(KeyEvent event);
注释已经说得很清楚:
*“该方法用于自定义窗口下(如dialog、Activity),key事件在View层级中的传递,开发过程中不需要自己去实现或调用这个方法。” *
Window是一个抽象类,Window#superDispatchKeyEvent也是一个抽象方法,
这个方法具体都做了些什么,我们到Window的唯一实现类PhoneWindow去查看,
2、PhoneWindow.superDispatchKeyEvent
PhoneWindow.superDispatchKeyEvent源码如下:
// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;
@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
return mDecor.superDispatchKeyEvent(event);
}
我们看到,PhoneWindow中直接调用了DecorView的superDispatchKeyEvent方法
简单介绍一下DecorView:
它是Activity的顶层容器,整个View树结构的最顶层View,代表整个应用的界面。Window、DecorView、Activity三者关系可以这样类比:
Window是窗户,DecorView是粘在窗户上的纸,Activity显示的内容就是纸上所画的内容。
DecorView分为title和content两部分,而这个content,也就是我们在Activity中setContentView所设置的布局View。
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;
}
}
return super.dispatchKeyEvent(event);
}
- DecorView在keyCode == KECODE_BACK时,检查是否有Menu弹出,有则毁MENU,防止内存泄漏
(mPrimaryActionMode是一种菜单方式,这里使用它销毁Menu) - 不是BACK按键时,return super.dispatchKeyEvent,即交给父类处理,
因为DecorView继承于FrameLayout,所以super.dispatchKeyEvent其实就是ViewGroup.dispatchKeyEvent,key事件在View层级中的传递最终在这里进行,所以我们再看ViewGroup的dispatchKeyEvent方法。
3、ViewGroup.dispatchKeyEvent
ViewGroup.dispatchKeyEvent源码:
// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
@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)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
- mInputEventConsistencyVerifier主要在debug的时候使用,暂不深究
- super.dispatchKeyEvent:
如果ViewGroup自身已经获得焦点,就调用super.dispatchKeyEvent,即View.dispatchKeyEvent - mFocus.dispatchKeyEvent:
如果ViewGroup没有获得焦点,而是ViewGroup中的某一个child获得了焦点,则把事件交给这个child,即上述代码中的mFocused,将key事件传递给焦点子View处理,也是View.dispatchKeyEvent - 如果都没有获取焦点,直接return false,不消费,所以ViewGroup.dispatchKeyEvent中,只有当ViewGroup或其child获取了焦点,才有机会去消费这个事件,而具体怎么消费,都交给了View的dispatchKeyEvent方法
继续往下,查看View.dispatchKeyEvent
4、View.dispatchKeyEvent
View.dispatchKeyEvent源码:
/**
* Dispatch a key event to the next view on the focus path. This path runs
* from the top of the view tree down to the currently focused view. If this
* view has focus, it will dispatch to itself. Otherwise it will dispatch
* the next node down the focus path. This method also fires any key
* listeners.
*
* @param event The key event to be dispatched.
* @return True if the event was handled, false otherwise.
*/
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
- dispatchKeyEvent做了什么?先看看注释怎么说:“将key事件派发到焦点路径上的某一个View,这个焦点路径遵循View树结构自顶向下的规则,如果当前View获得焦点,事件就派发给它自己,否则继续在焦点路径上往下派发。该方法也会触发KeyListener。”
- 如果View设置了KeyListener,且该View是启用状态,则将事件交给它设置的KeyListener处理
- 如果KeyListener不消费,或者没有KeyListener,则调用KeyEvent的dispatch方法
KeyEvent.dispath的第一个参数this,其实就是在View中实现的接口:KeyEvent.CallBack
(KeyEvent.CallBack在View和Activity中都有被实现)
5、KeyEvent.dispatch
(1)KeyEvent.dispatch源码:
public final boolean dispatch(Callback receiver, DispatcherState state,
Object target) {
switch (mAction) {
case ACTION_DOWN: {
mFlags &= ~FLAG_START_TRACKING;
if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state
+ ": " + this);
boolean res = receiver.onKeyDown(mKeyCode, this);
if (state != null) {
if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) {
if (DEBUG) Log.v(TAG, " Start tracking!");
state.startTracking(this, target);
} else if (isLongPress() && state.isTracking(this)) {
try {
if (receiver.onKeyLongPress(mKeyCode, this)) {
if (DEBUG) Log.v(TAG, " Clear from long press!");
state.performedLongPress(this);
res = true;
}
} catch (AbstractMethodError e) {
}
}
}
return res;
}
case ACTION_UP:
if (DEBUG) Log.v(TAG, "Key up to " + target + " in " + state
+ ": " + this);
if (state != null) {
state.handleUpEvent(this);
}
return receiver.onKeyUp(mKeyCode, this);
case ACTION_MULTIPLE:
final int count = mRepeatCount;
final int code = mKeyCode;
if (receiver.onKeyMultiple(code, count, this)) {
return true;
}
if (code != KeyEvent.KEYCODE_UNKNOWN) {
mAction = ACTION_DOWN;
mRepeatCount = 0;
boolean handled = receiver.onKeyDown(code, this);
if (handled) {
mAction = ACTION_UP;
receiver.onKeyUp(code, this);
}
mAction = ACTION_MULTIPLE;
mRepeatCount = count;
return handled;
}
return false;
}
return false;
}
dispatch方法中,根据KeyEvent.action进行判断:
ACTION_DOWN调用receiver.onKeyDown
ACTION_UP调用receiver.onKeyUp
ACTION_MULTIPLE调用receiver.onKeyMultiple那么这个receiver来自哪里?
回溯到View.dispatchKeyEvent中,调用event.dispatch时传入了this,查看View实现的接口就豁然开朗了
@UiThread
public class View implements Drawable.Callback, KeyEvent.Callback,AccessibilityEventSource {
显然,这里的receiver就是View本身,所以KeyEvent的dispatch方法将事件分了一下类别,然后又交给了View中对应的方法去处理,接下来看看KeyEvent.Callback在View中的实现
6、KeyEvent.Callback
KeyEvent中对Callback的定义如下:
public interface Callback {
boolean onKeyDown(int keyCode, KeyEvent event);
boolean onKeyLongPress(int keyCode, KeyEvent event);
boolean onKeyUp(int keyCode, KeyEvent event);
boolean onKeyMultiple(int keyCode, int count, KeyEvent event);
}
- KeyEvent的Callback接口主要包含onKeyDown、onKeyUp、onKeyLongPress、onKeyMultiPe方法,这不就是平时开发中,接收key事件常常复写的那个onKeyDown吗
View中对Callback的实现如下:
public boolean onKeyLongPress(int keyCode, KeyEvent event) {
return false;
}
public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
return false;
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
boolean result = false;
if (KeyEvent.isConfirmKey(keyCode)) {
if ((mViewFlags & ENABLED_MASK) == DISABLED) {
return true;
}
// Long clickable items don't necessarily have to be clickable
if (((mViewFlags & CLICKABLE) == CLICKABLE ||
(mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) &&
(event.getRepeatCount() == 0)) {
setPressed(true);
checkForLongClick(0);
return true;
}
}
return result;
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (KeyEvent.isConfirmKey(keyCode)) {
if ((mViewFlags & ENABLED_MASK) == DISABLED) {
return true;
}
if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
setPressed(false);
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
return performClick();
}
}
}
return false;
}
/** Whether key will, by default, trigger a click on the focused view.
* @hide
*/
public static final boolean isConfirmKey(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
return true;
default:
return false;
}
}
- View的onKeyLongPress和onKeyMultiPe方法都默认返回了false,
- View的onKeyDown和onKeyUp方法中默认使用isConfirmKey进行判断,
查看上述源码可知,isConfirmKey只在ENTER或PPAD_CENTER时返回true,即遥控器上的确认键或OK键,
所以onKeyDown和onKeyUp在默认情况下只消费ENTER和CENTER这种确认类型的按键 - 如果View的onKeyDown、onKeyUp等KeyEvent.Callback的实现方法都不消费,表示Activity.dispatchKeyEvent将事件派发给Window没有被消费,事件继续往下传递,到了Activity.dispatchKeyEvent方法中的第三部分——交给Activity中的event.dispatch处理
7、最后,总结一下Key事件在Window传递的整个流程:
1、 如果是BACK按键,先交给MENU处理,
2、如果不是BACK按键或MENU未处理,交给ViewGroup传递给它的焦点View:焦点View可以是ViewGroup本身或ViewGroup的child,如果焦点View设置了KeyListener,传递给KeyListener,如果没有,按KeyEvent.action类型传递给对应的onKeyDown等方法
3、 如果事件传递到了onKeyDown都没被处理,代表Window传递到它的View各个层级都没有消费该事件,回溯到Activity.dispatchKeyEvent继续往下传递
- 因为自己曾经在这里踩过坑,再次强调,重要的事情说三遍,只有ViewGroup或childView获取了焦点,ViewGroup才有机会消费key事件,换句话说,当ViewGroup或childView没有焦点时,key事件会往下传递,即最终交给了Activity.dispatchKeyEvent的第三步骤——event.dispatch
三、event.dispatch交给自己的onKeyDown
1、如果window不消费,最后直接执行了keyEvent.dispatch方法,这里的逻辑与View中的keyEvent.dispatch完全相同,只是Callback在Activity中实现,所以key事件都到了Activity的onKeyDown、onKeyUp....
2、注意:
正因为key事件从dispatchKeyEvent分发到onKeyDown、onKeyUp等是由envet.dispatch实现的,
所以如果用户在Activity中复写了dispatchKeyEvent方法,
如果返回true或false,key事件都不会到Activity的onKeyDown或onKeyUp中,
只有返回super.dispatchKeyEvent,执行到了event.dispatch,key事件才能够派发到Activity的onKeyDown或onKeyUp中,这是不同于dispatchTouchEvent的地方
四、Activity中Key事件传递流程总结
activity中先在dispatch函数中接收到事件,
1、key事件首先交给了actionBar处理
2、如果actionBar未处理,则交给window处理,window直接交给了DecorView.superDispatchKeyEvent,将事件传递给了获取焦点的View
4、如果焦点view未处理,继续往后传递,到了KeyEvent.dispatch,最终KeyEvent.dispatch将事件交给Activity的onKeyDown、onKeyUp、onKeyMultiPe或onKeyLongPress处理。
5、如果Activity的onKeyDown、onKeyUp、onKeyMultiPe或onKeyLongPress都没处理,即Activity本身及其ContentView都没有消费该事件,最终该事件就交给PhoneWindow或ViewRootImpl自动处理:自动查找焦点、系统音量调节等
整个事件分发流程图如下:
五、Activity中Key事件与Touch事件传递机制的区别
Touch事件:
Touch事件从Activity到Window,进行View树结构的分发,首先是顶层DecorView,再到ViewGroup,然后传递到子View按层级逐一向下分发,如果View树结构的分发都不消费,就回传给Activity的onTouchEvent处理Key事件:
Key事件从Activity先判断ActionBar需不需要,再判断Menu需不需要,然后交给Window在View树结构中分发,
同样首先也是顶层DecorView,DecorView直接在View层级间根据焦点位置决定谁获得key事件,若View各层级都不需要,再回到Activity自身的onKeyDown中
以上分析,如果有错误或任何建议的地方,欢迎指正,一起交流学习!