Animate task translation on dismissal.

Test: manual
Bug: 186416527
Fix: 186416527
Change-Id: I3919b96b96fdd490e15968b7c2e680dad7f6e5d6
diff --git a/quickstep/res/values/dimens.xml b/quickstep/res/values/dimens.xml
index c459472..7c9a2b8 100644
--- a/quickstep/res/values/dimens.xml
+++ b/quickstep/res/values/dimens.xml
@@ -39,6 +39,11 @@
     <dimen name="overview_grid_focus_vertical_margin">90dp</dimen>
     <dimen name="split_placeholder_size">110dp</dimen>
 
+    <!-- These speeds are in dp/s -->
+    <dimen name="max_task_dismiss_drag_velocity">2.25dp</dimen>
+    <dimen name="default_task_dismiss_drag_velocity">1.75dp</dimen>
+    <dimen name="default_task_dismiss_drag_velocity_grid">0.75dp</dimen>
+
     <dimen name="recents_page_spacing">16dp</dimen>
     <dimen name="recents_clear_all_deadzone_vertical_margin">70dp</dimen>
 
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 70b3870..c6ea953 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -28,6 +28,7 @@
 import com.android.launcher3.AbstractFloatingView;
 import com.android.launcher3.BaseDraggingActivity;
 import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
@@ -50,6 +51,10 @@
         extends AnimatorListenerAdapter implements TouchController,
         SingleAxisSwipeDetector.Listener {
 
+    private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f;
+    private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300;
+    private static final long MAX_TASK_DISMISS_ANIMATION_DURATION = 600;
+
     protected final T mActivity;
     private final SingleAxisSwipeDetector mDetector;
     private final RecentsView mRecentsView;
@@ -277,14 +282,32 @@
         } else {
             mFlingBlockCheck.onEvent();
         }
-        mCurrentAnimation.setPlayFraction(Utilities.boundToRange(
-                totalDisplacement * mProgressMultiplier, 0, 1));
+
+        // Once halfway through task dismissal interpolation, switch from reversible dragging-task
+        // animation to playing the remaining task translation animations
+        if (mCurrentAnimation.getProgressFraction() < ANIMATION_PROGRESS_FRACTION_MIDPOINT) {
+            // Halve the value as we are animating the drag across the full length for only the
+            // first half of the progress
+            mCurrentAnimation.setPlayFraction(
+                    Utilities.boundToRange(totalDisplacement * mProgressMultiplier / 2, 0, 1));
+        } else {
+            float dragVelocity = -mTaskBeingDragged.getResources().getDimension(
+                    mRecentsView.showAsGrid() ? R.dimen.default_task_dismiss_drag_velocity_grid
+                            : R.dimen.default_task_dismiss_drag_velocity);
+            onDragEnd(dragVelocity);
+            return true;
+        }
 
         return true;
     }
 
     @Override
     public void onDragEnd(float velocity) {
+        // Limit velocity, as very large scalar values make animations play too quickly
+        float maxTaskDismissDragVelocity = mTaskBeingDragged.getResources().getDimension(
+                R.dimen.max_task_dismiss_drag_velocity);
+        velocity = Utilities.boundToRange(velocity, -maxTaskDismissDragVelocity,
+                maxTaskDismissDragVelocity);
         boolean fling = mDetector.isFling(velocity);
         final boolean goingToEnd;
         boolean blockedFling = fling && mFlingBlockCheck.isBlocked();
@@ -305,6 +328,11 @@
         if (blockedFling && !goingToEnd) {
             animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity);
         }
