Add "KeyFallback" handling ability to Views

This gives any view the ability to receive unhandled KeyEvents.
The order of Views receiving fallback key events is inverse
drawing order: this means higher views will receive fallback
events first.

FallbackHandlers can be added to any view via
addKeyFallbackListener. Within a view, listeners are tapped
in reverse order (such that more-recently added listeners will
receive the event first).

Bug: 32722450
Test: Added a CTS test ViewTest#testKeyFallback
Change-Id: Ibfff4db70de8fb98db0035e5aeb09271be1574c6
This commit is contained in:
Evan Rosky
2017-06-02 17:31:22 -07:00
parent 181799bc53
commit 5e29c076cb
6 changed files with 215 additions and 1 deletions

View File

@@ -45931,6 +45931,7 @@ package android.view {
method public void addExtraDataToAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo, java.lang.String, android.os.Bundle);
method public void addFocusables(java.util.ArrayList<android.view.View>, int);
method public void addFocusables(java.util.ArrayList<android.view.View>, int, int);
method public void addKeyFallbackListener(android.view.View.OnKeyFallbackListener);
method public void addKeyboardNavigationClusters(java.util.Collection<android.view.View>, int);
method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
@@ -46265,6 +46266,7 @@ package android.view {
method public void onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
method public void onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo);
method public boolean onKeyDown(int, android.view.KeyEvent);
method public boolean onKeyFallback(android.view.KeyEvent);
method public boolean onKeyLongPress(int, android.view.KeyEvent);
method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
method public boolean onKeyPreIme(int, android.view.KeyEvent);
@@ -46318,6 +46320,7 @@ package android.view {
method public void refreshDrawableState();
method public void releasePointerCapture();
method public boolean removeCallbacks(java.lang.Runnable);
method public void removeKeyFallbackListener(android.view.View.OnKeyFallbackListener);
method public void removeOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
method public void removeOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
method public void requestApplyInsets();
@@ -46736,6 +46739,10 @@ package android.view {
method public abstract boolean onHover(android.view.View, android.view.MotionEvent);
}
public static abstract interface View.OnKeyFallbackListener {
method public abstract boolean onKeyFallback(android.view.View, android.view.KeyEvent);
}
public static abstract interface View.OnKeyListener {
method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
}

View File

@@ -49665,6 +49665,7 @@ package android.view {
method public void addExtraDataToAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo, java.lang.String, android.os.Bundle);
method public void addFocusables(java.util.ArrayList<android.view.View>, int);
method public void addFocusables(java.util.ArrayList<android.view.View>, int, int);
method public void addKeyFallbackListener(android.view.View.OnKeyFallbackListener);
method public void addKeyboardNavigationClusters(java.util.Collection<android.view.View>, int);
method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
@@ -49999,6 +50000,7 @@ package android.view {
method public void onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
method public void onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo);
method public boolean onKeyDown(int, android.view.KeyEvent);
method public boolean onKeyFallback(android.view.KeyEvent);
method public boolean onKeyLongPress(int, android.view.KeyEvent);
method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
method public boolean onKeyPreIme(int, android.view.KeyEvent);
@@ -50052,6 +50054,7 @@ package android.view {
method public void refreshDrawableState();
method public void releasePointerCapture();
method public boolean removeCallbacks(java.lang.Runnable);
method public void removeKeyFallbackListener(android.view.View.OnKeyFallbackListener);
method public void removeOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
method public void removeOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
method public void requestApplyInsets();
@@ -50470,6 +50473,10 @@ package android.view {
method public abstract boolean onHover(android.view.View, android.view.MotionEvent);
}
public static abstract interface View.OnKeyFallbackListener {
method public abstract boolean onKeyFallback(android.view.View, android.view.KeyEvent);
}
public static abstract interface View.OnKeyListener {
method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
}

View File

@@ -46552,6 +46552,7 @@ package android.view {
method public void addExtraDataToAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo, java.lang.String, android.os.Bundle);
method public void addFocusables(java.util.ArrayList<android.view.View>, int);
method public void addFocusables(java.util.ArrayList<android.view.View>, int, int);
method public void addKeyFallbackListener(android.view.View.OnKeyFallbackListener);
method public void addKeyboardNavigationClusters(java.util.Collection<android.view.View>, int);
method public void addOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
method public void addOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
@@ -46888,6 +46889,7 @@ package android.view {
method public void onInitializeAccessibilityEvent(android.view.accessibility.AccessibilityEvent);
method public void onInitializeAccessibilityNodeInfo(android.view.accessibility.AccessibilityNodeInfo);
method public boolean onKeyDown(int, android.view.KeyEvent);
method public boolean onKeyFallback(android.view.KeyEvent);
method public boolean onKeyLongPress(int, android.view.KeyEvent);
method public boolean onKeyMultiple(int, int, android.view.KeyEvent);
method public boolean onKeyPreIme(int, android.view.KeyEvent);
@@ -46941,6 +46943,7 @@ package android.view {
method public void refreshDrawableState();
method public void releasePointerCapture();
method public boolean removeCallbacks(java.lang.Runnable);
method public void removeKeyFallbackListener(android.view.View.OnKeyFallbackListener);
method public void removeOnAttachStateChangeListener(android.view.View.OnAttachStateChangeListener);
method public void removeOnLayoutChangeListener(android.view.View.OnLayoutChangeListener);
method public void requestApplyInsets();
@@ -47363,6 +47366,10 @@ package android.view {
method public abstract boolean onHover(android.view.View, android.view.MotionEvent);
}
public static abstract interface View.OnKeyFallbackListener {
method public abstract boolean onKeyFallback(android.view.View, android.view.KeyEvent);
}
public static abstract interface View.OnKeyListener {
method public abstract boolean onKey(android.view.View, int, android.view.KeyEvent);
}

View File

@@ -4245,6 +4245,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
OnApplyWindowInsetsListener mOnApplyWindowInsetsListener;
OnCapturedPointerListener mOnCapturedPointerListener;
private ArrayList<OnKeyFallbackListener> mKeyFallbackListeners;
}
ListenerInfo mListenerInfo;
@@ -25171,6 +25173,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
boolean onKey(View v, int keyCode, KeyEvent event);
}
/**
* Interface definition for a callback to be invoked when a hardware key event is
* dispatched to this view during the fallback phase. This means no view in the hierarchy
* has handled this event.
*/
public interface OnKeyFallbackListener {
/**
* Called when a hardware key is dispatched to a view in the fallback phase. This allows
* listeners to respond to events after the view hierarchy has had a chance to respond.
* <p>Key presses in software keyboards will generally NOT trigger this method,
* although some may elect to do so in some situations. Do not assume a
* software input method has to be key-based; even if it is, it may use key presses
* in a different way than you expect, so there is no way to reliably catch soft
* input key presses.
*
* @param v The view the key has been dispatched to.
* @param event The KeyEvent object containing full information about
* the event.
* @return True if the listener has consumed the event, false otherwise.
*/
boolean onKeyFallback(View v, KeyEvent event);
}
/**
* Interface definition for a callback to be invoked when a touch event is
* dispatched to this view. The callback will be invoked before the touch
@@ -26844,4 +26869,56 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
return mTooltipInfo.mTooltipPopup.getContentView();
}
/**
* Allows this view to handle {@link KeyEvent}s which weren't handled by normal dispatch. This
* occurs after the normal view hierarchy dispatch, but before the window callback. By default,
* this will dispatch into all the listeners registered via
* {@link #addKeyFallbackListener(OnKeyFallbackListener)} in last-in-first-out order (most
* recently added will receive events first).
*
* @param event A not-previously-handled event.
* @return {@code true} if the event was handled, {@code false} otherwise.
* @see #addKeyFallbackListener
*/
public boolean onKeyFallback(@NonNull KeyEvent event) {
if (mListenerInfo != null && mListenerInfo.mKeyFallbackListeners != null) {
for (int i = mListenerInfo.mKeyFallbackListeners.size() - 1; i >= 0; --i) {
if (mListenerInfo.mKeyFallbackListeners.get(i).onKeyFallback(this, event)) {
return true;
}
}
}
return false;
}
/**
* Adds a listener which will receive unhandled {@link KeyEvent}s.
* @param listener the receiver of fallback {@link KeyEvent}s.
* @see #onKeyFallback(KeyEvent)
*/
public void addKeyFallbackListener(OnKeyFallbackListener listener) {
ArrayList<OnKeyFallbackListener> fallbacks = getListenerInfo().mKeyFallbackListeners;
if (fallbacks == null) {
fallbacks = new ArrayList<>();
getListenerInfo().mKeyFallbackListeners = fallbacks;
}
fallbacks.add(listener);
}
/**
* Removes a listener which will receive unhandled {@link KeyEvent}s.
* @param listener the receiver of fallback {@link KeyEvent}s.
* @see #onKeyFallback(KeyEvent)
*/
public void removeKeyFallbackListener(OnKeyFallbackListener listener) {
if (mListenerInfo != null) {
if (mListenerInfo.mKeyFallbackListeners != null) {
mListenerInfo.mKeyFallbackListeners.remove(listener);
if (mListenerInfo.mKeyFallbackListeners.isEmpty()) {
mListenerInfo.mKeyFallbackListeners = null;
}
}
}
}
}

View File

@@ -73,6 +73,7 @@ import android.util.Log;
import android.util.MergedConfiguration;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.TimeUtils;
import android.util.TypedValue;
import android.view.Surface.OutOfResourcesException;
@@ -362,6 +363,8 @@ public final class ViewRootImpl implements ViewParent,
InputStage mFirstPostImeInputStage;
InputStage mSyntheticInputStage;
private final KeyFallbackManager mKeyFallbackManager = new KeyFallbackManager();
boolean mWindowAttributesChanged = false;
int mWindowAttributesChangesFlag = 0;
@@ -4764,6 +4767,13 @@ public final class ViewRootImpl implements ViewParent,
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
mKeyFallbackManager.mDispatched = false;
if (mKeyFallbackManager.hasFocus()
&& mKeyFallbackManager.dispatchUnique(mView, event)) {
return FINISH_HANDLED;
}
// Deliver the key to the view hierarchy.
if (mView.dispatchKeyEvent(event)) {
return FINISH_HANDLED;
@@ -4773,6 +4783,10 @@ public final class ViewRootImpl implements ViewParent,
return FINISH_NOT_HANDLED;
}
if (mKeyFallbackManager.dispatchUnique(mView, event)) {
return FINISH_HANDLED;
}
int groupNavigationDirection = 0;
if (event.getAction() == KeyEvent.ACTION_DOWN
@@ -7529,6 +7543,16 @@ public final class ViewRootImpl implements ViewParent,
}
}
/**
* Dispatches a KeyEvent to all registered key fallback handlers.
*
* @param event
* @return {@code true} if the event was handled, {@code false} otherwise.
*/
public boolean dispatchKeyFallbackEvent(KeyEvent event) {
return mKeyFallbackManager.dispatch(mView, event);
}
class TakenSurfaceHolder extends BaseSurfaceHolder {
@Override
public boolean onAllowLockCanvas() {
@@ -8093,4 +8117,92 @@ public final class ViewRootImpl implements ViewParent,
run();
}
}
private static class KeyFallbackManager {
// This is used to ensure that key-fallback events are only dispatched once. We attempt
// to dispatch more than once in order to achieve a certain order. Specifically, if we
// are in an Activity or Dialog (and have a Window.Callback), the keyfallback events should
// be dispatched after the view hierarchy, but before the Activity. However, if we aren't
// in an activity, we still want key fallbacks to be dispatched.
boolean mDispatched = false;
SparseBooleanArray mCapturedKeys = new SparseBooleanArray();
WeakReference<View> mFallbackReceiver = null;
int mVisitCount = 0;
private void updateCaptureState(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
mCapturedKeys.append(event.getKeyCode(), true);
}
if (event.getAction() == KeyEvent.ACTION_UP) {
mCapturedKeys.delete(event.getKeyCode());
}
}
boolean dispatch(View root, KeyEvent event) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "KeyFallback dispatch");
mDispatched = true;
updateCaptureState(event);
if (mFallbackReceiver != null) {
View target = mFallbackReceiver.get();
if (mCapturedKeys.size() == 0) {
mFallbackReceiver = null;
}
if (target != null && target.isAttachedToWindow()) {
return target.onKeyFallback(event);
}
// consume anyways so that we don't feed uncaptured key events to other views
return true;
}
boolean result = dispatchInZOrder(root, event);
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
return result;
}
private boolean dispatchInZOrder(View view, KeyEvent evt) {
if (view instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) view;
ArrayList<View> orderedViews = vg.buildOrderedChildList();
if (orderedViews != null) {
try {
for (int i = orderedViews.size() - 1; i >= 0; --i) {
View v = orderedViews.get(i);
if (dispatchInZOrder(v, evt)) {
return true;
}
}
} finally {
orderedViews.clear();
}
} else {
for (int i = vg.getChildCount() - 1; i >= 0; --i) {
View v = vg.getChildAt(i);
if (dispatchInZOrder(v, evt)) {
return true;
}
}
}
}
if (view.onKeyFallback(evt)) {
mFallbackReceiver = new WeakReference<>(view);
return true;
}
return false;
}
boolean hasFocus() {
return mFallbackReceiver != null;
}
boolean dispatchUnique(View root, KeyEvent event) {
if (mDispatched) {
return false;
}
return dispatch(root, event);
}
}
}

View File

@@ -431,7 +431,11 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
}
return super.dispatchKeyEvent(event);
if (super.dispatchKeyEvent(event)) {
return true;
}
return (getViewRootImpl() != null) && getViewRootImpl().dispatchKeyFallbackEvent(event);
}
public boolean superDispatchKeyShortcutEvent(KeyEvent event) {