Overshoot when flinging up from an app

- Use OvershootInterpolator based on velocity (consistent with swiping up
  from home)
- Scale down recents as well, to be consistent with adjacent pages scaling
  up when you launch a task

Bug: 109709720
Change-Id: Ie47309058ccf673a4b86c40c843c415beb2d8dc7
diff --git a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
index c58bb94..717179d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/PortraitStatesTouchController.java
@@ -25,7 +25,6 @@
 import android.animation.ValueAnimator;
 import android.view.MotionEvent;
 import android.view.animation.Interpolator;
-import android.view.animation.OvershootInterpolator;
 
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.DeviceProfile;
@@ -192,7 +191,7 @@
                 // Update all apps interpolator to add a bit of overshoot starting from currFraction
                 final float currFraction = mCurrentAnimation.getProgressFraction();
                 mAllAppsInterpolatorWrapper.baseInterpolator = Interpolators.clampToProgress(
-                        new OvershootInterpolator(Math.min(Math.abs(velocity), 3f)), currFraction, 1);
+                        Interpolators.overshootInterpolatorForVelocity(velocity), currFraction, 1);
                 animator.setDuration(Math.min(expectedDuration, ATOMIC_DURATION))
                         .setInterpolator(LINEAR);
             }
diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
index 202d8fc..0205c1f 100644
--- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java
+++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java
@@ -15,7 +15,9 @@
  */
 package com.android.quickstep;
 
+import static android.view.View.TRANSLATION_Y;
 import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
+import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 import static com.android.launcher3.LauncherState.FAST_OVERVIEW;
 import static com.android.launcher3.LauncherState.OVERVIEW;
 import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS;
@@ -25,6 +27,7 @@
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_BACK;
 import static com.android.systemui.shared.system.NavigationBarCompat.HIT_TARGET_ROTATION;
 
+import android.animation.Animator;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.annotation.TargetApi;
@@ -55,14 +58,15 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto;
 import com.android.launcher3.util.MultiValueAlpha.AlphaProperty;
 import com.android.quickstep.TouchConsumer.InteractionType;
+import com.android.quickstep.util.ClipAnimationHelper;
 import com.android.quickstep.util.LayoutUtils;
-import com.android.quickstep.util.TransformedRect;
 import com.android.quickstep.util.RemoteAnimationProvider;
 import com.android.quickstep.util.RemoteAnimationTargetSet;
+import com.android.quickstep.util.TransformedRect;
 import com.android.quickstep.views.LauncherLayoutListener;
-import com.android.quickstep.views.LauncherRecentsView;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.RecentsViewContainer;
+import com.android.quickstep.views.TaskView;
 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
 
 import java.util.Objects;
@@ -248,28 +252,52 @@
                 return;
             }
 
-            if (activity.getDeviceProfile().isVerticalBarLayout()) {
-                return;
-            }
-
-            AllAppsTransitionController controller = activity.getAllAppsController();
             AnimatorSet anim = new AnimatorSet();
 
-            float scrollRange = Math.max(controller.getShiftRange(), 1);
-            float progressDelta = (transitionLength / scrollRange);
+            if (!activity.getDeviceProfile().isVerticalBarLayout()) {
+                AllAppsTransitionController controller = activity.getAllAppsController();
+                float scrollRange = Math.max(controller.getShiftRange(), 1);
+                float progressDelta = (transitionLength / scrollRange);
 
-            float endProgress = endState.getVerticalProgress(activity);
-            float startProgress = endProgress + progressDelta;
-            ObjectAnimator shiftAnim = ObjectAnimator.ofFloat(
-                    controller, ALL_APPS_PROGRESS, startProgress, endProgress);
-            shiftAnim.setInterpolator(LINEAR);
-            anim.play(shiftAnim);
+                float endProgress = endState.getVerticalProgress(activity);
+                float startProgress = endProgress + progressDelta;
+                ObjectAnimator shiftAnim = ObjectAnimator.ofFloat(
+                        controller, ALL_APPS_PROGRESS, startProgress, endProgress);
+                shiftAnim.setInterpolator(LINEAR);
+                anim.play(shiftAnim);
+            }
+
+            if (interactionType == INTERACTION_NORMAL) {
+                playScaleDownAnim(anim, activity);
+            }
 
             anim.setDuration(transitionLength * 2);
             activity.getStateManager().setCurrentAnimation(anim);
             callback.accept(AnimatorPlaybackController.wrap(anim, transitionLength * 2));
         }
 