+        // Due to very high or low velocity dismissals, animation durations can be inconsistently
+        // long or short. Bound the duration for animation of task translations for a more
+        // standardized feel.
+        animationDuration = Utilities.boundToRange(animationDuration,
+                MIN_TASK_DISMISS_ANIMATION_DURATION, MAX_TASK_DISMISS_ANIMATION_DURATION);
 
         mCurrentAnimation.setEndAction(this::clearState);
         mCurrentAnimation.startWithVelocity(mActivity, goingToEnd,
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 5bad5e8..b2e4c42 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -32,11 +32,12 @@
 import static com.android.launcher3.Utilities.squaredHypot;
 import static com.android.launcher3.Utilities.squaredTouchSlop;
 import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_0_5;
 import static com.android.launcher3.anim.Interpolators.ACCEL_0_75;
-import static com.android.launcher3.anim.Interpolators.ACCEL_2;
 import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
+import static com.android.launcher3.anim.Interpolators.clampToProgress;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_CLEAR_ALL;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP;
 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN;
@@ -356,6 +357,11 @@
     // OverScroll constants
     private static final int OVERSCROLL_PAGE_SNAP_ANIMATION_DURATION = 270;
 
+    private static final int DISMISS_TASK_DURATION = 300;
+    private static final int ADDITION_TASK_DURATION = 200;
+    private static final float INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.55f;
+    private static final float ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET = 0.05f;
+
     protected final RecentsOrientedState mOrientationState;
     protected final BaseActivityInterface<STATE_TYPE, ACTIVITY_TYPE> mSizeStrategy;
     protected RecentsAnimationController mRecentsAnimationController;
@@ -379,10 +385,6 @@
     private final List<OnScrollChangedListener> mScrollListeners = new ArrayList<>();
     private float mFullscreenScale;
 
-    private static final int DISMISS_TASK_DURATION = 300;
-    private static final int DISMISS_TASK_TRANSLATION_DURATION = 200;
-    private static final int ADDITIONAL_DISMISS_TASK_TRANSLATION_DURATION = 75;
-    private static final int ADDITION_TASK_DURATION = 200;
     // The threshold at which we update the SystemUI flags when animating from the task into the app
     public static final float UPDATE_SYSUI_FLAGS_THRESHOLD = 0.85f;
 
@@ -1909,21 +1911,18 @@
      * This method is used when no task dismissal has occurred.
      */
     private void updateGridProperties() {
-        updateGridProperties(null, -1);
+        updateGridProperties(false);
     }
 
     /**
      * Updates TaskView and ClearAllButton scaling and translation required to turn into grid
      * layout.
      * This method only calculates the potential position and depends on {@link #setGridProgress} to
-     * apply the actual scaling and translation. This adds task translation animations in the case
-     * of task dismissals: e.g. when dismissedTask is not null.
+     * apply the actual scaling and translation.
      *
-     * @param dismissedTask the TaskView dismissed, possibly null
-     * @param dismissedIndex the index at which the dismissedTask was prior to dismissal, if no
-     *                       dismissal occurred, this is unused
+     * @param isTaskDismissal indicates if update was called due to task dismissal
      */
-    private void updateGridProperties(TaskView dismissedTask, int dismissedIndex) {
+    private void updateGridProperties(boolean isTaskDismissal) {
         int taskCount = getTaskViewCount();
         if (taskCount == 0) {
             return;
@@ -1963,10 +1962,6 @@
         int snappedPage = getNextPage();
         TaskView snappedTaskView = getTaskViewAtByAbsoluteIndex(snappedPage);
 
-        boolean isTaskDismissal = dismissedTask != null;
-        float dismissedTaskWidth =
-                isTaskDismissal ? dismissedTask.getLayoutParams().width + mPageSpacing : 0;
-
         if (!isTaskDismissal) {
             mTopRowIdSet.clear();
         }
@@ -2062,34 +2057,13 @@
             snappedTaskGridTranslationX = gridTranslations[snappedPage - mTaskViewStartIndex];
         }
 
-        // Animate task dismissTranslationX for tasks with index >= dismissed index and in the
-        // same row as the dismissed index, or if the dismissed task was the focused task. Offset
-        // successive task dismissal durations for a staggered effect.
-        ArrayList<Animator> gridTranslationAnimators = new ArrayList<>();
-        boolean isFocusedTaskDismissed =
-                isTaskDismissal && dismissedTask.getTask().key.id == mFocusedTaskId;
         for (int i = 0; i < taskCount; i++) {
             TaskView taskView = getTaskViewAt(i);
-            if (isFocusedTaskDismissed || (i >= dismissedIndex && isSameGridRow(dismissedTask,
-                    taskView))) {
-                Animator taskDismissAnimator = ObjectAnimator.ofFloat(taskView,
-                        taskView.getPrimaryDismissTranslationProperty(),
-                        mIsRtl ? -dismissedTaskWidth : dismissedTaskWidth, 0f);
-                int additionalTranslationDuration =
-                        i >= dismissedIndex ? (ADDITIONAL_DISMISS_TASK_TRANSLATION_DURATION * (
-                                (i - dismissedIndex) / 2)) : 0;
-                taskDismissAnimator.setDuration(
-                        DISMISS_TASK_TRANSLATION_DURATION + additionalTranslationDuration);
-                gridTranslationAnimators.add(taskDismissAnimator);
-            }
             taskView.setGridTranslationX(gridTranslations[i] - snappedTaskGridTranslationX);
             taskView.getPrimaryNonFullscreenTranslationProperty().set(taskView,
                     snappedTaskFullscreenScrollAdjustment);
             taskView.getSecondaryNonFullscreenTranslationProperty().set(taskView, 0f);
         }
-        AnimatorSet gridTranslationAnimatorSet = new AnimatorSet();
-        gridTranslationAnimatorSet.playTogether(gridTranslationAnimators);
-        gridTranslationAnimatorSet.start();
 
         // Use the accumulated translation of the row containing the last task.
         float clearAllAccumulatedTranslation = topSet.contains(taskCount - 1)
@@ -2233,7 +2207,7 @@
             PendingAnimation anim) {
         // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
         // alpha is set to 0 so that it can be recycled in the view pool properly
-        anim.setFloat(taskView, VIEW_ALPHA, 0, ACCEL_2);
+        anim.setFloat(taskView, VIEW_ALPHA, 0, clampToProgress(ACCEL, 0, 0.5f));
         SplitSelectStateController splitController = mSplitPlaceholderView.getSplitController();
 
         ResourceProvider rp = DynamicResource.provider(mActivity);
@@ -2269,8 +2243,10 @@
                     throw new IllegalStateException("Invalid split task translation: " + dir);
             }
         }
+        // Double translation distance so dismissal drag is the full height, as we only animate
+        // the drag for the first half of the progress.
         anim.add(ObjectAnimator.ofFloat(taskView, dismissingTaskViewTranslate,
-                positiveNegativeFactor * translateDistance).setDuration(duration), LINEAR, sp);
+                positiveNegativeFactor * translateDistance * 2).setDuration(duration), LINEAR, sp);
 
         if (LIVE_TILE.get() && taskView.isRunningTask()) {
             anim.addOnFrameCallback(() -> {
@@ -2306,6 +2282,11 @@
         }
         int draggedIndex = indexOfChild(taskView);
 
+        boolean isFocusedTaskDismissed = taskView.getTask().key.id == mFocusedTaskId;
+        if (isFocusedTaskDismissed && showAsGrid()) {
+            anim.setFloat(mActionsView, VIEW_ALPHA, 0, clampToProgress(ACCEL_0_5, 0, 0.5f));
+        }
+        float dismissedTaskWidth = taskView.getLayoutParams().width + mPageSpacing;
         boolean needsCurveUpdates = false;
         for (int i = 0; i < count; i++) {
             View child = getChildAt(i);
@@ -2314,7 +2295,7 @@
                     addDismissedTaskAnimations(taskView, duration, anim);
                 }
             } else if (!showAsGrid()) {
-                // For grid layout, don't animate other tasks when dismissing in grid for now.
+                // Compute scroll offsets from task dismissal for animation.
                 // If we just take newScroll - oldScroll, everything to the right of dragged task
                 // translates to the left. We need to offset this in some cases:
                 // - In RTL, add page offset to all pages, since we want pages to move to the right
@@ -2341,15 +2322,31 @@
                             ? ((TaskView) child).getPrimaryDismissTranslationProperty()
                             : mOrientationHandler.getPrimaryViewTranslate();
 
-                    ResourceProvider rp = DynamicResource.provider(mActivity);
-                    SpringProperty sp = new SpringProperty(SpringProperty.FLAG_CAN_SPRING_ON_END)
-                            .setDampingRatio(
-                                    rp.getFloat(R.dimen.dismiss_task_trans_x_damping_ratio))
-                            .setStiffness(rp.getFloat(R.dimen.dismiss_task_trans_x_stiffness));
-                    anim.add(ObjectAnimator.ofFloat(child, translationProperty, scrollDiff)
-                            .setDuration(duration), ACCEL, sp);
+                    float additionalDismissDuration =
+                            ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
+                                    i - draggedIndex);
+                    anim.setFloat(child, translationProperty, scrollDiff, clampToProgress(LINEAR,
+                            Utilities.boundToRange(INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+                                    + additionalDismissDuration, 0f, 1f), 1));
                     needsCurveUpdates = true;
                 }
