- 「事件的产生」:当用户按下物理按键、使用遥控器、输入法或其他USB-OTG外接键盘等设备时,会产生按键事件(KeyEvent)。
- 「事件的分发」:
- 「分发起点」:按键事件由Android系统接收,通过Linux层分发到PhoneWindowManager(系统进程)和ViewRootImpl(应用进程)。
- 「分发顺序」:PhoneWindowManager先执行,处理系统级的按键事件(如音量键、电源键等),ViewRootImpl后执行,处理应用层的按键事件(如方向键、回车键等)。
- 「分发过程」:在ViewRootImpl中,存在一个名为InputStage的责任链,用于处理输入事件,每个阶段都可能对事件进行处理或将其传递给下一个阶段。
/frameworks/base/core/java/android/view/ViewRootImpl.java
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.
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 {
if (performFocusNavigation(event)) {
return FINISH_HANDLED;
}
}
}
return FORWARD;
}
- 在处理按键事件之前,首先会调用mUnhandledKeyManager.preViewDispatch(event)来判断是否有未处理的按键事件。如果有未处理的事件,则直接返回FINISH_HANDLED表示该事件已被处理。
- 如果没有未处理的事件,则调用mView.dispatchKeyEvent(event)将按键事件分发给View层次结构。如果View层次结构中的任何一个View处理了该事件,则直接返回FINISH_HANDLED表示该事件已被处理。
- 如果按键事件不能被处理或应该被丢弃,则调用shouldDropInputEvent(q)方法来决定是否应该丢弃该事件。如果应该丢弃,则返回FINISH_NOT_HANDLED表示该事件未被处理;否则继续处理该事件。
- 如果事件仍未被处理,则调用mUnhandledKeyManager.dispatch(mView, event)来判断是否有未处理的事件。如果有,则直接返回FINISH_HANDLED表示该事件已被处理。
- 如果按键事件是一个特定的按键(如Tab键)并且同时满足一些特定的条件,则设置groupNavigationDirection变量并调用performKeyboardGroupNavigation方法来处理自动的聚焦变化。
- 如果按键事件是一个快捷键(即同时按下一个或多个修饰键和另一个键),则调用mView.dispatchKeyShortcutEvent(event)将事件分发给View层次结构来尝试解释该按键事件。如果该事件被处理,则返回FINISH_HANDLED表示该事件已被处理。
- 如果事件仍未被处理,则调用mFallbackEventHandler.dispatchKeyEvent(event)应用回退事件策略来尝试处理该事件。如果该事件被处理,则返回FINISH_HANDLED表示该事件已被处理。
- 如果事件仍未被处理,则调用performFocusNavigation方法来处理自动的聚焦变化。如果该事件被处理,则返回FINISH_HANDLED表示该事件已被处理。
- 最后,如果事件仍未被处理,则返回FORWARD表示该事件应该被传递给下一个事件处理程序。
private boolean performFocusNavigation(KeyEvent event) {
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)) {
Log.i(TAG, "v.requestFocus == true");
boolean isFastScrolling = event.getRepeatCount() > 0;
playSoundEffect(SoundEffectConstants.getConstantForFocusDirection(direction,isFastScrolling));
return true;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return true;
}
} else {
if (mView.restoreDefaultFocus()) {
return true;
}
}
}
return false;
}
该方法用于处理焦点导航相关的按键事件,例如方向键和Tab键等。该方法接收一个KeyEvent对象作为参数,根据不同的按键码和修饰符,计算出焦点导航的方向,然后尝试在View树中找到新的焦点,并将其设置为当前焦点。如果找到新的焦点并成功设置为当前焦点,则播放焦点变化时的声音效果,并返回true表示焦点变化事件已被处理。如果没有找到新的焦点,或者新的焦点不接受焦点设置请求,则返回false表示该事件未被处理。
/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;
}
}
}
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;
}
}
boolean result = isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event) : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
return result;
}
该方法用于处理按键事件的分发和处理,该方法接收一个KeyEvent对象,并提取出键码和事件类型。如果该事件为按下事件并且重复次数为0,则执行以下操作:
- 首先,处理面板按键的弹奏:如果面板按键已按下但未释放,则尝试在其中执行一个快捷方式。如果已处理该快捷方式,则返回true表示事件已处理。
- 然后,如果面板已经打开,则在没有面板按键的情况下执行其上的快捷方式。如果已处理该快捷方式,则返回true表示事件已处理。
- 如果事件仍未被处理,则检查Window对象是否已被销毁,如果没有,则获取Window.Callback对象并将事件分派给它。如果已处理该事件,则返回true表示事件已处理。
- 最后,如果事件仍未被处理,则将其传递给Window对象进行处理,并返回该事件是否为按下事件的结果。
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;
}
}
if (super.dispatchKeyEvent(event)) {
return true;
}
boolean handle = (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event);
return handle;
}
其作用是对KeyEvent事件进行分发处理。该方法首先判断事件是否为返回键,如果是则优先处理当前的操作模式,如果存在操作模式则先结束操作模式,否则将事件交由父类ViewGroup进行处理。如果父类能够处理该事件,则返回true,否则返回false,并将事件交由该DecorView 所对应的ViewRootImpl实例进行处理。
其中,getViewRootImpl()方法返回当前DecorView所在的ViewRootImpl实例。如果该实例存在,则调用dispatchUnhandledKeyEvent(event)方法进行处理,否则返回false。dispatchUnhandledKeyEvent(event)方法用于将该事件交由输入法进行处理。
- 「View的事件处理」:
- 「ViewGroup与View」:在Android中,ViewGroup是View的容器,负责分发事件给其子View。如果ViewGroup的dispatchTouchEvent方法返回true,事件被消费,不会传递给其onTouchEvent方法或子View。如果返回false,调用父控件的onTouchEvent方法或继续传递给子View。
- 「事件拦截」:ViewGroup有一个onInterceptTouchEvent方法,可以在事件传递给子View之前进行拦截。如果该方法返回true,则事件被拦截并在当前ViewGroup的onTouchEvent方法中处理。
- 「事件消费」:无论是ViewGroup还是View,如果onTouchEvent方法返回true,表示事件被消费,不再传递给其他View或父控件。
/frameworks/base/core/java/android/app/Activity.java
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();
if (win.superDispatchKeyEvent(event)) {
return true;
}
View decor = mDecor;
if (decor == null) decor = win.getDecorView();
boolean handler = event.dispatch(this, decor != null ? decor.getKeyDispatcherState() : null, this);
return handler;
}
该方法用于分发键事件,当用户按下或释放某个按键时,该方法将被调用。
- 首先,方法调用onUserInteraction(),用于通知Activity用户正在与应用程序交互。
- 接着,方法检查事件是否为菜单键事件,如果是,则首先检查 ActionBar 是否存在,并且将事件传递给 ActionBar 的 onMenuKeyEvent() 方法进行处理。如果 ActionBar 成功处理该事件,则直接返回 true,表示事件已被处理。
- 如果事件不是菜单键事件或者ActionBar无法处理该事件,则将事件传递给窗口处理,并返回窗口处理结果。如果窗口处理了该事件,则直接返回true,表示事件已被处理。
- 如果事件既不是菜单键事件,也无法被窗口处理,则将事件分发给DecorView(该Activity 的根View),并返回DecorView的dispatchKeyEvent()方法的处理结果。
/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public boolean superDispatchKeyEvent(KeyEvent event) {
return mDecor.superDispatchKeyEvent(event);
}
/frameworks/base/core/java/android/view/ViewGroup.java
@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)) {
Log.e("ViewRootImpl","super.dispatchKeyEvent(event)");
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS) == PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
Log.e("ViewRootImpl","Focused.dispatchKeyEvent(event)");
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}
return false;
}
用于将按键事件分派到该ViewGroup及其子View。该方法首先通过检查ViewGroup本身的状态(是否拥有焦点且有边界)来决定是否自己处理KeyEvent,如果ViewGroup本身满足条件,则通过调用父类的dispatchKeyEvent方法处理事件,并返回true表示已处理。如果ViewGroup本身不满足条件,则将KeyEvent分派到当前拥有焦点的子View,如果该子View处理了事件,则返回true表示已处理。如果KeyEvent最终未被处理,则返回false表示未处理。此方法还包含一些调试代码,用于确保事件派发的一致性。
/frameworks/base/core/java/android/view/View.java
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)) {
Log.e("ViewRootImpl","mOnKeyListener.onKey");
return true;
}
if (event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this)) {
Log.e("ViewRootImpl","event.dispatch");
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
该方法用于分派键盘事件到对应的视图,并根据事件的处理结果返回一个布尔值。方法的参数event表示一个键盘事件,该事件将被分派到相应的视图。方法中的第一步是调用mInputEventConsistencyVerifier.onKeyEvent(event, 0)方法来记录键盘事件的一些基本信息,用于后续的事件一致性检查。然后会首先调用视图的OnKeyListener对象(如果有的话)的onKey 方法,如果该方法返回true,则表示该键盘事件被该监听器处理,方法返回true,否则该键盘事件被传递给了该视图的dispatch方法。在这里调用了event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this)方法,该方法会根据键盘事件的类型,将其分发给视图的onKeyDown或onKeyUp方法进行处理。如果该事件被处理了,则返回true,否则会调用mInputEventConsistencyVerifier.onUnhandledEvent(event, 0)方法记录该事件未被处理的信息,并返回false表示该事件未被处理。
/frameworks/base/core/java/android/view/KeyEvent.java
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;
}
KeyEvent事件分发的核心代码,主要处理按键事件的分发。当一个按键事件被分发到一个View时,该View首先尝试处理该事件。如果该View无法处理该事件,则事件将被分发给它的parent View或Activity,直到事件被处理或到达了View层级的最顶层。
- 该方法根据KeyEvent的不同Action,分别进行处理。如果Action是ACTION_DOWN,即按下按键的事件,首先会调用Callback接口的onKeyDown方法来处理该事件,并根据事件处理的结果进行相应的处理。如果返回true,表示事件被成功处理,并且设置了FLAG_START_TRACKING标志位,则会调用DispatcherState的startTracking方法来开始追踪该事件。如果事件是长按事件,且当前正在追踪该事件,则会调用Callback接口的onKeyLongPress方法来处理长按事件。
- 如果Action是ACTION_UP,即松开按键的事件,会调用Callback接口的onKeyUp方法来处理该事件,并根据DispatcherState是否存在,调用DispatcherState的handleUpEvent方法来结束追踪该事件。
- 如果Action是ACTION_MULTIPLE,即按键事件包含多个重复事件,会调用Callback接口的onKeyMultiple方法来处理该事件,并根据KeyEvent的repeatCount和keyCode信息,依次调用Callback接口的onKeyDown和onKeyUp方法来处理每一个重复事件,最后返回处理结果。
总的来说,KeyEvent类的dispatch方法实现了按键事件的分发和处理,为Android应用程序提供了丰富的按键事件处理能力。
- 「特殊情况」:
- 「自定义处理」:可以通过重写Activity、ViewGroup或View的dispatchKeyEvent、onInterceptTouchEvent、onTouchEvent等方法来自定义按键事件的处理逻辑。
- 「外围设备」:对于蓝牙键盘、USB键盘等外围设备,Android通过KeyEvent事件机制与它们进行交互。开发者可以通过重写dispatchKeyEvent方法来截获和处理这些设备发送的事件。
Key事件的分发逻辑是一个复杂但有序的过程,涉及事件的产生、分发和View的事件处理等多个阶段。通过理解这个过程,可以更好地控制Android应用中按键事件的行为。