+        /**
+         * Scale down recents from the center task being full screen to being in overview.
+         */
+        private void playScaleDownAnim(AnimatorSet anim, Launcher launcher) {
+            RecentsView recentsView = launcher.getOverviewPanel();
+            TaskView v = recentsView.getPageAt(recentsView.getCurrentPage());
+            ClipAnimationHelper clipHelper = new ClipAnimationHelper();
+            clipHelper.fromTaskThumbnailView(v.getThumbnail(), (RecentsView) v.getParent(), null);
+            if (!clipHelper.getSourceRect().isEmpty() && !clipHelper.getTargetRect().isEmpty()) {
+                float fromScale = clipHelper.getSourceRect().width()
+                        / clipHelper.getTargetRect().width();
+                float fromTranslationY = clipHelper.getSourceRect().centerY()
+                        - clipHelper.getTargetRect().centerY();
+                Animator scale = ObjectAnimator.ofFloat(recentsView, SCALE_PROPERTY, fromScale, 1);
+                Animator translateY = ObjectAnimator.ofFloat(recentsView, TRANSLATION_Y,
+                        fromTranslationY, 0);
+                scale.setInterpolator(LINEAR);
+                translateY.setInterpolator(LINEAR);
+                anim.playTogether(scale, translateY);
+            }
+        }
+
         @Override
         public ActivityInitListener createActivityInitListener(
                 BiPredicate<Launcher, Boolean> onInitListener) {
diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
index 765b5ff..66bc501 100644
--- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
+++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java
@@ -58,6 +58,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimationSuccessListener;
 import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
@@ -569,7 +570,8 @@
     }
 
     private void updateFinalShiftUi() {
-        if (mLauncherTransitionController == null) {
+        if (mLauncherTransitionController == null || mLauncherTransitionController
+                .getAnimationPlayer().isStarted()) {
             return;
         }
         mLauncherTransitionController.setPlayFraction(mCurrentShift.value);
@@ -663,17 +665,23 @@
     }
 
     private void handleNormalGestureEnd(float endVelocity, boolean isFling) {
+        float velocityPxPerMs = endVelocity / 1000;
         long duration = MAX_SWIPE_DURATION;
         final float endShift;
         final float startShift;
+        final Interpolator interpolator;
         if (!isFling) {
             endShift = mCurrentShift.value >= MIN_PROGRESS_FOR_OVERVIEW && mGestureStarted ? 1 : 0;
             long expectedDuration = Math.abs(Math.round((endShift - mCurrentShift.value)
                     * MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
             duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
             startShift = mCurrentShift.value;
+            interpolator = DEACCEL;
         } else {
             endShift = endVelocity < 0 ? 1 : 0;
+            interpolator = endVelocity < 0
+                    ? Interpolators.overshootInterpolatorForVelocity(velocityPxPerMs, 2f)
+                    : DEACCEL;
             float minFlingVelocity = mContext.getResources()
                     .getDimension(R.dimen.quickstep_fling_min_velocity);
             if (Math.abs(endVelocity) > minFlingVelocity && mTransitionDragLength > 0) {
@@ -682,14 +690,13 @@
                 // we want the page's snap velocity to approximately match the velocity at
                 // which the user flings, so we scale the duration by a value near to the
                 // derivative of the scroll interpolator at zero, ie. 2.
-                long baseDuration = Math.round(1000 * Math.abs(distanceToTravel / endVelocity));
+                long baseDuration = Math.round(Math.abs(distanceToTravel / velocityPxPerMs));
                 duration = Math.min(MAX_SWIPE_DURATION, 2 * baseDuration);
             }
-            startShift = Utilities.boundToRange(mCurrentShift.value - endVelocity * SINGLE_FRAME_MS
-                            / (mTransitionDragLength * 1000), 0, 1);
+            startShift = Utilities.boundToRange(mCurrentShift.value - velocityPxPerMs
+                    * SINGLE_FRAME_MS / (mTransitionDragLength), 0, 1);
         }
-
-        animateToProgress(startShift, endShift, duration, DEACCEL);
+        animateToProgress(startShift, endShift, duration, interpolator);
     }
 
     private void doLogGesture(boolean toLauncher) {
@@ -716,6 +723,12 @@
     /** Animates to the given progress, where 0 is the current app and 1 is overview. */
     private void animateToProgress(float start, float end, long duration,
             Interpolator interpolator) {
+        mRecentsAnimationWrapper.runOnInit(() -> animateToProgressInternal(start, end, duration,
+                interpolator));
+    }
+
+    private void animateToProgressInternal(float start, float end, long duration,
+            Interpolator interpolator) {
         mIsGoingToHome = Float.compare(end, 1) == 0;
         ObjectAnimator anim = mCurrentShift.animateToValue(start, end).setDuration(duration);
         anim.setInterpolator(interpolator);
@@ -727,7 +740,26 @@
                         : STATE_SCALED_CONTROLLER_APP);
             }
         });
-        mRecentsAnimationWrapper.runOnInit(anim::start);
+        anim.start();
+        long startMillis = SystemClock.uptimeMillis();
+        executeOnUiThread(() -> {
+            // Animate the launcher components at the same time as the window, always on UI thread.
+            if (mLauncherTransitionController != null && !mWasLauncherAlreadyVisible
+                    && start != end && duration > 0) {
+                // Adjust start progress and duration in case we are on a different thread.
+                long elapsedMillis = SystemClock.uptimeMillis() - startMillis;
+                elapsedMillis = Utilities.boundToRange(elapsedMillis, 0, duration);
+                float elapsedProgress = (float) elapsedMillis / duration;
+                float adjustedStart = Utilities.mapRange(elapsedProgress, start, end);
+                long adjustedDuration = duration - elapsedMillis;
+                // We want to use the same interpolator as the window, but need to adjust it to
+                // interpolate over the remaining progress (end - start).
+                mLauncherTransitionController.dispatchSetInterpolator(Interpolators.mapToProgress(
+                        interpolator, adjustedStart, end));
+                mLauncherTransitionController.getAnimationPlayer().setDuration(adjustedDuration);
+                mLauncherTransitionController.getAnimationPlayer().start();
+            }
+        });
     }
 
     @UiThread
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 4bd9a9b..5355c5e 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -272,6 +272,24 @@
         return scale;
     }
 