+            } else if (child instanceof TaskView) {
+                // Animate task with index >= dismissed index and in the same row as the
+                // dismissed index, or if the dismissed task was the focused task. Offset
+                // successive task dismissal durations for a staggered effect.
+                if (isFocusedTaskDismissed || (i >= draggedIndex && isSameGridRow((TaskView) child,
+                        taskView))) {
+                    FloatProperty translationProperty =
+                            ((TaskView) child).getPrimaryDismissTranslationProperty();
+                    float additionalDismissDuration =
+                            ADDITIONAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET * Math.abs(
+                                    i - draggedIndex);
+                    anim.setFloat(child, translationProperty,
+                            !mIsRtl ? -dismissedTaskWidth : dismissedTaskWidth,
+                            clampToProgress(LINEAR, Utilities.boundToRange(
+                                    INITIAL_DISMISS_TRANSLATION_INTERPOLATION_OFFSET
+                                            + additionalDismissDuration, 0f, 1f), 1));
+                }
             }
         }
 
@@ -2387,6 +2384,10 @@
                         }
                     }
 
+                    // Reset task translations as they may have updated via animations in
+                    // createTaskDismissAnimation
+                    resetTaskVisuals();
+
                     int pageToSnapTo = mCurrentPage;
                     // Snap to start if focused task was dismissed, as after quick switch it could
                     // be at any page but the focused task always displays at the start.
