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

Commit 74a2746b 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: I3adbaee1763f21557fb628b60d03b0a03e7079ab
parent 144f638f
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 −66
Original line number Diff line number Diff line
@@ -24,12 +24,15 @@ 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;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.launcher3.BaseDraggingActivity;
@@ -44,24 +47,36 @@ 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;
    // When a gesture crosses this length, this recognizer will attempt to interpret touch events.
    private final float mSquaredSlop;
    // When a gesture crosses this length, this recognizer will become the sole active recognizer.
    private final float mSquaredActiveThreshold;
    // When a gesture crosses this length, the overscroll view should be shown.
    private final float mSquaredFinishThreshold;
    private boolean mThisDownIsIgnored = false;

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

    @Nullable
    private RecentsView mRecentsView;
@@ -72,15 +87,24 @@ 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 finishGestureThreshold = (int) context.getResources()
                .getDimension(R.dimen.gestures_overscroll_finish_threshold);
        mSquaredFinishThreshold = finishGestureThreshold * finishGestureThreshold;

        float activeThreshold = (int) context.getResources()
                .getDimension(R.dimen.gestures_overscroll_active_threshold);
        mSquaredActiveThreshold = activeThreshold * activeThreshold;
    }

    @Override
@@ -90,12 +114,26 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {

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

        debugPrint("got event, underlying activity is " + getUnderlyingActivity());
        switch (ev.getActionMasked()) {
            case ACTION_DOWN: {
                debugPrint("ACTION_DOWN");
                mActivePointerId = ev.getPointerId(0);
                mDownPos.set(ev.getX(), ev.getY());
                mLastPos.set(mDownPos);

                if (mPlugin.blockOtherGestures()) {
                    debugPrint("mPlugin.blockOtherGestures(), becoming active on ACTION_DOWN");
                    // Otherwise, if an appear gesture is performed when the Activity is visible,
                    // the Activity will dismiss its keyboard.
                    mPassedActiveThreshold = true;
                    mPassedSlop = true;
                    mStartDragPos.set(mLastPos.x, mLastPos.y);
                    setActive(ev);
                }
                break;
            }
            case ACTION_POINTER_DOWN: {
@@ -121,57 +159,61 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {
                if (mState == STATE_DELEGATE_ACTIVE) {
                    break;
                }

                if (!mDelegate.allowInterceptByParent()) {
                    mState = STATE_DELEGATE_ACTIVE;
                    break;
                }

                // Update last touch position.
                int pointerIndex = ev.findPointerIndex(mActivePointerId);
                if (pointerIndex == -1) {
                    break;
                }
                mLastPos.set(ev.getX(pointerIndex), ev.getY(pointerIndex));

                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) {

                float squaredDist = squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
                if ((!mPassedSlop) && (squaredDist > mSquaredSlop)) {
                    mPassedSlop = true;
                    mStartDragPos.set(mLastPos.x, mLastPos.y);
                        if (isOverscrolled()) {
                            setActive(ev);

                            if (mPlugin != null) {
                                mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
                            }
                        } else {
                            mState = STATE_DELEGATE_ACTIVE;
                        }
                    mGestureState.setState(GestureState.STATE_OVERSCROLL_WINDOW_CREATED);
                }

                boolean becomeActive = mPassedSlop && !mPassedActiveThreshold && isOverscrolled()
                        && (squaredDist > mSquaredActiveThreshold);
                if (becomeActive) {
                    debugPrint("Past slop and past threshold, set active");
                    mPassedActiveThreshold = true;
                    setActive(ev);
                }

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

                break;
            }
            case ACTION_CANCEL:
            case ACTION_UP:
                if (mState != STATE_DELEGATE_ACTIVE && mPassedSlop && mPlugin != null) {
                    mPlugin.onTouchEnd(getDistancePx());
                debugPrint("ACTION_UP");
                if (mPassedActiveThreshold) {
                    debugPrint("ACTION_UP Relaying touch event");

                    mPlugin.onTouchEvent(ev, getHorizontalDistancePx(), getVerticalDistancePx(),
                            (int) Math.sqrt(mSquaredFinishThreshold), 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);
        }
@@ -192,15 +234,20 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {
            maxIndex = 1;
        }

        boolean atRightMostApp = (mRecentsView == null
                || mRecentsView.getRunningTaskIndex() <= maxIndex);
        boolean atRightMostApp = mRecentsView == null
                || (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 = mLastPos.x - mDownPos.x;

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

        return atRightMostApp && angleInBounds;
        boolean overscrollVisible = mPlugin.blockOtherGestures();
        boolean overscrollInvisibleAndLeftSwipe = !overscrollVisible && deltaX < 0;
        boolean gestureDirectionMatchesVisibility = overscrollVisible
                || overscrollInvisibleAndLeftSwipe;
        return atRightMostApp && angleInBounds && gestureDirectionMatchesVisibility;
    }

    private String getDeviceState() {
@@ -219,35 +266,22 @@ public class OverscrollInputConsumer extends DelegateInputConsumer {
        return deviceState;
    }

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

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

    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);
                }
            }
            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 @NonNull 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();
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -79,6 +79,8 @@

    <!-- Overscroll Gesture -->
    <dimen name="gestures_overscroll_fling_threshold">40dp</dimen>
    <dimen name="gestures_overscroll_active_threshold">80dp</dimen>
    <dimen name="gestures_overscroll_finish_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
@@ -107,6 +107,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