+    /**
+     * Maps t from one range to another range.
+     * @param t The value to map.
+     * @param fromMin The lower bound of the range that t is being mapped from.
+     * @param fromMax The upper bound of the range that t is being mapped from.
+     * @param toMin The lower bound of the range that t is being mapped to.
+     * @param toMax The upper bound of the range that t is being mapped to.
+     * @return The mapped value of t.
+     */
+    public static float mapToRange(float t, float fromMin, float fromMax, float toMin, float toMax) {
+        if (fromMin == fromMax || toMin == toMax) {
+            Log.e(TAG, "mapToRange: range has 0 length");
+            return toMin;
+        }
+        float progress = Math.abs(t - fromMin) / Math.abs(fromMax - fromMin);
+        return mapRange(progress, toMin, toMax);
+    }
+
     public static float mapRange(float value, float min, float max) {
         return min + (value * (max - min));
     }
@@ -463,6 +481,13 @@
     }
 
     /**
+     * @see #boundToRange(int, int, int).
+     */
+    public static long boundToRange(long value, long lowerBound, long upperBound) {
+        return Math.max(lowerBound, Math.min(value, upperBound));
+    }
+
+    /**
      * Wraps a message with a TTS span, so that a different message is spoken than
      * what is getting displayed.
      * @param msg original message
diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
index 84085cb..50fb0a5 100644
--- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java
+++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java
@@ -202,6 +202,19 @@
         }
     }
 
+    public void dispatchSetInterpolator(TimeInterpolator interpolator) {
+        dispatchSetInterpolatorRecursively(mAnim, interpolator);
+    }
+
+    private void dispatchSetInterpolatorRecursively(Animator anim, TimeInterpolator interpolator) {
+        anim.setInterpolator(interpolator);
+        if (anim instanceof AnimatorSet) {
+            for (Animator child : nonNullList(((AnimatorSet) anim).getChildAnimations())) {
+                dispatchSetInterpolatorRecursively(child, interpolator);
+            }
+        }
+    }
+
     public void setOnCancelRunnable(Runnable runnable) {
         mOnCancelRunnable = runnable;
     }
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index bace7df..d17572e 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -24,6 +24,8 @@
 import android.view.animation.OvershootInterpolator;
 import android.view.animation.PathInterpolator;
 
+import com.android.launcher3.Utilities;
+
 
 /**
  * Common interpolators used in Launcher
@@ -116,6 +118,19 @@
         return Math.abs(velocity) > FAST_FLING_PX_MS ? SCROLL : SCROLL_CUBIC;
     }
 
+    public static Interpolator overshootInterpolatorForVelocity(float velocity) {
+        return overshootInterpolatorForVelocity(velocity, 1f);
+    }
+
+    /**
+     * Create an OvershootInterpolator with tension directly related to the velocity (in px/ms).
+     * @param velocity The start velocity of the animation we want to overshoot.
+     * @param dampFactor An optional factor to reduce the amount of tension (how far we overshoot).
+     */
+    public static Interpolator overshootInterpolatorForVelocity(float velocity, float dampFactor) {
+        return new OvershootInterpolator(Math.min(Math.abs(velocity), 3f) / dampFactor);
+    }
+
     /**
      * Runs the given interpolator such that the entire progress is set between the given bounds.
      * That is, we set the interpolation to 0 until lowerBound and reach 1 by upperBound.
@@ -135,4 +150,15 @@
             return interpolator.getInterpolation((t - lowerBound) / (upperBound - lowerBound));
         };
     }
+
+    /**
+     * Runs the given interpolator such that the interpolated value is mapped to the given range.
+     * This is useful, for example, if we only use this interpolator for part of the animation,
+     * such as to take over a user-controlled animation when they let go.
+     */
+    public static Interpolator mapToProgress(Interpolator interpolator, float lowerBound,
+            float upperBound) {
+        return t -> Utilities.mapToRange(interpolator.getInterpolation(t), 0, 1,
+                lowerBound, upperBound);
+    }
 }
\ No newline at end of file