平台:Android6.0
Android开发中在自定义Activity以及View时经常会重写onKeyDown,onKeyUp,dispatchKeyEvent,同时View还有setOnKeyListener等,当一个按键事件发生时,这些方法将会被回调,但是到底哪个先回调,哪个后回调呢,一直不是特别清楚,只知道个大概,下面将详细讲述按键在java层的分发过程,其中重点关注按键事件在View层次中的分发
java层的按键分发从ViewRootImpl.java的WindowInputEventReceiver中的onInputEvent开始,从前面的应用程序注册消息监听过程分析和InputDispatcher分发键盘消息过程分析两篇文章的分析,InputDispatcher在处理按键事件时,会通过InputChannel::sendMessage函数将按键消息从server端写入,这里的InputChannel是当前获取焦点的窗口的InputChannel对的server端,这样应用程序端就可以收到该消息,然后调用NativeInputEventReceiver的handleEvent,最后调用到InputEventReceiver的onInputEvent函数(具体的可以看应用程序注册消息监听过程分析 的Step20-Step23)
Step 1. WindowInputEventReceiver.onInputEvent
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
@Override
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
...
}
这里只列出部分代码,当一个按键按下时onInputEvent方法就会被回调,其中调用了ViewRootImpl::enqueueInputEvent(event, this, 0, true);
ViewRootImpl.enqueueInputEvent
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
//从队列中获取一个QueuedInputEvent,这里的flags传入的是0
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
...
if (processImmediately) {
doProcessInputEvents();//这里传入的processImmediately是true,所以调用doProcessInputEvents
} else {
scheduleProcessInputEvents();
}
}
从前面的参数可知,这里表示要立即处理,所以调用doProcessInputEvents函数.
ViewRootImpl.doProcessInputEvents
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java
void doProcessInputEvents() {
// Deliver all pending input events in the queue.
while (mPendingInputEventHead != null) {
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
if (mPendingInputEventHead == null) {
mPendingInputEventTail = null;
}
q.mNext = null;
mPendingInputEventCount -= 1;
...
//分发按键
deliverInputEvent(q);
}
}
在deliverInputEvent函数中实际做按键的分发
ViewRootImpl.deliverInputEvent
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java
private void deliverInputEvent(QueuedInputEvent q) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
q.mEvent.getSequenceNumber());
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
}
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
//选择责任链的模式的入口,如果InputEvent需要跳过IME处理,则从mFirstPostImeInputStage(EarlyPostImeInputStage)开始,否则从mFirstInputStage(NativePreImeInputStage)开始分发
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
if (stage != null) {
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
这里调用了InputStage的deliver方法分发,这里的InputStage代表了输入事件的处理阶段,是一种责任链模式(可以看我的另外一篇文章责任链模式)
在ViewRootImpl的setView函数中会构造一个如图所示的InputStage的链,按键会从入口阶段,进入责任链,顺序处理,入口阶段根据QueuedInputEvent的状态来决定。q.shouldSendToSynthesizer() 这里一般是false,因此主要看stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage; 这里的shouldSkipIme其实是一个flag在构造QueuedInputEvent时传入的,从前面的onInputEvent调用的enqueueInputEvent(event, this, 0, true);可知,这里传入的flags是第三个参数0,那这里的shouldSkipIme就是false,那么按键会从mFirstPostImeInputStage 开始分发,就是图中的NativePreImeInputStage分发。
下面只从跟本文前面提到的Activity,View的按键分发流程相关的InputStage(ViewPostImeInputStage)开始分析
该函数定义在frameworks/base/core/java/android/view/ViewRootImpl.java
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
...
// Deliver the key to the view hierarchy.
// 调用成员变量mView的dispatchKeyEvent函数,这里mView是PhoneWindow.DecorView对象
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
}
...
// 如果按键是四向键或者是TAB键,则移动焦点
// Handle automatic focus changes.
if (event.getAction() == KeyEvent.ACTION_DOWN) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (event.hasNoModifiers()) {
direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.hasNoModifiers()) {
direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
if (direction != 0) {
View focused = mView.findFocus();
if (focused != null) {
View v = focused.focusSearch(direction);
if (v != null && v != focused) {
// do the math the get the interesting rect
// of previous focused into the coord system of
// newly focused view
focused.getFocusedRect(mTempRect);
if (mView instanceof ViewGroup) {
((ViewGroup) mView).offsetDescendantRectToMyCoords(
focused, mTempRect);
((ViewGroup) mView).offsetRectIntoDescendantCoords(
v, mTempRect);
}
if (v.requestFocus(direction, mTempRect)) {
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return FINISH_HANDLED;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return FINISH_HANDLED;
}
} else {
// find the best view to give focus to in this non-touch-mode with no-focus
View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
return FINISH_HANDLED;
}
}
}
}
return FORWARD;
}
上述主要分两步:
第一步是调用PhoneWindow.DecorView的dispatchKeyEvent函数,DecorView是View层次结构的根节点,按键从根节点开始根据Focuse view的path自上而下的分发。
第二步是判断按键是否是四向键,或者是TAB键,如果是则需要移动焦点
public boolean dispatchKeyEvent(KeyEvent event) {
...
if (!isDestroyed()) {
final Callback cb = getCallback();
final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
: super.dispatchKeyEvent(event);
if (handled) {
return true;
}
}
return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event)
: PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event);
}
主要的分发在下面开始,如果cb不为空并且mFeatureId小于0,则调用cb.dispatchKeyEvent开始分发,否则会调用DecorView的父类(View)的dispatchKeyEvent函数。cb是Window.Callback类型,Activity实现了Window.Callback接口,在attach函数中,会调用Window.setCallback函数将自己注册进PhoneWindow中,所以cb不为空。在PhoneWindow初始化时会调用installDecor函数生成DecorView对象,该函数中传入的mFeatureId是-1,所以mFeatureId也小于0。因此此处会调用Activity的dispatchKeyEvent函数,开始在View中分发按键。
接下来来看这里先看看Activity(Callback)的dispatchKeyEvent实现:
该函数定义在frameworks/base/core/java/android/app/Activity.java
public boolean dispatchKeyEvent(KeyEvent event) {
//调用自定义的onUserInteraction
onUserInteraction();
Window win = getWindow();
//调用PhoneWindow的superDispatchKeyEvent,实际调用DecorView的superDispatchKeyEvent,从DecorView开始从顶层View往子视图传递
if (win.superDispatchKeyEvent(event)) {
return true;
}
View decor = mDecor;
if (decor == null) decor = win.getDecorView();
//到这里如果view层次结构没有返回true则交给KeyEvent本身的dispatch方法,Activity的onKeyDown/onKeyUp/onKeyMultiple就会被触发
return event.dispatch(this, decor != null
? decor.getKeyDispatcherState() : null, this);
}
接着看下PhoneWindow的superDispatchKeyEvent
@Override
public boolean superDispatchKeyEvent(KeyEvent event) {
return mDecor.superDispatchKeyEvent(event);
}
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;
}
}
//进入View的层次结构,调用ViewGroup.dispatchKeyEvent
return super.dispatchKeyEvent(event);
}
再看ViewGroup的dispatchKeyEvent函数
该函数定义在frameworks/base/core/java/android/view/ViewGroup.java
public boolean dispatchKeyEvent(KeyEvent event) {
...
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
//如果此ViewGroup是focused并且具体的大小被设置了(有边界),则交给它处理,即调用View的实现
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
//否则,如果此ViewGroup中有focused的child,且child有具体的大小,则交给mFocused处理
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
...
return false;
}
这里可以看出如果ViewGroup满足条件,则优先处理事件而不发给子视图去处理。
下面看下View的dispatchKeyEvent实现
该函数定义在frameworks/base/core/java/android/view/View.java
public boolean dispatchKeyEvent(KeyEvent event) {
...
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//调用onKeyListener,如果注册了OnKeyListener,并且View属于Enable状态,则触发
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
//调用KeyEvent.dispatch方法,并将view作为参数传递进去,实际会回调View的onKeyUp/onKeyDown等方法
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
...
return false;
}
该函数定义在frameworks/base/core/java/android/view/View.java
下面看下View的onKeyDown/onKeyUp
// frameworks/base/core/java/android/view/View.java
public boolean onKeyDown(int keyCode, KeyEvent event) {
boolean result = false;
//处理KEYCODE_DPAD_CENTER、KEYCODE_ENTER按键
if (KeyEvent.isConfirmKey(keyCode)) {
if ((mViewFlags & ENABLED_MASK) == DISABLED) {
//disabled的view直接返回true,不再继续分发,即Activity的onKeyDown和onKeyUp无法收到KEYCODE_DPAD_CENTER、KEYCODE_ENTER事件
return true;
}
// Long clickable items don't necessarily have to be clickable
if (((mViewFlags & CLICKABLE) == CLICKABLE ||
(mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) &&
(event.getRepeatCount() == 0)) {// clickable或者long_clickable且是第一次down事件
setPressed(true);// 标记pressed,你可能设置了View不同的background,这时候就会有所体现(比如高亮效果)
checkForLongClick(0);
return true;
}
}
return result;
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
//处理KEYCODE_DPAD_CENTER、KEYCODE_ENTER按键
if (KeyEvent.isConfirmKey(keyCode)) {
if ((mViewFlags & ENABLED_MASK) == DISABLED) {
//disabled的view直接返回true,不再继续分发,即Activity的onKeyDown和onKeyUp无法收到KEYCODE_DPAD_CENTER、KEYCODE_ENTER事件
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;
}
最后再分析一下Activity的onKeyDown/onKeyUp
// frameworks/base/core/java/android/app/Activity.java
public boolean onKeyDown(int keyCode, KeyEvent event) {
//如果是back键则启动追踪
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.ECLAIR) {
event.startTracking();
} else {
onBackPressed();
}
return true;
}
...
}
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (getApplicationInfo().targetSdkVersion
>= Build.VERSION_CODES.ECLAIR) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
&& !event.isCanceled()) {
//如果是back键并且正在追踪该Event,则调用onBackPressed
onBackPressed();
return true;
}
}
return false;
}
总结
1. 调用Activity的dispatchKeyEvent
1.1. 调用onUserInteraction
1.2. 调用PhoneWindow的superDispatchKeyEvent
1.2.1.再然后调用DecorView的superDispatchKeyEvent,DecorView的superDispatchKeyEvent再调用父类的dispatchKeyEvent
1.2.2. DecorView是窗口中的顶级视图,按键从DecorView开始往子节点分发,因为DecorView继承自FrameLayout,FrameworkLayout继承自ViewGroup,所以实际调用ViewGroup的dispatchKeyEvent
1.2.3. ViewGroup中会先判断是否可以处理KeyEvent,如果可以则调用父类(View)的dispatchKeyEvent,如果当前的ViewGroup不满足条件,则调用mFocused的dispatchKeyEvent,这里的mFocused是焦点子视图,也可以是含有焦点子视图的ViewGroup,因此这里可能会发生递归调用。
1.2.3.1. 在View的dispatchKeyEvent中会先调用onKey函数,即会调用各个View注册的View.OnKeyListener对象的接口
1.2.3.2. 在View的dispatchKeyEvent中接着调用KeyEvent的dispatch函数,因为View实现了Window.Callback函数,因此会调用View的onKeyDown/onKeyUp/onKeyMultiple函数
1.3 调用KeyEvent的dispatch函数,因为Activity实现了Window.Callback函数,因此会调用Activity的onKeyDown/onKeyUp/onKeyMultiple函数
2. 如果整个View层次都没有返回true,则调用PhoneWindow的onKeyDown/onKeyUp函数
总结:
以一次KEYCODE_DPAD_DOWN按键为例,说明各个回调的顺序
Down事件
Activity::dispatchKeyEvent –> Activity::onUserInteraction –> View.OnKeyListener::onKey –> View.onKeyDown –> Activity::onKeyDown –> PhoneWindow.onKeyDown
Up事件
Activity::dispatchKeyEvent –> Activity::onUserInteraction –> View.OnKeyListener::onKey –> View.onKeyUp –> Activity::onKeyUp –> PhoneWindow.onKeyUp
View的KEYCODE_DPAD_CENTER KEYCODE_ENTER是在View的onKeyDown和onKeyUp中处理的,会处理一些高亮效果
一个Enable并且CLICKABLE的View响应KEYCODE_DPAD_CENTER KEYCODE_ENTER的DOWN事件后会return true,即Activity的onKeyDown不会回调,但是up事件没有return true,Activity的onKeyUp会回调
Activity的KEYCODE_BACK在Activity的onKeyDown启动追踪,在onKeyUp中实际调用onBackPressed函数处理返回键,所以长按back键是不会切换Activity的
参考:
http://www.cnblogs.com/xiaoweiz/p/3803301.html