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
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
index 1f6c506..6b0d7a3 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OtherActivityInputConsumer.java
@@ -26,6 +26,7 @@
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 @@
@Override
public boolean allowInterceptByParent() {
- return !mPassedPilferInputSlop;
+ return !mPassedPilferInputSlop || mGestureState.hasState(STATE_OVERSCROLL_WINDOW_CREATED);
}
}
diff --git a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
index c49b8f2..fb420a2 100644
--- a/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
+++ b/quickstep/recents_ui_overrides/src/com/android/quickstep/inputconsumers/OverscrollInputConsumer.java
@@ -24,12 +24,15 @@
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 @@
* 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 @@
mAngleThreshold = context.getResources()
.getInteger(R.integer.assistant_gesture_corner_deg_threshold);
- mFlingThresholdPx = context.getResources()
- .getDimension(R.dimen.gestures_overscroll_fling_threshold);
+ 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 @@
@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 @@
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) {
-
- mPassedSlop = true;
- mStartDragPos.set(mLastPos.x, mLastPos.y);
- if (isOverscrolled()) {
- setActive(ev);
-
- if (mPlugin != null) {
- mPlugin.onTouchStart(getDeviceState(), getUnderlyingActivity());
- }
- } else {
- mState = STATE_DELEGATE_ACTIVE;
- }
- }
+ float squaredDist = squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y);
+ if ((!mPassedSlop) && (squaredDist > mSquaredSlop)) {
+ mPassedSlop = true;
+ mStartDragPos.set(mLastPos.x, mLastPos.y);
+ mGestureState.setState(GestureState.STATE_OVERSCROLL_WINDOW_CREATED);
}
- if (mPassedSlop && mState != STATE_DELEGATE_ACTIVE && isOverscrolled()
- && mPlugin != null) {
- mPlugin.onTouchTraveled(getDistancePx());
+ boolean becomeActive = mPassedSlop && !mPassedActiveThreshold && isOverscrolled()
+ && (squaredDist > mSquaredActiveThreshold);
+ if (becomeActive) {
+ debugPrint("Past slop and past threshold, set active");
+ mPassedActiveThreshold = true;
+ setActive(ev);
+ }
+
+ 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 @@
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;
- return atRightMostApp && angleInBounds;
+ boolean angleInBounds = (Math.toDegrees(Math.atan2(deltaY, abs(deltaX))) < mAngleThreshold);
+
+ boolean overscrollVisible = mPlugin.blockOtherGestures();
+ boolean overscrollInvisibleAndLeftSwipe = !overscrollVisible && deltaX < 0;
+ boolean gestureDirectionMatchesVisibility = overscrollVisible
+ || overscrollInvisibleAndLeftSwipe;
+ return atRightMostApp && angleInBounds && gestureDirectionMatchesVisibility;
}
private String getDeviceState() {
@@ -219,35 +266,22 @@
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() {
+ private int getVerticalDistancePx() {
+ return (int) (mLastPos.y - mDownPos.y);
+ }
+
+ 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();
}
-
- 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);
- }
- }
}
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index b06dc6b..8586804 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -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>
diff --git a/quickstep/src/com/android/quickstep/GestureState.java b/quickstep/src/com/android/quickstep/GestureState.java
index f06e1a6..5adcc2e 100644
--- a/quickstep/src/com/android/quickstep/GestureState.java
+++ b/quickstep/src/com/android/quickstep/GestureState.java
@@ -107,6 +107,10 @@
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");
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 78d194b..6919339 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -110,6 +110,9 @@
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");
diff --git a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
index 28a9193..a434d07 100644
--- a/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
+++ b/src_plugins/com/android/systemui/plugins/OverscrollPlugin.java
@@ -15,6 +15,8 @@
*/
package com.android.systemui.plugins;
+import android.view.MotionEvent;
+
import com.android.systemui.plugins.annotations.ProvidesInterface;
/**
@@ -28,7 +30,7 @@
public interface OverscrollPlugin extends Plugin {
String ACTION = "com.android.systemui.action.PLUGIN_LAUNCHER_OVERSCROLL";
- int VERSION = 3;
+ int VERSION = 4;
String DEVICE_STATE_LOCKED = "Locked";
String DEVICE_STATE_LAUNCHER = "Launcher";
@@ -41,33 +43,33 @@
boolean isActive();
/**
- * Called when a touch is down and has been recognized as an overscroll gesture.
- * A call of this method will always result in `onTouchUp` being called, and possibly
- * `onFling` as well.
- *
+ * Called when a touch has been recognized as an overscroll gesture.
+ * @param horizontalDistancePx Horizontal distance from the last finger location to the finger
+ * location when it first touched the screen.
+ * @param verticalDistancePx Horizontal distance from the last finger location to the finger
+ * location when it first touched the screen.
+ * @param thresholdPx Minimum distance for gesture.
+ * @param flingDistanceThresholdPx Minimum distance for gesture by fling.
+ * @param flingVelocityThresholdPx Minimum velocity for gesture by fling.
* @param deviceState String representing the current device state
* @param underlyingActivity String representing the currently active Activity
*/
- void onTouchStart(String deviceState, String underlyingActivity);
+ void onTouchEvent(MotionEvent event,
+ int horizontalDistancePx,
+ int verticalDistancePx,
+ int thresholdPx,
+ int flingDistanceThresholdPx,
+ int flingVelocityThresholdPx,
+ String deviceState,
+ String underlyingActivity);
/**
- * Called when a touch that was previously recognized has moved.
- *
- * @param px distance between the position of touch on this update and the position of the
- * touch when it was initially recognized.
+ * @return `true` if overscroll gesture handling should override all other gestures.
*/
- void onTouchTraveled(int px);
+ boolean blockOtherGestures();
/**
- * Called when a touch that was previously recognized has ended.
- *
- * @param px distance between the position of touch on this update and the position of the
- * touch when it was initially recognized.
+ * @return `true` if the overscroll gesture can pan the underlying app.
*/
- void onTouchEnd(int px);
-
- /**
- * Called when the user starts Compose with a fling. `onTouchUp` will also be called.
- */
- void onFling(float velocity);
+ boolean allowsUnderlyingActivityOverscroll();
}