Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 769b8638 authored by Adam Powell's avatar Adam Powell
Browse files

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
parent 2da69198
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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);
+125 −0
Original line number Diff line number Diff line
/*
 * 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;
        }
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -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);
    }
+99 −0
Original line number Diff line number Diff line
@@ -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.
     *
+53 −29
Original line number Diff line number Diff line
@@ -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;
            }
        }
    }
Loading