From 5d0197890b9fd0d0310b3de7a042eedcef8f1d09 Mon Sep 17 00:00:00 2001 From: Arthur Hung Date: Wed, 6 Nov 2019 19:42:44 +0800 Subject: [PATCH] Allow unbuffered MotionEvent dispatch for View (1/2) MotionEvents sent from InputDispather would be buffered and dispatched align the vsync by default. And it would provides many of benifits. But for a high quality gaming experience, low latency input is critical when we use analogs inputs (e.g mouse or joystick, etc.). So It's important for gaming applications to process these input events in a raw way, without them being coalesced on each frame. - Add new api View.requestUnbufferedDispatch(source) to control which input source classes could be unbuffered while handled by the view. Bug: 135740001 Bug: 136277595 Test: atest ViewUnbufferedTest Change-Id: If65ed1906f59947dcd1e5062519b643a17d0e8e5 --- api/current.txt | 1 + core/java/android/view/InputDevice.java | 16 +++++++++ core/java/android/view/View.java | 41 +++++++++++++++++++++++- core/java/android/view/ViewGroup.java | 26 +++++++++++++++ core/java/android/view/ViewParent.java | 14 ++++++++ core/java/android/view/ViewRootImpl.java | 30 +++++++++++++++-- 6 files changed, 125 insertions(+), 3 deletions(-) diff --git a/api/current.txt b/api/current.txt index ac42e66d54f57..15aab8243f21e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -53891,6 +53891,7 @@ package android.view { method public boolean requestRectangleOnScreen(android.graphics.Rect); method public boolean requestRectangleOnScreen(android.graphics.Rect, boolean); method public final void requestUnbufferedDispatch(android.view.MotionEvent); + method public final void requestUnbufferedDispatch(int); method @NonNull public final T requireViewById(@IdRes int); method public void resetPivot(); method public static int resolveSize(int, int); diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 360deddf544dc..58e5b2dfaa375 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -16,6 +16,7 @@ package android.view; +import android.annotation.IntDef; import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; @@ -28,6 +29,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.Vibrator; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -138,6 +141,19 @@ public final class InputDevice implements Parcelable { */ public static final int SOURCE_CLASS_JOYSTICK = 0x00000010; + /** @hide */ + @IntDef(flag = true, prefix = { "SOURCE_CLASS_" }, value = { + SOURCE_CLASS_NONE, + SOURCE_CLASS_BUTTON, + SOURCE_CLASS_POINTER, + SOURCE_CLASS_POINTER, + SOURCE_CLASS_TRACKBALL, + SOURCE_CLASS_POSITION, + SOURCE_CLASS_JOYSTICK + }) + @Retention(RetentionPolicy.SOURCE) + @interface InputSourceClass {} + /** * The input source is unknown. */ diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index b9be33ca0f59f..4c7307ee5b8c0 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -110,6 +110,7 @@ import android.view.AccessibilityIterators.ParagraphTextSegmentIterator; import android.view.AccessibilityIterators.TextSegmentIterator; import android.view.AccessibilityIterators.WordTextSegmentIterator; import android.view.ContextMenu.ContextMenuInfo; +import android.view.InputDevice.InputSourceClass; import android.view.Window.OnContentApplyWindowInsetsListener; import android.view.WindowInsets.Type; import android.view.WindowInsetsAnimation.Bounds; @@ -5204,6 +5205,14 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @StyleRes private int mExplicitStyle; + /** + * Specifies which input source classes should provide unbuffered input events to this view + * + * @see View#requestUnbufferedDispatch(int) + */ + @InputSourceClass + int mUnbufferedInputSource = InputDevice.SOURCE_CLASS_NONE; + /** * Simple constructor to use when creating a view from code. * @@ -8013,6 +8022,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo.mKeyDispatchState.reset(this); } + if (mParent != null) { + mParent.onDescendantUnbufferedRequested(); + } + notifyEnterOrExitForAutoFillIfNeeded(gainFocus); } @@ -15820,12 +15833,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * system not batch {@link MotionEvent}s but instead deliver them as soon as they're * available. This method should only be called for touch events. * - *

This api is not intended for most applications. Buffered dispatch + *

