Updating the swipe-to-dismiss animation

Swipe to dismiss animates the page translation so that the
final position at the endof swipe is same as the final position
on removing the task view.

Change-Id: I393acd5ae75cd94797bb4f1aa3dd3ec9017cbb47
diff --git a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
index 66f3450..c8b54ad 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/OverviewSwipeController.java
@@ -18,14 +18,11 @@
 import static com.android.launcher3.LauncherState.ALL_APPS;
 import static com.android.launcher3.LauncherState.NORMAL;
 import static com.android.launcher3.LauncherState.OVERVIEW;
-import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
-import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.scrollInterpolatorForVelocity;
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.util.Log;
 import android.view.MotionEvent;
@@ -43,6 +40,7 @@
 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
 import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType;
 import com.android.launcher3.util.TouchController;
+import com.android.quickstep.PendingAnimation;
 import com.android.quickstep.views.RecentsView;
 import com.android.quickstep.views.TaskView;
 
@@ -65,6 +63,7 @@
     private final RecentsView mRecentsView;
     private final int[] mTempCords = new int[2];
 
+    private PendingAnimation mPendingAnimation;
     private AnimatorPlaybackController mCurrentAnimation;
     private boolean mCurrentAnimationIsGoingUp;
 
@@ -178,6 +177,11 @@
         if (mCurrentAnimation != null) {
             mCurrentAnimation.setPlayFraction(0);
         }
+        if (mPendingAnimation != null) {
+            mPendingAnimation.finish(false);
+            mPendingAnimation = null;
+        }
+
         mCurrentAnimationIsGoingUp = goingUp;
         float range = mLauncher.getAllAppsController().getShiftRange();
         long maxDuration = (long) (2 * range);
