System gesture exclusion rects for Views

Allow views to register a list of rects where the device's system UI
should not intercept complex (read: down+move+up) gestures on specific
sub-regions of the view. This should not be used for large-scale,
full-view gesture recognition such as scrolling, but rather for specific
areas such as SeekBar's scroll thumb or DrawerLayout's edge strip for
swiping open a navigation drawer.

Add ability for ViewTreeObserver to observe transformed exclusion rects

Bug: 126360272
Test: atest android.view.cts.SystemGestureExclusionRectsTest
Change-Id: If89b6f66637e40efa12955d6408f6e37b25cb46f
This commit is contained in:
Adam Powell
2019-02-04 11:28:22 -08:00
parent 2da691987f
commit 769b8638f9
8 changed files with 402 additions and 34 deletions

View File

@@ -50515,6 +50515,7 @@ package android.view {
method public android.animation.StateListAnimator getStateListAnimator();
method protected int getSuggestedMinimumHeight();
method protected int getSuggestedMinimumWidth();
method @NonNull public java.util.List<android.graphics.Rect> getSystemGestureExclusionRects();
method public int getSystemUiVisibility();
method @android.view.ViewDebug.ExportedProperty public Object getTag();
method public Object getTag(int);
@@ -50853,6 +50854,7 @@ package android.view {
method public void setSelected(boolean);
method public void setSoundEffectsEnabled(boolean);
method public void setStateListAnimator(android.animation.StateListAnimator);
method public void setSystemGestureExclusionRects(@NonNull java.util.List<android.graphics.Rect>);
method public void setSystemUiVisibility(int);
method public void setTag(Object);
method public void setTag(int, Object);
@@ -51681,6 +51683,7 @@ package android.view {
method public void addOnGlobalLayoutListener(android.view.ViewTreeObserver.OnGlobalLayoutListener);
method public void addOnPreDrawListener(android.view.ViewTreeObserver.OnPreDrawListener);
method public void addOnScrollChangedListener(android.view.ViewTreeObserver.OnScrollChangedListener);
method public void addOnSystemGestureExclusionRectsChangedListener(java.util.function.Consumer<java.util.List<android.graphics.Rect>>);
method public void addOnTouchModeChangeListener(android.view.ViewTreeObserver.OnTouchModeChangeListener);
method public void addOnWindowAttachListener(android.view.ViewTreeObserver.OnWindowAttachListener);
method public void addOnWindowFocusChangeListener(android.view.ViewTreeObserver.OnWindowFocusChangeListener);
@@ -51695,6 +51698,7 @@ package android.view {
method public void removeOnGlobalLayoutListener(android.view.ViewTreeObserver.OnGlobalLayoutListener);
method public void removeOnPreDrawListener(android.view.ViewTreeObserver.OnPreDrawListener);
method public void removeOnScrollChangedListener(android.view.ViewTreeObserver.OnScrollChangedListener);
method public void removeOnSystemGestureExclusionRectsChangedListener(java.util.function.Consumer<java.util.List<android.graphics.Rect>>);
method public void removeOnTouchModeChangeListener(android.view.ViewTreeObserver.OnTouchModeChangeListener);
method public void removeOnWindowAttachListener(android.view.ViewTreeObserver.OnWindowAttachListener);
method public void removeOnWindowFocusChangeListener(android.view.ViewTreeObserver.OnWindowFocusChangeListener);

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Rect;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* Used by {@link ViewRootImpl} to track system gesture exclusion rects reported by views.
*/
class GestureExclusionTracker {
private boolean mGestureExclusionViewsChanged = false;
private List<GestureExclusionViewInfo> mGestureExclusionViewInfos = new ArrayList<>();
private List<Rect> mGestureExclusionRects = Collections.emptyList();
public void updateRectsForView(@NonNull View view) {
boolean found = false;
final Iterator<GestureExclusionViewInfo> i = mGestureExclusionViewInfos.iterator();
while (i.hasNext()) {
final GestureExclusionViewInfo info = i.next();
final View v = info.getView();
if (v == null || !v.isAttachedToWindow()) {
mGestureExclusionViewsChanged = true;
i.remove();
continue;
}
if (v == view) {
found = true;
info.mDirty = true;
break;
}
}
if (!found && view.isAttachedToWindow()) {
mGestureExclusionViewInfos.add(new GestureExclusionViewInfo(view));
mGestureExclusionViewsChanged = true;
}
}
@Nullable
public List<Rect> computeChangedRects() {
boolean changed = false;
final Iterator<GestureExclusionViewInfo> i = mGestureExclusionViewInfos.iterator();
final List<Rect> rects = new ArrayList<>();
while (i.hasNext()) {
final GestureExclusionViewInfo info = i.next();
switch (info.update()) {
case GestureExclusionViewInfo.CHANGED:
changed = true;
// Deliberate fall-through
case GestureExclusionViewInfo.UNCHANGED:
rects.addAll(info.mExclusionRects);
break;
case GestureExclusionViewInfo.GONE:
mGestureExclusionViewsChanged = true;
i.remove();
break;
}
}
if (changed || mGestureExclusionViewsChanged) {
mGestureExclusionViewsChanged = false;
if (!mGestureExclusionRects.equals(rects)) {
mGestureExclusionRects = rects;
return rects;
}
}
return null;
}
private static class GestureExclusionViewInfo {
public static final int CHANGED = 0;
public static final int UNCHANGED = 1;
public static final int GONE = 2;
private final WeakReference<View> mView;
boolean mDirty = true;
List<Rect> mExclusionRects = Collections.emptyList();
GestureExclusionViewInfo(View view) {
mView = new WeakReference<>(view);
}
public View getView() {
return mView.get();
}
public int update() {
final View excludedView = getView();
if (excludedView == null || !excludedView.isAttachedToWindow()) return GONE;
final List<Rect> localRects = excludedView.getSystemGestureExclusionRects();
final List<Rect> newRects = new ArrayList<>(localRects.size());
for (Rect src : localRects) {
Rect mappedRect = new Rect(src);
ViewParent p = excludedView.getParent();
if (p != null && p.getChildVisibleRect(excludedView, mappedRect, null)) {
newRects.add(mappedRect);
}
}
if (mExclusionRects.equals(localRects)) return UNCHANGED;
mExclusionRects = newRects;
return CHANGED;
}
}
}

View File

@@ -210,7 +210,7 @@ public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallb
public SurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mRenderNode.requestPositionUpdates(mPositionListener);
mRenderNode.addPositionUpdateListener(mPositionListener);
setWillNotDraw(true);
}

View File

@@ -4603,6 +4603,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private ArrayList<OnUnhandledKeyEventListener> mUnhandledKeyListeners;
private WindowInsetsAnimationListener mWindowInsetsAnimationListener;
/**
* This lives here since it's only valid for interactive views.
*/
private List<Rect> mSystemGestureExclusionRects;
/**
* Used to track {@link #mSystemGestureExclusionRects}
*/
public RenderNode.PositionUpdateListener mPositionUpdateListener;
}
@UnsupportedAppUsage
@@ -10973,6 +10983,95 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
}
/**
* Sets a list of areas within this view's post-layout coordinate space where the system
* should not intercept touch or other pointing device gestures. <em>This method should
* be called by {@link #onLayout(boolean, int, int, int, int)} or {@link #onDraw(Canvas)}.</em>
*
* <p>Use this to tell the system which specific sub-areas of a view need to receive gesture
* input in order to function correctly in the presence of global system gestures that may
* conflict. For example, if the system wishes to capture swipe-in-from-screen-edge gestures
* to provide system-level navigation functionality, a view such as a navigation drawer
* container can mark the left (or starting) edge of itself as requiring gesture capture
* priority using this API. The system may then choose to relax its own gesture recognition
* to allow the app to consume the user's gesture. It is not necessary for an app to register
* exclusion rects for broadly spanning regions such as the entirety of a
* <code>ScrollView</code> or for simple press and release click targets such as
* <code>Button</code>. Mark an exclusion rect when interacting with a view requires
* a precision touch gesture in a small area in either the X or Y dimension, such as
* an edge swipe or dragging a <code>SeekBar</code> thumb.</p>
*
* <p>Do not modify the provided list after this method is called.</p>
*
* @param rects A list of precision gesture regions that this view needs to function correctly
*/
public void setSystemGestureExclusionRects(@NonNull List<Rect> rects) {
if (rects.isEmpty() && mListenerInfo == null) return;
final ListenerInfo info = getListenerInfo();
if (rects.isEmpty()) {
info.mSystemGestureExclusionRects = null;
if (info.mPositionUpdateListener != null) {
mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
}
} else {
info.mSystemGestureExclusionRects = rects;
if (info.mPositionUpdateListener == null) {
info.mPositionUpdateListener = new RenderNode.PositionUpdateListener() {
@Override
public void positionChanged(long n, int l, int t, int r, int b) {
postUpdateSystemGestureExclusionRects();
}
@Override
public void positionLost(long frameNumber) {
postUpdateSystemGestureExclusionRects();
}
};
mRenderNode.addPositionUpdateListener(info.mPositionUpdateListener);
}
}
postUpdateSystemGestureExclusionRects();
}
/**
* WARNING: this can be called by a hwui worker thread, not just the UI thread!
*/
void postUpdateSystemGestureExclusionRects() {
// Potentially racey from a background thread. It's ok if it's not perfect.
final Handler h = getHandler();
if (h != null) {
h.postAtFrontOfQueue(this::updateSystemGestureExclusionRects);
}
}
void updateSystemGestureExclusionRects() {
final AttachInfo ai = mAttachInfo;
if (ai != null) {
ai.mViewRootImpl.updateSystemGestureExclusionRectsForView(this);
}
}
/**
* Retrieve the list of areas within this view's post-layout coordinate space where the system
* should not intercept touch or other pointing device gestures.
*
* <p>Do not modify the returned list.</p>
*
* @return the list set by {@link #setSystemGestureExclusionRects(List)}
*/
@NonNull
public List<Rect> getSystemGestureExclusionRects() {
final ListenerInfo info = mListenerInfo;
if (info != null) {
final List<Rect> list = info.mSystemGestureExclusionRects;
if (list != null) {
return list;
}
}
return Collections.emptyList();
}
/**
* Compute the view's coordinate within the surface.
*

View File

@@ -605,6 +605,8 @@ public final class ViewRootImpl implements ViewParent,
private final InsetsController mInsetsController = new InsetsController(this);
private final GestureExclusionTracker mGestureExclusionTracker = new GestureExclusionTracker();
static final class SystemUiVisibilityInfo {
int seq;
int globalVisibility;
@@ -3977,6 +3979,20 @@ public final class ViewRootImpl implements ViewParent,
return mAttachInfo.mAccessibilityFocusDrawable;
}
void updateSystemGestureExclusionRectsForView(View view) {
mGestureExclusionTracker.updateRectsForView(view);
mHandler.sendEmptyMessage(MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED);
}
void systemGestureExclusionChanged() {
final List<Rect> rectsForWindowManager = mGestureExclusionTracker.computeChangedRects();
if (rectsForWindowManager != null) {
// TODO Send to WM
mAttachInfo.mTreeObserver
.dispatchOnSystemGestureExclusionRectsChanged(rectsForWindowManager);
}
}
/**
* Requests that the root render node is invalidated next time we perform a draw, such that
* {@link WindowCallbacks#onPostDraw} gets called.
@@ -4433,35 +4449,36 @@ public final class ViewRootImpl implements ViewParent,
}
}
private final static int MSG_INVALIDATE = 1;
private final static int MSG_INVALIDATE_RECT = 2;
private final static int MSG_DIE = 3;
private final static int MSG_RESIZED = 4;
private final static int MSG_RESIZED_REPORT = 5;
private final static int MSG_WINDOW_FOCUS_CHANGED = 6;
private final static int MSG_DISPATCH_INPUT_EVENT = 7;
private final static int MSG_DISPATCH_APP_VISIBILITY = 8;
private final static int MSG_DISPATCH_GET_NEW_SURFACE = 9;
private final static int MSG_DISPATCH_KEY_FROM_IME = 11;
private final static int MSG_DISPATCH_KEY_FROM_AUTOFILL = 12;
private final static int MSG_CHECK_FOCUS = 13;
private final static int MSG_CLOSE_SYSTEM_DIALOGS = 14;
private final static int MSG_DISPATCH_DRAG_EVENT = 15;
private final static int MSG_DISPATCH_DRAG_LOCATION_EVENT = 16;
private final static int MSG_DISPATCH_SYSTEM_UI_VISIBILITY = 17;
private final static int MSG_UPDATE_CONFIGURATION = 18;
private final static int MSG_PROCESS_INPUT_EVENTS = 19;
private final static int MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST = 21;
private final static int MSG_INVALIDATE_WORLD = 22;
private final static int MSG_WINDOW_MOVED = 23;
private final static int MSG_SYNTHESIZE_INPUT_EVENT = 24;
private final static int MSG_DISPATCH_WINDOW_SHOWN = 25;
private final static int MSG_REQUEST_KEYBOARD_SHORTCUTS = 26;
private final static int MSG_UPDATE_POINTER_ICON = 27;
private final static int MSG_POINTER_CAPTURE_CHANGED = 28;
private final static int MSG_DRAW_FINISHED = 29;
private final static int MSG_INSETS_CHANGED = 30;
private final static int MSG_INSETS_CONTROL_CHANGED = 31;
private static final int MSG_INVALIDATE = 1;
private static final int MSG_INVALIDATE_RECT = 2;
private static final int MSG_DIE = 3;
private static final int MSG_RESIZED = 4;
private static final int MSG_RESIZED_REPORT = 5;
private static final int MSG_WINDOW_FOCUS_CHANGED = 6;
private static final int MSG_DISPATCH_INPUT_EVENT = 7;
private static final int MSG_DISPATCH_APP_VISIBILITY = 8;
private static final int MSG_DISPATCH_GET_NEW_SURFACE = 9;
private static final int MSG_DISPATCH_KEY_FROM_IME = 11;
private static final int MSG_DISPATCH_KEY_FROM_AUTOFILL = 12;
private static final int MSG_CHECK_FOCUS = 13;
private static final int MSG_CLOSE_SYSTEM_DIALOGS = 14;
private static final int MSG_DISPATCH_DRAG_EVENT = 15;
private static final int MSG_DISPATCH_DRAG_LOCATION_EVENT = 16;
private static final int MSG_DISPATCH_SYSTEM_UI_VISIBILITY = 17;
private static final int MSG_UPDATE_CONFIGURATION = 18;
private static final int MSG_PROCESS_INPUT_EVENTS = 19;
private static final int MSG_CLEAR_ACCESSIBILITY_FOCUS_HOST = 21;
private static final int MSG_INVALIDATE_WORLD = 22;
private static final int MSG_WINDOW_MOVED = 23;
private static final int MSG_SYNTHESIZE_INPUT_EVENT = 24;
private static final int MSG_DISPATCH_WINDOW_SHOWN = 25;
private static final int MSG_REQUEST_KEYBOARD_SHORTCUTS = 26;
private static final int MSG_UPDATE_POINTER_ICON = 27;
private static final int MSG_POINTER_CAPTURE_CHANGED = 28;
private static final int MSG_DRAW_FINISHED = 29;
private static final int MSG_INSETS_CHANGED = 30;
private static final int MSG_INSETS_CONTROL_CHANGED = 31;
private static final int MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED = 32;
final class ViewRootHandler extends Handler {
@Override
@@ -4519,6 +4536,10 @@ public final class ViewRootImpl implements ViewParent,
return "MSG_DRAW_FINISHED";
case MSG_INSETS_CHANGED:
return "MSG_INSETS_CHANGED";
case MSG_INSETS_CONTROL_CHANGED:
return "MSG_INSETS_CONTROL_CHANGED";
case MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED:
return "MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED";
}
return super.getMessageName(message);
}
@@ -4750,6 +4771,9 @@ public final class ViewRootImpl implements ViewParent,
case MSG_DRAW_FINISHED: {
pendingDrawFinished();
} break;
case MSG_SYSTEM_GESTURE_EXCLUSION_CHANGED: {
systemGestureExclusionChanged();
} break;
}
}
}

View File

@@ -26,7 +26,9 @@ import android.os.Build;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
/**
* A view tree observer is used to register listeners that can be notified of global
@@ -57,6 +59,7 @@ public final class ViewTreeObserver {
private CopyOnWriteArray<OnScrollChangedListener> mOnScrollChangedListeners;
private CopyOnWriteArray<OnPreDrawListener> mOnPreDrawListeners;
private CopyOnWriteArray<OnWindowShownListener> mOnWindowShownListeners;
private CopyOnWriteArray<Consumer<List<Rect>>> mGestureExclusionListeners;
// These listeners cannot be mutated during dispatch
private boolean mInDispatchOnDraw;
@@ -450,6 +453,14 @@ public final class ViewTreeObserver {
}
}
if (observer.mGestureExclusionListeners != null) {
if (mGestureExclusionListeners != null) {
mGestureExclusionListeners.addAll(observer.mGestureExclusionListeners);
} else {
mGestureExclusionListeners = observer.mGestureExclusionListeners;
}
}
observer.kill();
}
@@ -913,6 +924,35 @@ public final class ViewTreeObserver {
mOnEnterAnimationCompleteListeners.remove(listener);
}
/**
* Add a listener to be notified when the tree's <em>transformed</em> gesture exclusion rects
* change. This could be the result of an animation or other layout change, or a view calling
* {@link View#setSystemGestureExclusionRects(List)}.
*
* @param listener listener to add
* @see View#setSystemGestureExclusionRects(List)
*/
public void addOnSystemGestureExclusionRectsChangedListener(Consumer<List<Rect>> listener) {
checkIsAlive();
if (mGestureExclusionListeners == null) {
mGestureExclusionListeners = new CopyOnWriteArray<>();
}
mGestureExclusionListeners.add(listener);
}
/**
* Unsubscribe the given listener from gesture exclusion rect changes.
* @see #addOnSystemGestureExclusionRectsChangedListener(Consumer)
* @see View#setSystemGestureExclusionRects(List)
*/
public void removeOnSystemGestureExclusionRectsChangedListener(Consumer<List<Rect>> listener) {
checkIsAlive();
if (mGestureExclusionListeners == null) {
return;
}
mGestureExclusionListeners.remove(listener);
}
private void checkIsAlive() {
if (!mAlive) {
throw new IllegalStateException("This ViewTreeObserver is not alive, call "
@@ -1178,6 +1218,21 @@ public final class ViewTreeObserver {
}
}
void dispatchOnSystemGestureExclusionRectsChanged(@NonNull List<Rect> rects) {
final CopyOnWriteArray<Consumer<List<Rect>>> listeners = mGestureExclusionListeners;
if (listeners != null && listeners.size() > 0) {
CopyOnWriteArray.Access<Consumer<List<Rect>>> access = listeners.start();
try {
final int count = access.size();
for (int i = 0; i < count; i++) {
access.get(i).accept(rects);
}
} finally {
listeners.end();
}
}
}
/**
* Copy on write array. This array is not thread safe, and only one loop can
* iterate over this array at any given time. This class avoids allocations

View File

@@ -27,6 +27,8 @@ import android.view.RenderNodeAnimator;
import android.view.Surface;
import android.view.View;
import com.android.internal.util.ArrayUtils;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
@@ -179,6 +181,10 @@ public final class RenderNode {
private final AnimationHost mAnimationHost;
private RecordingCanvas mCurrentRecordingCanvas;
// Will be null if not currently registered
@Nullable
private CompositePositionUpdateListener mCompositePositionUpdateListener;
/**
* Creates a new RenderNode that can be used to record batches of
* drawing operations, and store / apply render properties when drawn.
@@ -248,15 +254,70 @@ public final class RenderNode {
}
private static final class CompositePositionUpdateListener implements PositionUpdateListener {
private final PositionUpdateListener[] mListeners;
CompositePositionUpdateListener(PositionUpdateListener... listeners) {
mListeners = listeners;
}
public CompositePositionUpdateListener with(PositionUpdateListener listener) {
return new CompositePositionUpdateListener(
ArrayUtils.appendElement(PositionUpdateListener.class, mListeners, listener));
}
public CompositePositionUpdateListener without(PositionUpdateListener listener) {
return new CompositePositionUpdateListener(
ArrayUtils.removeElement(PositionUpdateListener.class, mListeners, listener));
}
@Override
public void positionChanged(long frameNumber, int left, int top, int right, int bottom) {
for (PositionUpdateListener pul : mListeners) {
pul.positionChanged(frameNumber, left, top, right, bottom);
}
}
@Override
public void positionLost(long frameNumber) {
for (PositionUpdateListener pul : mListeners) {
pul.positionLost(frameNumber);
}
}
}
/**
* Enable callbacks for position changes.
* Enable callbacks for position changes. Call only from the UI thread or with
* external synchronization.
*
* @hide
*/
public void requestPositionUpdates(PositionUpdateListener listener) {
nRequestPositionUpdates(mNativeRenderNode, listener);
public void addPositionUpdateListener(@NonNull PositionUpdateListener listener) {
CompositePositionUpdateListener comp = mCompositePositionUpdateListener;
if (comp == null) {
comp = new CompositePositionUpdateListener(listener);
} else {
comp = comp.with(listener);
}
mCompositePositionUpdateListener = comp;
nRequestPositionUpdates(mNativeRenderNode, comp);
}
/**
* Disable a callback for position changes. Call only from the UI thread or with
* external synchronization.
*
* @param listener Callback to remove
* @hide
*/
public void removePositionUpdateListener(@NonNull PositionUpdateListener listener) {
CompositePositionUpdateListener comp = mCompositePositionUpdateListener;
if (comp != null) {
comp = comp.without(listener);
mCompositePositionUpdateListener = comp;
nRequestPositionUpdates(mNativeRenderNode, comp);
}
}
/**
* Starts recording a display list for the render node. All

View File

@@ -53,7 +53,7 @@ public class PositionListenerActivity extends Activity {
MyPositionReporter(Context c) {
super(c);
mNode = new RenderNode("positionListener");
mNode.requestPositionUpdates(this);
mNode.addPositionUpdateListener(this);
setTextAlignment(TEXT_ALIGNMENT_VIEW_START);
}