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

Commit d9c86d4b authored by James O'Leary's avatar James O'Leary
Browse files

Compose gesture integrated fully into Launcher

- Support dismissing Compose via the reverse gesture from the appear
gesture
- Use Tony Wickham's ag/10204761 with some glue code to enable the
app below Compose panning in the same direction as the gesture as
Compose peeks in
- Add feature flag to use Compose hosted in a window (permits underlying
app panning)
- Use InterpolatingVelocityTracker to fix OtherActivityInputConsumer
processing swipes in the wrong direction ~20% of the time due to a bug
in VelocityTracker (see go/quirky-bubbles)

Change-Id: I65aa07ac112db8bd89cec9acfa0ce2b6ebacd43f
parent e93d6d61
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import static android.view.MotionEvent.INVALID_POINTER_ID;
import static com.android.launcher3.Utilities.EDGE_NAV_BAR;
import static com.android.launcher3.Utilities.squaredHypot;
import static com.android.launcher3.util.TraceHelper.FLAG_CHECK_FOR_RACE_CONDITIONS;
import static com.android.quickstep.GestureState.STATE_OVERSCROLL_WINDOW_CREATED;
import static com.android.quickstep.util.ActiveGestureLog.INTENT_EXTRA_LOG_TRACE_ID;
import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS;

@@ -430,6 +431,6 @@ public class OtherActivityInputConsumer extends ContextWrapper implements InputC

    @Override
    public boolean allowInterceptByParent() {
        return !mPassedPilferInputSlop;
        return !mPassedPilferInputSlop || mGestureState.hasState(STATE_OVERSCROLL_WINDOW_CREATED);
    }
}
+100 −55
Original line number Diff line number Diff line
@@ -24,9 +24,11 @@ import static android.view.MotionEvent.ACTION_UP;

import static com.android.launcher3.Utilities.squaredHypot;

import static java.lang.Math.abs;

import android.content.Context;
import android.graphics.PointF;
import android.view.GestureDetector;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewConfiguration;

@@ -44,24 +46,31 @@ import com.android.systemui.shared.system.InputMonitorCompat;
 * Input consumer for handling events to pass to an {@code OverscrollPlugin}.
 */
public class OverscrollInputConsumer extends DelegateInputConsumer {

    private static final String TAG = "OverscrollInputConsumer";
    private static final boolean DEBUG_LOGS_ENABLED = false;
    private static void debugPrint(String log) {
        if (DEBUG_LOGS_ENABLED) {
            Log.v(TAG, log);
        }
    }

    private final PointF mDownPos = new PointF();
    private final PointF mLastPos = new PointF();
    private final PointF mStartDragPos = new PointF();
    private final int mAngleThreshold;

    private final float mFlingThresholdPx;
    private final int mFlingDistanceThresholdPx;
    private final int mFlingVelocityThresholdPx;
    private int mActivePointerId = -1;
    private boolean mPassedSlop = false;

    // True if we set ourselves as active, meaning we no longer pass events to the delegate.
    private boolean mPassedActiveThreshold = false;
    private final float mSquaredActiveThreshold;
    private final float mSquaredSlop;

    private final GestureState mGestureState;
    @Nullable
    private final OverscrollPlugin mPlugin;
    private final GestureDetector mGestureDetector;

    @Nullable
    private RecentsView mRecentsView;
@@ -72,15 +81,19 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {

        mAngleThreshold = context.getResources()
                .getInteger(R.integer.assistant_gesture_corner_deg_threshold);
        mFlingThresholdPx = context.getResources()
        mFlingDistanceThresholdPx = (int) context.getResources()
                .getDimension(R.dimen.gestures_overscroll_fling_threshold);
        mFlingVelocityThresholdPx = ViewConfiguration.get(context).getScaledMinimumFlingVelocity();
        mGestureState = gestureState;
        mPlugin = plugin;

        float slop = ViewConfiguration.get(context).getScaledTouchSlop();

        mSquaredSlop = slop * slop;
        mGestureDetector = new GestureDetector(context, new FlingGestureListener());

        float dragThreshold = (int) context.getResources()
                .getDimension(R.dimen.gestures_overscroll_drag_threshold);
        mSquaredActiveThreshold = dragThreshold * dragThreshold;
    }