@@ -194,19 +198,11 @@
             }
         } else {
             if (goingUp) {
-                AnimatorSet anim = new AnimatorSet();
-                ObjectAnimator translate = ObjectAnimator.ofFloat(
-                        mTaskBeingDragged, View.TRANSLATION_Y, -mTaskBeingDragged.getBottom());
-                translate.setInterpolator(LINEAR);
-                translate.setDuration(maxDuration);
-                anim.play(translate);
-
-                ObjectAnimator alpha = ObjectAnimator.ofFloat(mTaskBeingDragged, View.ALPHA, 0);
-                alpha.setInterpolator(DEACCEL_1_5);
-                alpha.setDuration(maxDuration);
-                anim.play(alpha);
-                mCurrentAnimation = AnimatorPlaybackController.wrap(anim, maxDuration);
-                mEndDisplacement = -mTaskBeingDragged.getBottom();
+                mPendingAnimation = mRecentsView
+                        .createTaskDismissAnimation(mTaskBeingDragged, maxDuration);
+                mCurrentAnimation = AnimatorPlaybackController
+                        .wrap(mPendingAnimation.anim, maxDuration);
+                mEndDisplacement = -mTaskBeingDragged.getHeight();
             } else {
                 AnimatorSet anim = new AnimatorSet();
                 // TODO: Setup a zoom animation
@@ -292,15 +288,17 @@
     }
 
     private void onCurrentAnimationEnd(boolean wasSuccess, int logAction) {
+        if (mPendingAnimation != null) {
+            mPendingAnimation.finish(wasSuccess);
+            mPendingAnimation = null;
+        }
         if (mTaskBeingDragged == null) {
             LauncherState state = wasSuccess ?
                     (mCurrentAnimationIsGoingUp ? ALL_APPS : NORMAL) : OVERVIEW;
             mLauncher.getStateManager().goToState(state, false);
 
         } else if (wasSuccess) {
-            if (mCurrentAnimationIsGoingUp) {
-                mRecentsView.onTaskDismissed(mTaskBeingDragged);
-            } else {
+            if (!mCurrentAnimationIsGoingUp) {
                 mTaskBeingDragged.launchTask(false);
                 mLauncher.getUserEventDispatcher().logTaskLaunch(logAction,
                         Direction.DOWN, mTaskBeingDragged.getTask().getTopComponent());
diff --git a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
index 8b07934..b7f79b3 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/RecentsViewStateController.java
@@ -57,10 +57,7 @@
         setVisibility(state.overviewUi);
         setTransitionProgress(state.overviewUi ? 1 : 0);
         if (state.overviewUi) {
-            for (int i = 0; i < mRecentsView.getPageCount(); i++) {
-                ((TaskView) mRecentsView.getPageAt(i)).resetVisualProperties();
-            }
-            mRecentsView.updateCurveProperties();
+            mRecentsView.resetTaskVisuals();
         }
     }
 
diff --git a/quickstep/src/com/android/quickstep/PendingAnimation.java b/quickstep/src/com/android/quickstep/PendingAnimation.java
new file mode 100644
index 0000000..d22ef61
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/PendingAnimation.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep;
+
+import android.animation.AnimatorSet;
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import java.util.ArrayList;
+import java.util.function.Consumer;
+
+/**
+ * Utility class to keep track of a running animation.
+ *
+ * This class allows attaching end callbacks to an animation is intended to be used with
+ * {@link com.android.launcher3.anim.AnimatorPlaybackController}, since in that case
+ * AnimationListeners are not properly dispatched.
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class PendingAnimation {
+
+    private final ArrayList<Consumer<Boolean>> mEndListeners = new ArrayList<>();
+
+    public final AnimatorSet anim;
+
+    public PendingAnimation(AnimatorSet anim) {
+        this.anim = anim;
+    }
+
+    public void finish(boolean isSuccess) {
+        for (Consumer<Boolean> listeners : mEndListeners) {
+            listeners.accept(isSuccess);
+        }
+        mEndListeners.clear();
+    }
+
+    public void addEndListener(Consumer<Boolean> listener) {
+        mEndListeners.add(listener);
+    }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index fb0a757..23e6e5b 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -16,23 +16,32 @@
 
 package com.android.quickstep.views;
 
-import android.animation.LayoutTransition;
-import android.animation.LayoutTransition.TransitionListener;
+import static com.android.launcher3.anim.Interpolators.ACCEL;
+import static com.android.launcher3.anim.Interpolators.ACCEL_2;
+import static com.android.launcher3.anim.Interpolators.LINEAR;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.annotation.TargetApi;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
 import android.graphics.Rect;
+import android.os.Build;
 import android.util.AttributeSet;
 import android.util.SparseBooleanArray;
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 
 import com.android.launcher3.BaseActivity;
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.PagedView;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
+import com.android.quickstep.PendingAnimation;
 import com.android.quickstep.QuickScrubController;
 import com.android.quickstep.RecentsModel;
 import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
@@ -49,6 +58,7 @@
 /**
  * A list of recent tasks.
  */
+@TargetApi(Build.VERSION_CODES.P)
 public abstract class RecentsView<T extends BaseActivity>
         extends PagedView implements OnSharedPreferenceChangeListener {
 
@@ -90,14 +100,14 @@
 
     private boolean mOverviewStateEnabled;
     private boolean mTaskStackListenerRegistered;
-    private LayoutTransition mLayoutTransition;
     private Runnable mNextPageSwitchRunnable;
 
+    private PendingAnimation mPendingAnimation;
+
     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr) {
         super(context, attrs, defStyleAttr);
         setPageSpacing(getResources().getDimensionPixelSize(R.dimen.recents_page_spacing));
         enableFreeScroll(true);
-        setupLayoutTransition();
         setClipToOutline(true);
 
         mFastFlingVelocity = getResources()
@@ -136,33 +146,6 @@
         return null;
     }
 
-    private void setupLayoutTransition() {
-        // We want to show layout transitions when pages are deleted, to close the gap.
-        // TODO: We should this manually so we can control the animation (fill in the gap as the
-        // dismissing task is being tracked, and also so we can update the visible task data during
-        // the transition. For now, the workaround is to expand the visible tasks to load.
-        mLayoutTransition = new LayoutTransition();
-        mLayoutTransition.enableTransitionType(LayoutTransition.DISAPPEARING);
-        mLayoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
-
-        mLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
-        mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
-        mLayoutTransition.addTransitionListener(new TransitionListener() {
-            @Override
-            public void startTransition(LayoutTransition layoutTransition, ViewGroup viewGroup,
-                    View view, int i) {
-                loadVisibleTaskData();
-            }
-
-            @Override
-            public void endTransition(LayoutTransition layoutTransition, ViewGroup viewGroup,
-                    View view, int i) {
-                loadVisibleTaskData();
-            }
-        });
-        setLayoutTransition(mLayoutTransition);
-    }
-
     @Override
     protected void onWindowVisibilityChanged(int visibility) {
         super.onWindowVisibilityChanged(visibility);
@@ -231,6 +214,10 @@
     }
 
     private void applyLoadPlan(RecentsTaskLoadPlan loadPlan) {
+        if (mPendingAnimation != null) {
+            mPendingAnimation.addEndListener((b) -> applyLoadPlan(loadPlan));
+            return;
+        }
         TaskStack stack = loadPlan != null ? loadPlan.getTaskStack() : null;
         if (stack == null) {
             removeAllViews();
@@ -243,7 +230,6 @@
         // necessary)
         final LayoutInflater inflater = LayoutInflater.from(getContext());
         final ArrayList<Task> tasks = new ArrayList<>(stack.getTasks());
-        setLayoutTransition(null);
 
         final int requiredChildCount = tasks.size();
         for (int i = getChildCount(); i < requiredChildCount; i++) {
@@ -254,7 +240,6 @@
             final TaskView taskView = (TaskView) getChildAt(getChildCount() - 1);
             removeView(taskView);
         }
-        setLayoutTransition(mLayoutTransition);
 
         // Unload existing visible task data
         unloadVisibleTaskData();
@@ -265,12 +250,8 @@
             final Task task = tasks.get(i);
             final TaskView taskView = (TaskView) getChildAt(pageIndex);
             taskView.bind(task);
-            taskView.resetVisualProperties();
         }
-        updateCurveProperties();
-
-        // Update the set of visible task's data
-        loadVisibleTaskData();
+        resetTaskVisuals();
         applyIconScale(false /* animate */);
 
         if (oldChildCount != getChildCount()) {
@@ -278,6 +259,16 @@
         }
     }
 
+    public void resetTaskVisuals() {
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            ((TaskView) getChildAt(i)).resetVisualProperties();
+        }
+
+        updateCurveProperties();
+        // Update the set of visible task's data
+        loadVisibleTaskData();
+    }
+
     private void updateTaskStackListenerState() {
         boolean registerStackListener = mOverviewStateEnabled && isAttachedToWindow()
                 && getWindowVisibility() == VISIBLE;
@@ -375,7 +366,7 @@
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            int pageCenter = page.getLeft() + halfPageWidth;
+            float pageCenter = page.getLeft() + page.getTranslationX() + halfPageWidth;
             float distanceFromScreenCenter = screenCenter - pageCenter;
             float distanceToReachEdge = halfScreenWidth + halfPageWidth + pageSpacing;
             mScrollState.linearInterpolation = Math.min(1,
@@ -432,13 +423,6 @@
         mHasVisibleTaskData.clear();
     }
 
-    public void onTaskDismissed(TaskView taskView) {
-        ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id);
-        removeView(taskView);
-        if (getChildCount() == 0) {
-            onAllTasksRemoved();
-        }
-    }
 
     protected abstract void onAllTasksRemoved();
 
@@ -470,11 +454,9 @@
         if (getChildCount() == 0) {
             needsReload = true;
             // Add an empty view for now
-            setLayoutTransition(null);
             final TaskView taskView = (TaskView) LayoutInflater.from(getContext())
                     .inflate(R.layout.task, this, false);
             addView(taskView, 0);
-            setLayoutTransition(mLayoutTransition);
         }
         mRunningTaskId = runningTaskId;
         setCurrentPage(0);
@@ -529,4 +511,78 @@
          */
         public float linearInterpolation;
     }
+
+    public PendingAnimation createTaskDismissAnimation(TaskView taskView, long duration) {
+        if (FeatureFlags.IS_DOGFOOD_BUILD && mPendingAnimation != null) {
+            throw new IllegalStateException("Another pending animation is still running");
+        }
+        AnimatorSet anim = new AnimatorSet();
+        PendingAnimation pendingAnimation = new PendingAnimation(anim);
+
+        int count = getChildCount();
+        if (count == 0) {
+            return pendingAnimation;
+        }
+
+        int[] oldScroll = new int[count];
+        getPageScrolls(oldScroll, false, SIMPLE_SCROLL_LOGIC);
+
+        int[] newScroll = new int[count];
+        getPageScrolls(newScroll, false, (v) -> v.getVisibility() != GONE && v != taskView);
+
+        int maxScrollDiff = 0;
+        int lastPage = mIsRtl ? 0 : count - 1;
+        if (getChildAt(lastPage) == taskView) {
+            if (count > 1) {
+                int secondLastPage = mIsRtl ? 1 : count - 2;
+                maxScrollDiff = oldScroll[lastPage] - newScroll[secondLastPage];
+            }
+        }
+
+        boolean needsCurveUpdates = false;
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            if (child == taskView) {
+                addAnim(ObjectAnimator.ofFloat(taskView, ALPHA, 0), duration, ACCEL_2, anim);
+                addAnim(ObjectAnimator.ofFloat(taskView, TRANSLATION_Y, -taskView.getHeight()),
+                        duration, LINEAR, anim);
+            } else {
+                int scrollDiff = newScroll[i] - oldScroll[i] + maxScrollDiff;
+                if (scrollDiff != 0) {
+                    addAnim(ObjectAnimator.ofFloat(child, TRANSLATION_X, scrollDiff),
+                            duration, ACCEL, anim);
+                    needsCurveUpdates = true;
+                }
+            }
+        }
+
+        if (needsCurveUpdates) {
+            ValueAnimator va = ValueAnimator.ofFloat(0, 1);
+            va.addUpdateListener((a) -> updateCurveProperties());
+            anim.play(va);
+        }
+
+        // Add a tiny bit of translation Z, so that it draws on top of other views
+        taskView.setTranslationZ(0.1f);
+
+        mPendingAnimation = pendingAnimation;
+        mPendingAnimation.addEndListener((isSuccess) -> {
+           if (isSuccess) {
+               ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id);
+               removeView(taskView);
+               if (getChildCount() == 0) {
+                   onAllTasksRemoved();
+               }
+           }
+           resetTaskVisuals();
+           mPendingAnimation = null;
+        });
+        return pendingAnimation;
+    }
+
+    private static void addAnim(ObjectAnimator anim, long duration,
+            TimeInterpolator interpolator, AnimatorSet set) {
+        anim.setDuration(duration).setInterpolator(interpolator);
+        set.play(anim);
+    }
 }
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 4d734a2..7a575ad 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -154,6 +154,7 @@
         setScaleY(1f);
         setTranslationX(0f);
         setTranslationY(0f);
