1、按键派发总体过程
ViewRoot中定义了一个InputHandler对象:
private final InputHandler mInputHandler = new InputHandler() {
public void handleKey(KeyEvent event, Runnable finishedCallback) {
startInputEvent(finishedCallback);
dispatchKey(event, true);
}
public void handleMotion(MotionEvent event, Runnable finishedCallback) {
startInputEvent(finishedCallback);
dispatchMotion(event, true);
}
};
该对象有两个方法,分别是handleKey()和handleMotion()
当底层得到按键消息,会调用InputHandler对象的handleKey(),handleKey()中再调用dispatchKey(),在dispatchKey()中发送一个异步消息,然后调用到了deliverKeyEvent(),代码如下:
private void deliverKeyEvent(KeyEvent event, boolean sendDone) {
// If mView is null, we just consume the key event because it doesn't
// make sense to do anything else with it.
boolean handled = mView != null
? mView.dispatchKeyEventPreIme(event) : true;
if (handled) {
if (sendDone) {
finishInputEvent();
}
return;
}
// If it is possible for this window to interact with the input
// method window, then we want to first dispatch our key events
// to the input method.
if (mLastWasImTarget) {
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null && mView != null) {
int seq = enqueuePendingEvent(event, sendDone);
if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq="
+ seq + " event=" + event);
imm.dispatchKeyEvent(mView.getContext(), seq, event,
mInputMethodCallback);
return;
}
}
deliverKeyEventToViewHierarchy(event, sendDone);
}
该方法中,在输入法窗口处理前,调用dispatchKeyEventPreIme(),如果需要回执,通过finishInputEvent()告诉WmS已经处理了该按键消息。接着把按键消息转发给输入法,在ViewRoot对象里包含真正的View对象mView,故最后就能通过deliverKeyEventToViewHierarchy()转发给真正的视图了。
private void deliverKeyEventToViewHierarchy(KeyEvent event, boolean sendDone) {
try {
if (mView != null && mAdded) {
final int action = event.getAction();
boolean isDown = (action == KeyEvent.ACTION_DOWN);
if (checkForLeavingTouchModeAndConsume(event)) {
return;
}
if (Config.LOGV) {
captureKeyLog("captureDispatchKeyEvent", event);
}
boolean keyHandled = mView.dispatchKeyEvent(event);
if (!keyHandled && isDown) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
direction = View.FOCUS_LEFT;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
direction = View.FOCUS_RIGHT;
break;
case KeyEvent.KEYCODE_DPAD_UP:
direction = View.FOCUS_UP;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
direction = View.FOCUS_DOWN;
break;
}
if (direction != 0) {
View focused = mView != null ? mView.findFocus() : null;
if (focused != null) {
View v = focused.focusSearch(direction);
boolean focusPassed = false;
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);
}
focusPassed = v.requestFocus(direction, mTempRect);
}
if (!focusPassed) {
mView.dispatchUnhandledMove(focused, direction);
} else {
playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
}
}
}
}
}
} finally {
if (sendDone) {
finishInputEvent();
}
// Let the exception fall through -- the looper will catch
// it and take care of the bad app for us.
}
}
deliverKeyEventToViewHierarchy()方法可分四步执行:
(1)checkForLeavingTouchModeAndConsume()判断该消息是否会导致离开触摸模式,一般返回false
(2)mView.dispatchKeyEvent(event)把事件派发给根视图
(3)如果是Down事件且是方向键,进行方向键的处理
(4)最后通过finishInputEvent()报告WmS已处理该消息。
2、根视图内部派发过程
接着上一节讲,系统调用到了mView.dispatchKeyEvent(event),这个根视图是DecorView或普通的ViewGroup。现在介绍PhoneWindow.DecorView这个根视图内部的按键消息派发流程,该函数代码如下:
public boolean dispatchKeyEvent(KeyEvent event) {
final int keyCode = event.getKeyCode();
final boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN;
/*
* If the user hits another key within the play sound delay, then
* cancel the sound
*/
if (keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_UP
&& mVolumeKeyUpTime + VolumePanel.PLAY_SOUND_DELAY
> SystemClock.uptimeMillis()) {
/*
* The user has hit another key during the delay (e.g., 300ms)
* since the last volume key up, so cancel any sounds.
*/
AudioManager audioManager = (AudioManager) getContext().getSystemService(
Context.AUDIO_SERVICE);
if (audioManager != null) {
audioManager.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME,
mVolumeControlStreamType, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
}
}
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 ((mPanelChordingKey > 0) && (mPanelChordingKey != keyCode)) {
// Perform the shortcut (mPreparedPanel can be null since
// global shortcuts (such as search) don't rely on a
// prepared panel or menu).
boolean handled = performPanelShortcut(mPreparedPanel, keyCode, event,
Menu.FLAG_PERFORM_NO_CLOSE);
if (!handled) {
/*
* If not handled, then pass it to the view hierarchy
* and anyone else that may be interested.
*/
handled = dispatchKeyShortcutEvent(event);
if (handled && mPreparedPanel != null) {
mPreparedPanel.isHandled = true;
}
}
if (handled) {
return true;
}
}
// If a panel is open, perform a shortcut on it without the
// chorded panel key
if ((mPreparedPanel != null) && mPreparedPanel.isOpen) {
if (performPanelShortcut(mPreparedPanel, keyCode, event, 0)) {
return true;
}
}
}
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);
}
现分析处理流程:
(1)处理间量键
(2)调用performPanelShortcut()方法处理系统快捷键
(3)调用Callback对象的dispatchKeyEvent()
(4)最后调用PhoneWindow对象的onKeyDown()和onKeyUp()事件根视图只处理了少数按键消息。
3、Activity内部派发过程
Activity实现了Window.Callback接口,上一节中的(3)调用Callback对象的dispatchKeyEvent(),实际上就是使Activity对象调用了dispatchKeyEvent(),代码如下:
public boolean dispatchKeyEvent(KeyEvent event) {
onUserInteraction();
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);
}
执行过程分析如下:
1、调用onUserInteraction(),当开始和activity交互的时候,用户可以做点什么
2、获取Window对象,实际上就是PhoneWindow对象,因此调用到:
public boolean superDispatchKeyEvent(KeyEvent event) {
return mDecor.superDispatchKeyEvent(event);
}
接着又调用:
public boolean superDispatchKeyEvent(KeyEvent event) {
return super.dispatchKeyEvent(event);
}
DecorView的父类是FrameLayout,该函数并没有重载dispatchKeyEvent(),则super最后会调用到ViewGroup的同名函数
ViewGroup的dispatchKeyEvent()的实现如下:
public boolean dispatchKeyEvent(KeyEvent event) {
if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) {
return super.dispatchKeyEvent(event);
} else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) {
return mFocused.dispatchKeyEvent(event);
}
return false;
}
如果ViewGroup本身拥有焦点,则把消息派发到试图本身,如果是子视图拥有焦点,则把消息派发给子视图。
例如有多重嵌套的视图,由于mFocused变量代表的是拥有焦点的视图或是包含拥有焦点视图的ViewGroup,所以从最上层开始,逐层调用dispatchKeyEvent(),最后到拥有焦点的视图。如果这个视图是View,则最后调用的是:
public boolean dispatchKeyEvent(KeyEvent event) {
// If any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
if (android.util.Config.LOGV) {
captureViewInfo("captureViewKeyEvent", this);
}
if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
return event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this);
}
这个方法中,首先回调onKey()函数,如果onKey()中没有消费该消息,则最后调用event.dispatch()函数。第一个参数是View本身,因此会调用到View对象的onKeyDown和onKeyUp()方法等。如果在这个地方继续返回false,则会执行到第三步。
3、第2步中派发给用户视图的消息没有被处理,则通过event.dispatch()方法调用Activity内部的onKeyDown()和onKeyUp()等。
分析到这里也解释出了如果自定义View没有获得焦点,那么你复写的onKeyDown()和onKeyUp()方法是不会被调用到,而最终调用了Activity对象的onKeyDown()和onKeyUp()。
下面具体分析KeyEvent的dispatch()中的Down事件过程,代码如下
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;
}
1、清除了mFlags标志
2、receiver.onKeyDown()执行,这个receiver可能是View对象或activity对象
3、如果第二步返回true,且是第一次按,并且mFlags包含FLAG_START_TRACKING,才执行后面的动作
这里的这个现象很奇怪,mFlags在前面明明在前被清除过,后面判断mFlags&FLAG_START_TRACKING肯定应该等于0啊
这里给出的解释是这样的,要想让你的自定义View能够执行到onKeyLongPress(),称为”处理生理长按“,须做以下几件事情。
1、你的View必须重载onKeyDown()事件,且返回true
2、onKeyDown()中必须执行event.startTracking(),参见Activity中的onKeyDown()方法也是调用了的。
3、最后要做的就是重载onKeyLongPress(),做你想要做的事情了。
在处理ACTION_UP,先调用state.handleUpEvent(),判断是否发生了“生理长按”,最后调到了receiver.onKeyUp()。
分析View中的onKeyDown()和onKeyUp()
onKeyDown()中实现的功能:
1、只接收DPAD_CENTER和KEYCODE_ENTER这两个按键消息
2、判断是View是否ENABLE,如果不是,返回false
3、如果可点击或可长按,通过setPress(true)设置视图为按下状态
4、postDelay一个Runnable来实现长按,不则于前面讲的“生理长按”,这里超过500ms,则会回调onLongClick()
onKeyUp()中实现的功能
1、判断是否可点
2、如果处于pressed的状态,调用setPress(false)
3、如果释放按键时还没有500ms,则取消前面的Runnable
4、调用performClick()方法,回调onClick()方法