    @Override
@@ -90,12 +103,27 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {

    @Override
    public void onMotionEvent(MotionEvent ev) {
        if (mPlugin == null) {
            return;
        }

        switch (ev.getActionMasked()) {
            case ACTION_DOWN: {
                if (mPlugin.blockOtherGestures()) {
                    // When an Activity is visible, blocking other gestures prevents the Activity
                    // from disappearing upon ACTION_DOWN in the navigation bar. (it will reappear
                    // on ACTION_MOVE or ACTION_UP)
                    debugPrint("Becoming active on ACTION_DOWN");
                    if (mState != STATE_ACTIVE) {
                        setActive(ev);
                    }
                }
                mActivePointerId = ev.getPointerId(0);
                mDownPos.set(ev.getX(), ev.getY());
                mLastPos.set(mDownPos);

                mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
                        (int) Math.sqrt(mSquaredActiveThreshold), mFlingDistanceThresholdPx,
                        mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
                break;
            }
            case ACTION_POINTER_DOWN: {
@@ -131,53 +159,82 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {
                }
                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));

                float squaredDist = squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);



                if (!mPassedSlop) {
                    // Normal gesture, ensure we pass the slop before we start tracking the gesture
                    if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
                            > mSquaredSlop) {

                    if (squaredDist > mSquaredSlop) {
                        debugPrint("passed slop");
                        mPassedSlop = true;
                        mStartDragPos.set(mLastPos.x, mLastPos.y);
                        if (isOverscrolled()) {
                            debugPrint("setting STATE_OVERSCROLL_WINDOW_CREATED");
                            mGestureState.setState(GestureState.STATE_OVERSCROLL_WINDOW_CREATED);
                            if (!mPlugin.allowsUnderlyingActivityOverscroll()
                                    && (mState != STATE_ACTIVE)) {
                                debugPrint("setting active gesture handler to overscroll to "
                                        + "prevent losing active touch when Activity starts");
                                setActive(ev);

                            if (mPlugin != null) {
                                mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
                            }
                        }
                    } else {
                            mState = STATE_DELEGATE_ACTIVE;
                        debugPrint("Not past slop");
                    }
                }

                if (mPassedSlop && !mPassedActiveThreshold && isOverscrolled()) {
                    if ((squaredDist > mSquaredActiveThreshold)) {
                        debugPrint("Past slop and past threshold, set active");

                        mPassedActiveThreshold = true;
                        if (mState != STATE_ACTIVE) {
                            setActive(ev);
                        }
                    }
                }

                if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()
                        && mPlugin != null) {
                    mPlugin.onTouchTraveled(getDistancePx());
                if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()) {
                    debugPrint("Relaying touch event");
                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
                            (int) Math.sqrt(mSquaredActiveThreshold), mFlingDistanceThresholdPx,
                            mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
                }

                break;
            }
            case ACTION_CANCEL:
            case ACTION_UP:
                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
                    mPlugin.onTouchEnd(getDistancePx());
                if (mPassedSlop && isOverscrolled()) {
                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
                            (int) Math.sqrt(mSquaredActiveThreshold), mFlingDistanceThresholdPx,
                            mFlingVelocityThresholdPx, getDeviceState(), getUnderlyingActivity());
                }

                mPassedSlop = false;
                mPassedActiveThreshold = false;
                mState = STATE_INACTIVE;
                break;
        }

        if (mState != STATE_DELEGATE_ACTIVE) {
            mGestureDetector.onTouchEvent(ev);
        }

        if (mState != STATE_ACTIVE) {
            mDelegate.onMotionEvent(ev);
        }
    }

    private boolean isOverscrolled() {
        if (mPlugin.blockOtherGestures()) {
            // When an Activity is visible, this `InputConsumer` immediately becomes
            // the active gesture handler to prevent the Activity from disappearing on TOUCH_DOWN
            // in the navbar.
            //
            // Returning `true` ensures that case will still result in touches being handled,
            // instead of dropping touches until the gesture reaches the thresholds calculated
            // below.
            return true;
        }

        if (mRecentsView == null) {
            BaseDraggingActivity activity = mGestureState.getActivityInterface()
                    .getCreatedActivity();
@@ -196,9 +253,10 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {
                || mRecentsView.getRunningTaskIndex() <= maxIndex);

        // Check if the gesture is within our angle threshold of horizontal
        float deltaY = Math.abs(mLastPos.y - mDownPos.y);
        float deltaX = mDownPos.x - mLastPos.x; // Positive if this is a gesture to the left
        boolean angleInBounds = Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold;
        float deltaY = abs(mLastPos.y - mDownPos.y);
        float deltaX = abs(mDownPos.x - mLastPos.x);

        boolean angleInBounds = (Math.toDegrees(Math.atan2(deltaY, deltaX)) < mAngleThreshold);

        return atRightMostApp && angleInBounds;
    }
@@ -219,35 +277,22 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {
        return deviceState;
    }

    private int getDistancePx() {
        return (int) Math.hypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
    }

    private String getUnderlyingActivity() {
        return mGestureState.getRunningTask().topActivity.flattenToString();
    private int getHorizontalDistancePx() {
        return (int) (mLastPos.x - mDownPos.x);
    }

    private class FlingGestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            if (isValidAngle(velocityX, -velocityY)
                    && getDistancePx() >= mFlingThresholdPx
                    && mState != STATE_DELEGATE_ACTIVE) {

                if (mPlugin != null) {
                    mPlugin.onFling(-velocityX);
                }
    private int getVerticalDistancePx() {
        return (int) (mLastPos.y - mDownPos.y);
    }
            return true;
        }

        private boolean isValidAngle(float deltaX, float deltaY) {
            float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
            // normalize so that angle is measured clockwise from horizontal in the bottom right
            // corner and counterclockwise from horizontal in the bottom left corner

            angle = angle > 90 ? 180 - angle : angle;
            return (angle < mAngleThreshold);
    private String getUnderlyingActivity() {
        // Overly defensive, got guidance on code review that something in the chain of
        // `mGestureState.getRunningTask().topActivity` can be null and thus cause a null pointer
        // exception to be thrown, but we aren't sure which part can be null.
        if ((mGestureState == null) || (mGestureState.getRunningTask() == null)
                || (mGestureState.getRunningTask().topActivity == null)) {
            return "";
        }
        return mGestureState.getRunningTask().topActivity.flattenToString();
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -79,6 +79,7 @@

    <!-- Overscroll Gesture -->
    <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
    <dimen name="gestures_overscroll_drag_threshold">136dp</dimen>

    <!-- Tips Gesture Tutorial -->
    <dimen name="gesture_tutorial_title_margin_start_end">40dp</dimen>
+4 −0
Original line number Diff line number Diff line
@@ -106,6 +106,10 @@ public class GestureState implements RecentsAnimationCallbacks.RecentsAnimationL
    public static final int STATE_RECENTS_ANIMATION_ENDED =
            getFlagForIndex("STATE_RECENTS_ANIMATION_ENDED");

    // Called when we create an overscroll window when swiping right to left on the most recent app
    public static final int STATE_OVERSCROLL_WINDOW_CREATED =
            getFlagForIndex("STATE_OVERSCROLL_WINDOW_CREATED");

    // Called when RecentsView stops scrolling and settles on a TaskView.
    public static final int STATE_RECENTS_SCROLLING_FINISHED =
            getFlagForIndex("STATE_RECENTS_SCROLLING_FINISHED");
+3 −0
Original line number Diff line number Diff line
@@ -110,6 +110,9 @@ public final class FeatureFlags {
    public static final BooleanFlag ENABLE_QUICK_CAPTURE_GESTURE = getDebugFlag(
            "ENABLE_QUICK_CAPTURE_GESTURE", true, "Swipe from right to left to quick capture");

    public static final BooleanFlag ENABLE_QUICK_CAPTURE_WINDOW = getDebugFlag(
            "ENABLE_QUICK_CAPTURE_WINDOW", false, "Use window to host quick capture");

    public static final BooleanFlag FORCE_LOCAL_OVERSCROLL_PLUGIN = getDebugFlag(
            "FORCE_LOCAL_OVERSCROLL_PLUGIN", false,
            "Use a launcher-provided OverscrollPlugin if available");
Loading