+        setTranslationZ(0);
         setAlpha(1f);
     }
 
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 30f9c8e..6e8cacd 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -56,7 +56,9 @@
 public abstract class PagedView<T extends View & PageIndicator> extends ViewGroup {
     private static final String TAG = "PagedView";
     private static final boolean DEBUG = false;
+
     protected static final int INVALID_PAGE = -1;
+    protected static final ComputePageScrollsLogic SIMPLE_SCROLL_LOGIC = (v) -> v.getVisibility() != GONE;
 
     public static final int PAGE_SNAP_ANIMATION_DURATION = 750;
     public static final int SLOW_PAGE_SNAP_ANIMATION_DURATION = 950;
@@ -540,43 +542,13 @@
         if (DEBUG) Log.d(TAG, "PagedView.onLayout()");
         final int childCount = getChildCount();
 
-        final int startIndex = mIsRtl ? childCount - 1 : 0;
-        final int endIndex = mIsRtl ? -1 : childCount;
-        final int delta = mIsRtl ? -1 : 1;
-
-        int verticalPadding = getPaddingTop() + getPaddingBottom();
-
-        int scrollOffsetLeft = mInsets.left + getPaddingLeft();
-        int childLeft = scrollOffsetLeft;
-
         boolean pageScrollChanged = false;
         if (mPageScrolls == null || childCount != mChildCountOnLastLayout) {
             mPageScrolls = new int[childCount];
             pageScrollChanged = true;
         }
-
-        for (int i = startIndex; i != endIndex; i += delta) {
-            final View child = getPageAt(i);
-            if (child.getVisibility() != View.GONE) {
-                int childTop = getPaddingTop() + mInsets.top;
-                childTop += (getMeasuredHeight() - mInsets.top - mInsets.bottom - verticalPadding
-                        - child.getMeasuredHeight()) / 2;
-
-                final int childWidth = child.getMeasuredWidth();
-                final int childHeight = child.getMeasuredHeight();
-
-                if (DEBUG) Log.d(TAG, "\tlayout-child" + i + ": " + childLeft + ", " + childTop);
-                child.layout(childLeft, childTop,
-                        childLeft + child.getMeasuredWidth(), childTop + childHeight);
-
-                final int pageScroll = childLeft - scrollOffsetLeft;
-                if (mPageScrolls[i] != pageScroll) {
-                    pageScrollChanged = true;
-                    mPageScrolls[i] = pageScroll;
-                }
-
-                childLeft += childWidth + mPageSpacing + getChildGap();
-            }
+        if (getPageScrolls(mPageScrolls, true, SIMPLE_SCROLL_LOGIC)) {
+            pageScrollChanged = true;
         }
 
         final LayoutTransition transition = getLayoutTransition();
@@ -614,6 +586,51 @@
         mChildCountOnLastLayout = childCount;
     }
 
+    /**
+     * Initializes {@code outPageScrolls} with scroll positions for view at that index. The length
+     * of {@code outPageScrolls} should be same as the the childCount
+     *
+     */
+    protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
+            ComputePageScrollsLogic scrollLogic) {
+        final int childCount = getChildCount();
+
+        final int startIndex = mIsRtl ? childCount - 1 : 0;
+        final int endIndex = mIsRtl ? -1 : childCount;
+        final int delta = mIsRtl ? -1 : 1;
+
+        int verticalPadding = getPaddingTop() + getPaddingBottom();
+
+        int scrollOffsetLeft = mInsets.left + getPaddingLeft();
+        int childLeft = scrollOffsetLeft;
+        boolean pageScrollChanged = false;
+
+        for (int i = startIndex; i != endIndex; i += delta) {
+            final View child = getPageAt(i);
+            if (scrollLogic.shouldIncludeView(child)) {
+                int childTop = getPaddingTop() + mInsets.top;
+                childTop += (getMeasuredHeight() - mInsets.top - mInsets.bottom - verticalPadding
+                        - child.getMeasuredHeight()) / 2;
+                final int childWidth = child.getMeasuredWidth();
+
+                if (layoutChildren) {
+                    final int childHeight = child.getMeasuredHeight();
+                    child.layout(childLeft, childTop,
+                            childLeft + child.getMeasuredWidth(), childTop + childHeight);
+                }
+
+                final int pageScroll = childLeft - scrollOffsetLeft;
+                if (outPageScrolls[i] != pageScroll) {
+                    pageScrollChanged = true;
+                    outPageScrolls[i] = pageScroll;
+                }
+
+                childLeft += childWidth + mPageSpacing + getChildGap();
+            }
+        }
+        return pageScrollChanged;
+    }
+
     protected int getChildGap() {
         return 0;
     }
@@ -1525,4 +1542,9 @@
     public boolean onHoverEvent(android.view.MotionEvent event) {
         return true;
     }
+
+    protected interface ComputePageScrollsLogic {
+
+        boolean shouldIncludeView(View view);
+    }
 }