@@ -2404,7 +2405,7 @@
                     } else {
                         snapToPageImmediately(pageToSnapTo);
                         // Grid got messed up, reapply.
-                        updateGridProperties(taskView, draggedIndex - mTaskViewStartIndex);
+                        updateGridProperties(true);
                         if (showAsGrid() && getFocusedTaskView() == null
                                 && mActionsView.getVisibilityAlpha().getValue() == 1) {
                             animateActionsViewOut();
@@ -2414,9 +2415,6 @@
                     // immediately available.
                     onLayout(false /*  changed */, getLeft(), getTop(), getRight(), getBottom());
                 }
-                if (!showAsGrid()) {
-                    resetTaskVisuals();
-                }
                 onDismissAnimationEnds();
                 mPendingAnimation = null;
             }
@@ -3794,7 +3792,8 @@
         return mColorTint;
     }
 
-    private boolean showAsGrid() {
+    /** Returns {@code true} if the overview tasks are displayed as a grid. */
+    public boolean showAsGrid() {
         return mOverviewGridEnabled || (mCurrentGestureEndTarget != null
                 && mSizeStrategy.stateFromGestureEndTarget(
                 mCurrentGestureEndTarget).displayOverviewTasksAsGrid(mActivity.getDeviceProfile()));
diff --git a/src/com/android/launcher3/anim/Interpolators.java b/src/com/android/launcher3/anim/Interpolators.java
index 9d73bba..1e7b224 100644
--- a/src/com/android/launcher3/anim/Interpolators.java
+++ b/src/com/android/launcher3/anim/Interpolators.java
@@ -35,6 +35,7 @@
     public static final Interpolator LINEAR = new LinearInterpolator();
 
     public static final Interpolator ACCEL = new AccelerateInterpolator();
+    public static final Interpolator ACCEL_0_5 = new AccelerateInterpolator(0.5f);
     public static final Interpolator ACCEL_0_75 = new AccelerateInterpolator(0.75f);
     public static final Interpolator ACCEL_1_5 = new AccelerateInterpolator(1.5f);
     public static final Interpolator ACCEL_2 = new AccelerateInterpolator(2);
@@ -149,11 +150,15 @@
      */
     public static Interpolator clampToProgress(Interpolator interpolator, float lowerBound,
             float upperBound) {
-        if (upperBound <= lowerBound) {
-            throw new IllegalArgumentException(String.format(
-                    "lowerBound (%f) must be less than upperBound (%f)", lowerBound, upperBound));
+        if (upperBound < lowerBound) {
+            throw new IllegalArgumentException(
+                    String.format("upperBound (%f) must be greater than lowerBound (%f)",
+                            upperBound, lowerBound));
         }
         return t -> {
+            if (t == lowerBound && t == upperBound) {
+                return t == 0f ? 0 : 1;
+            }
             if (t < lowerBound) {
                 return 0;
             }