- "Event generation": When the user presses a physical button, uses a remote control, input method, or other USB-OTG external keyboard and other devices, a key event (KeyEvent) is generated.
- 「Distribution of events」:
- "Distribution starting point": Key events are received by the Android system and distributed to PhoneWindowManager (system process) and ViewRootImpl (application process) through the Linux layer.
- 「Distribution order」: PhoneWindowManager is executed first to process system-level key events (such as volume keys, power keys, etc.), and ViewRootImpl is executed later to process application-level key events (such as arrow keys, enter keys, etc.).
- "Distribution process": In ViewRootImpl, there is a responsibility chain called InputStage for handling input events. Each stage may process the event or pass it to the next stage.
/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; } - Before processing a key event, mUnhandledKeyManager.preViewDispatch(event) is called first to determine whether there is an unhandled key event. If there is an unhandled event, FINISH_HANDLED is returned directly to indicate that the event has been processed.
- If there is no unprocessed event, call mView.dispatchKeyEvent(event) to distribute the key event to the View hierarchy. If any View in the View hierarchy handles the event, return FINISH_HANDLED directly to indicate that the event has been handled.
- If the key event cannot be processed or should be discarded, call the shouldDropInputEvent(q) method to determine whether the event should be discarded. If it should be discarded, return FINISH_NOT_HANDLED to indicate that the event is not processed; otherwise, continue to process the event.
- If the event has not been processed yet, call mUnhandledKeyManager.dispatch(mView, event) to determine whether there is an unprocessed event. If so, return FINISH_HANDLED directly to indicate that the event has been processed.
- If the key event is a specific key (such as the Tab key) and meets some specific conditions, the groupNavigationDirection variable is set and the performKeyboardGroupNavigation method is called to handle the automatic focus change.
- If the key event is a shortcut key (that is, one or more modifier keys and another key are pressed at the same time), call mView.dispatchKeyShortcutEvent(event) to distribute the event to the View hierarchy to try to interpret the key event. If the event is handled, FINISH_HANDLED is returned to indicate that the event has been handled.
- If the event has not been processed, call mFallbackEventHandler.dispatchKeyEvent(event) to apply the fallback event strategy to try to process the event. If the event is processed, return FINISH_HANDLED to indicate that the event has been processed.
- If the event has not been processed, the performFocusNavigation method is called to process the automatic focus change. If the event is processed, FINISH_HANDLED is returned to indicate that the event has been processed.
- Finally, if the event has not yet been handled, FORWARD is returned to indicate that the event should be passed to the next event handler.
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; } This method is used to handle key events related to focus navigation, such as arrow keys and Tab keys. This method receives a KeyEvent object as a parameter, calculates the direction of focus navigation based on different key codes and modifiers, and then tries to find a new focus in the View tree and set it as the current focus. If the new focus is found and successfully set as the current focus, the sound effect of the focus change is played, and true is returned to indicate that the focus change event has been processed. If the new focus is not found, or the new focus does not accept the focus setting request, false is returned to indicate that the event has not been processed. /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; } This method is used to handle the distribution and processing of key events. This method receives a KeyEvent object and extracts the key code and event type. If the event is a press event and the number of repetitions is 0, the following operations are performed: - First, the panel key is processed: if the panel key is pressed but not released, a shortcut is attempted to be executed in it. If the shortcut is handled, true is returned to indicate that the event has been handled.
- Then, if the panel is already open, the shortcut on it is executed without the panel key. If the shortcut is already handled, true is returned to indicate that the event was handled.
- If the event is still not handled, check whether the Window object has been destroyed. If not, get the Window.Callback object and dispatch the event to it. If the event has been handled, return true to indicate that the event has been handled.
- Finally, if the event is still unhandled, it is passed to the Window object for processing and returns whether the event was the result of a press 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; } } if (super.dispatchKeyEvent(event)) { return true; } boolean handle = (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event); return handle; } Its function is to distribute and process KeyEvent events. This method first determines whether the event is a return key. If it is, it will give priority to the current operation mode. If there is an operation mode, it will end the operation mode first. Otherwise, the event will be handed over to the parent class ViewGroup for processing. If the parent class can handle the event, it returns true, otherwise it returns false, and the event will be handed over to the ViewRootImpl instance corresponding to the DecorView for processing. The getViewRootImpl() method returns the ViewRootImpl instance where the current DecorView is located. If the instance exists, the dispatchUnhandledKeyEvent(event) method is called for processing, otherwise false is returned. The dispatchUnhandledKeyEvent(event) method is used to hand over the event to the input method for processing. - 「View event handling」:
- 「ViewGroup and View」: In Android, ViewGroup is a container of View, responsible for distributing events to its child Views. If the dispatchTouchEvent method of ViewGroup returns true, the event is consumed and will not be passed to its onTouchEvent method or child View. If it returns false, the onTouchEvent method of the parent control is called or it continues to be passed to the child View.
- 「Event interception」: ViewGroup has an onInterceptTouchEvent method that can intercept the event before it is passed to the child View. If the method returns true, the event is intercepted and processed in the onTouchEvent method of the current ViewGroup.
- "Event consumption": Whether it is ViewGroup or View, if the onTouchEvent method returns true, it means that the event is consumed and is no longer passed to other Views or parent controls.
/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; } This method is used to distribute key events. When the user presses or releases a key, this method will be called. - First, the method calls onUserInteraction(), which notifies the Activity that the user is interacting with the application.
- Next, the method checks whether the event is a menu key event. If so, it first checks whether the ActionBar exists and passes the event to the onMenuKeyEvent() method of the ActionBar for processing. If the ActionBar successfully handles the event, it directly returns true, indicating that the event has been processed.
- If the event is not a menu key event or the ActionBar cannot handle the event, the event is passed to the window for processing and the window processing result is returned. If the window handles the event, true is returned directly, indicating that the event has been processed.
- If the event is neither a menu key event nor can be processed by the window, the event is dispatched to DecorView (the root View of the Activity) and the processing result of the dispatchKeyEvent() method of DecorView is returned.
/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; } Used to dispatch key events to the ViewGroup and its child Views. This method first decides whether to handle the KeyEvent by checking the state of the ViewGroup itself (whether it has focus and has boundaries). If the ViewGroup itself meets the conditions, it handles the event by calling the dispatchKeyEvent method of the parent class and returns true to indicate that it has been handled. If the ViewGroup itself does not meet the conditions, the KeyEvent is dispatched to the child View that currently has focus. If the child View handles the event, true is returned to indicate that it has been handled. If the KeyEvent is ultimately not handled, false is returned to indicate that it has not been handled. This method also contains some debugging code to ensure the consistency of event dispatch. /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; } This method is used to dispatch keyboard events to the corresponding view and return a Boolean value based on the result of the event processing. The method parameter event represents a keyboard event, which will be dispatched to the corresponding view. The first step in the method is to call the mInputEventConsistencyVerifier.onKeyEvent(event, 0) method to record some basic information of the keyboard event for subsequent event consistency checks. Then the onKey method of the view's OnKeyListener object (if any) will be called first. If the method returns true, it means that the keyboard event is processed by the listener and the method returns true. Otherwise, the keyboard event is passed to the dispatch method of the view. Here, the event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this) method is called. This method will dispatch the keyboard event to the view's onKeyDown or onKeyUp method for processing based on the type of the keyboard event. If the event is handled, it returns true, otherwise it calls the mInputEventConsistencyVerifier.onUnhandledEvent(event, 0) method to record the information that the event is not handled, and returns false to indicate that the event is not handled. /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; } The core code of KeyEvent event distribution mainly handles the distribution of key events. When a key event is distributed to a View, the View first tries to handle the event. If the View cannot handle the event, the event will be distributed to its parent View or Activity until the event is handled or reaches the top level of the View hierarchy. - This method processes the different actions of KeyEvent separately. If the Action is ACTION_DOWN, that is, the event of pressing a key, the onKeyDown method of the Callback interface will be called first to handle the event, and the corresponding processing will be performed according to the result of the event processing. If true is returned, it means that the event is successfully processed and the FLAG_START_TRACKING flag is set, then the startTracking method of DispatcherState will be called to start tracking the event. If the event is a long press event and the event is currently being tracked, the onKeyLongPress method of the Callback interface will be called to handle the long press event.
- If the Action is ACTION_UP, that is, the event of releasing the key, the onKeyUp method of the Callback interface will be called to handle the event, and depending on whether DispatcherState exists, the handleUpEvent method of DispatcherState will be called to end tracking the event.
- If Action is ACTION_MULTIPLE, that is, the key event contains multiple repeated events, the onKeyMultiple method of the Callback interface will be called to handle the event, and according to the repeatCount and keyCode information of KeyEvent, the onKeyDown and onKeyUp methods of the Callback interface will be called in sequence to handle each repeated event, and finally the processing result will be returned.
In general, the dispatch method of the KeyEvent class implements the distribution and processing of key events, providing Android applications with rich key event processing capabilities. - Special circumstances:
- 「Custom processing」: You can customize the processing logic of key events by overriding dispatchKeyEvent, onInterceptTouchEvent, onTouchEvent and other methods of Activity, ViewGroup or View.
- 「Peripheral devices」: Android interacts with peripheral devices such as Bluetooth keyboards and USB keyboards through the KeyEvent event mechanism. Developers can intercept and process events sent by these devices by overriding the dispatchKeyEvent method.
The distribution logic of key events is a complex but orderly process, involving multiple stages such as event generation, distribution, and View event processing. By understanding this process, you can better control the behavior of key events in Android applications. |