This API is not intended for most applications. Buffered dispatch * provides many of benefits, and just requesting unbuffered dispatch on most MotionEvent * streams will not improve your input latency. Side effects include: increased latency, * jittery scrolls and inability to take advantage of system resampling. Talk to your input * professional to see if {@link #requestUnbufferedDispatch(MotionEvent)} is right for * you.

+ * + * To receive unbuffered events for arbitrary input device source classes, use + * {@link #requestUnbufferedDispatch(int)}, + * + * @see View#requestUnbufferedDispatch(int) */ public final void requestUnbufferedDispatch(MotionEvent event) { final int action = event.getAction(); @@ -15837,6 +15855,27 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo.mUnbufferedDispatchRequested = true; } + /** + * Request unbuffered dispatch of the given event source class to this view. + * This is similar to {@link View#requestUnbufferedDispatch(MotionEvent)}, but does not + * automatically terminate, and allows the specification of arbitrary input source classes. + * + * @param source The combined input source class to request unbuffered dispatch for. All + * events coming from these source classes will not be buffered. Set to + * {@link InputDevice#SOURCE_CLASS_NONE} in order to return to default behaviour. + * + * @see View#requestUnbufferedDispatch(MotionEvent) + */ + public final void requestUnbufferedDispatch(@InputSourceClass int source) { + if (mUnbufferedInputSource == source) { + return; + } + mUnbufferedInputSource = source; + if (mParent != null) { + mParent.onDescendantUnbufferedRequested(); + } + } + private boolean hasSize() { return (mBottom > mTop) && (mRight > mLeft); } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 0367536919cf4..d0086928e1b5b 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -9004,4 +9004,30 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager getChildAt(i).encode(encoder); } } + + /** @hide */ + @Override + public final void onDescendantUnbufferedRequested() { + // First look at the focused child for focused events + int focusedChildNonPointerSource = InputDevice.SOURCE_CLASS_NONE; + if (mFocused != null) { + focusedChildNonPointerSource = mFocused.mUnbufferedInputSource + & (~InputDevice.SOURCE_CLASS_POINTER); + } + mUnbufferedInputSource = focusedChildNonPointerSource; + + // Request unbuffered dispatch for pointer events for this view if any child requested + // unbuffered dispatch for pointer events. This is because we can't expect that the pointer + // source would dispatch to the focused view. + for (int i = 0; i < mChildrenCount; i++) { + final View child = mChildren[i]; + if ((child.mUnbufferedInputSource & InputDevice.SOURCE_CLASS_POINTER) != 0) { + mUnbufferedInputSource |= InputDevice.SOURCE_CLASS_POINTER; + break; + } + } + if (mParent != null) { + mParent.onDescendantUnbufferedRequested(); + } + } } diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java index f25206dce8ffc..775c15e77d5dd 100644 --- a/core/java/android/view/ViewParent.java +++ b/core/java/android/view/ViewParent.java @@ -675,4 +675,18 @@ public interface ViewParent { */ default void subtractObscuredTouchableRegion(Region touchableRegion, View view) { } + + /** + * Unbuffered dispatch has been requested by a child of this view parent. + * This method is called by the View hierarchy to signal ancestors that a View needs to + * request unbuffered dispatch. + * + * @see View#requestUnbufferedDispatch(int) + * @hide + */ + default void onDescendantUnbufferedRequested() { + if (getParent() != null) { + getParent().onDescendantUnbufferedRequested(); + } + } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 043e5be8a0503..204d2c8bc2ce1 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -18,6 +18,7 @@ package android.view; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; +import static android.view.InputDevice.SOURCE_CLASS_NONE; import static android.view.InsetsState.ITYPE_NAVIGATION_BAR; import static android.view.InsetsState.ITYPE_STATUS_BAR; import static android.view.View.PFLAG_DRAW_ANIMATION; @@ -116,6 +117,7 @@ import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; import android.util.TypedValue; +import android.view.InputDevice.InputSourceClass; import android.view.InsetsState.InternalInsetsType; import android.view.Surface.OutOfResourcesException; import android.view.SurfaceControl.Transaction; @@ -499,6 +501,10 @@ public final class ViewRootImpl implements ViewParent, int mPendingInputEventCount; boolean mProcessInputEventsScheduled; boolean mUnbufferedInputDispatch; + boolean mUnbufferedInputDispatchBySource; + @InputSourceClass + int mUnbufferedInputSource = SOURCE_CLASS_NONE; + String mPendingInputEventQueueLengthCounterName = "pq"; InputStage mFirstInputStage; @@ -1849,7 +1855,7 @@ public final class ViewRootImpl implements ViewParent, mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); - if (!mUnbufferedInputDispatch) { + if (!mUnbufferedInputDispatch && !mUnbufferedInputDispatchBySource) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); @@ -7505,6 +7511,9 @@ public final class ViewRootImpl implements ViewParent, writer.print(mTraversalScheduled); writer.print(innerPrefix); writer.print("mIsAmbientMode="); writer.print(mIsAmbientMode); + writer.print(innerPrefix); writer.print("mUnbufferedInputSource="); + writer.print(Integer.toHexString(mUnbufferedInputSource)); + if (mTraversalScheduled) { writer.print(" (barrier="); writer.print(mTraversalBarrier); writer.println(")"); } else { @@ -8109,6 +8118,7 @@ public final class ViewRootImpl implements ViewParent, @Override public void onInputEvent(InputEvent event) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "processInputEventForCompatibility"); + processUnbufferedRequest(event); List processedEvents; try { processedEvents = @@ -8134,7 +8144,7 @@ public final class ViewRootImpl implements ViewParent, @Override public void onBatchedInputEventPending() { - if (mUnbufferedInputDispatch) { + if (mUnbufferedInputDispatch || mUnbufferedInputDispatchBySource) { super.onBatchedInputEventPending(); } else { scheduleConsumeBatchedInput(); @@ -8151,6 +8161,17 @@ public final class ViewRootImpl implements ViewParent, unscheduleConsumeBatchedInput(); super.dispose(); } + + private void processUnbufferedRequest(InputEvent event) { + if (!(event instanceof MotionEvent)) { + return; + } + mUnbufferedInputDispatchBySource = + (event.getSource() & mUnbufferedInputSource) != SOURCE_CLASS_NONE; + if (mUnbufferedInputDispatchBySource && mConsumeBatchedInputScheduled) { + scheduleConsumeBatchedInputImmediately(); + } + } } WindowInputEventReceiver mInputEventReceiver; @@ -9601,4 +9622,9 @@ public final class ViewRootImpl implements ViewParent, return mSurfaceControl; } } + + @Override + public void onDescendantUnbufferedRequested() { + mUnbufferedInputSource = mView.mUnbufferedInputSource; + } }