Merge "Refactoring and unifying TaskView animations."
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 40e8b50..955af82 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -172,7 +172,7 @@
     <integer name="recents_nav_bar_scrim_enter_duration">400</integer>
 
     <!-- The animation duration for animating the removal of a task view. -->
-    <integer name="recents_animate_task_view_remove_duration">250</integer>
+    <integer name="recents_animate_task_view_remove_duration">175</integer>
 
     <!-- The animation duration for scrolling the stack to a particular item. -->
     <integer name="recents_animate_task_stack_scroll_duration">200</integer>
diff --git a/packages/SystemUI/src/com/android/systemui/recents/events/activity/DismissRecentsToHomeAnimationStarted.java b/packages/SystemUI/src/com/android/systemui/recents/events/activity/DismissRecentsToHomeAnimationStarted.java
index 992afb4..e7be858 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/events/activity/DismissRecentsToHomeAnimationStarted.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/events/activity/DismissRecentsToHomeAnimationStarted.java
@@ -28,5 +28,4 @@
     public DismissRecentsToHomeAnimationStarted(boolean animated) {
         this.animated = animated;
     }
-
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/ReferenceCountedTrigger.java b/packages/SystemUI/src/com/android/systemui/recents/misc/ReferenceCountedTrigger.java
index 90eaca7..2637d88 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/ReferenceCountedTrigger.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/ReferenceCountedTrigger.java
@@ -18,8 +18,6 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.content.Context;
-import android.util.Log;
 
 import java.util.ArrayList;
 
@@ -107,16 +105,20 @@
         mLastDecRunnables.clear();
     }
 
-    /** Convenience method to decrement this trigger as a runnable. */
-    public Runnable decrementAsRunnable() {
-        return mDecrementRunnable;
-    }
-    /** Convenience method to decrement this trigger as a animator listener. */
+    /**
+     * Convenience method to decrement this trigger as a animator listener.  This listener is
+     * guarded to prevent being called back multiple times, and will trigger a decrement once and
+     * only once.
+     */
     public Animator.AnimatorListener decrementOnAnimationEnd() {
         return new AnimatorListenerAdapter() {
+            private boolean hasEnded;
+
             @Override
             public void onAnimationEnd(Animator animation) {
+                if (hasEnded) return;
                 decrement();
+                hasEnded = true;
             }
         };
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
index 2bf2ccb..086fb58 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/Utilities.java
@@ -88,6 +88,15 @@
     }
 
     /**
+     * Cancels an animation.
+     */
+    public static void cancelAnimation(Animator animator) {
+        if (animator != null) {
+            animator.cancel();
+        }
+    }
+
+    /**
      * Cancels an animation ensuring that if it has listeners, onCancel and onEnd
      * are not called.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
index d6262ac..d8dfce5 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
@@ -25,7 +25,6 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.ArraySet;
-import android.util.Log;
 import com.android.systemui.Prefs;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsConfiguration;
@@ -115,8 +114,6 @@
      * - least-recent to most-recent freeform tasks
      */
     public synchronized void preloadPlan(RecentsTaskLoader loader, boolean isTopTaskHome) {
-        RecentsConfiguration config = Recents.getConfiguration();
-        SystemServicesProxy ssp = Recents.getSystemServices();
         Resources res = mContext.getResources();
         ArrayList<Task> allTasks = new ArrayList<>();
         if (mRawTasks == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
index d030fc1..f7e2b9d 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
@@ -230,8 +230,7 @@
     public void notifyTaskDataUnloaded(Bitmap defaultThumbnail, Drawable defaultApplicationIcon) {
         icon = defaultApplicationIcon;
         thumbnail = defaultThumbnail;
-        int callbackCount = mCallbacks.size();
-        for (int i = 0; i < callbackCount; i++) {
+        for (int i = mCallbacks.size() - 1; i >= 0; i--) {
             mCallbacks.get(i).onTaskDataUnloaded();
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
index 5e720cb..bae4784 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/TaskStack.java
@@ -564,6 +564,22 @@
     }
 
     /**
+     * Returns the set of "freeform" tasks in the stack.
+     */
+    public ArrayList<Task> getFreeformTasks() {
+        ArrayList<Task> freeformTasks = new ArrayList<>();
+        ArrayList<Task> tasks = mStackTaskList.getTasks();
+        int taskCount = tasks.size();
+        for (int i = 0; i < taskCount; i++) {
+            Task task = tasks.get(i);
+            if (task.isFreeformTask()) {
+                freeformTasks.add(task);
+            }
+        }
+        return freeformTasks;
+    }
+
+    /**
      * Computes a set of all the active and historical tasks ordered by their last active time.
      */
     public ArrayList<Task> computeAllTasksList() {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java b/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
index c0b8a9d..b8bbf51 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/AnimateableViewBounds.java
@@ -18,8 +18,6 @@
 
 import android.graphics.Outline;
 import android.graphics.Rect;
-import android.util.IntProperty;
-import android.util.Property;
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
@@ -29,23 +27,11 @@
     View mSourceView;
     Rect mClipRect = new Rect();
     Rect mClipBounds = new Rect();
+    Rect mLastClipBounds = new Rect();
     int mCornerRadius;
     float mAlpha = 1f;
     final float mMinAlpha = 0.25f;
 
-    public static final Property<AnimateableViewBounds, Integer> CLIP_BOTTOM =
-            new IntProperty<AnimateableViewBounds>("clipBottom") {
-                @Override
-                public void setValue(AnimateableViewBounds object, int clip) {
-                    object.setClipBottom(clip, false /* force */);
-                }
-
-                @Override
-                public Integer get(AnimateableViewBounds object) {
-                    return object.getClipBottom();
-                }
-            };
-
     public AnimateableViewBounds(View source, int cornerRadius) {
         mSourceView = source;
         mCornerRadius = cornerRadius;
@@ -77,11 +63,9 @@
     }
 
     /** Sets the bottom clip. */
-    public void setClipBottom(int bottom, boolean force) {
-        if (bottom != mClipRect.bottom || force) {
-            mClipRect.bottom = bottom;
-            updateClipBounds();
-        }
+    public void setClipBottom(int bottom) {
+        mClipRect.bottom = bottom;
+        updateClipBounds();
     }
 
     /** Returns the bottom clip. */
@@ -93,7 +77,10 @@
         mClipBounds.set(Math.max(0, mClipRect.left), Math.max(0, mClipRect.top),
                 mSourceView.getWidth() - Math.max(0, mClipRect.right),
                 mSourceView.getHeight() - Math.max(0, mClipRect.bottom));
-        mSourceView.setClipBounds(mClipBounds);
-        mSourceView.invalidateOutline();
+        if (!mLastClipBounds.equals(mClipBounds)) {
+            mSourceView.setClipBounds(mClipBounds);
+            mSourceView.invalidateOutline();
+            mLastClipBounds.set(mClipBounds);
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
index 15b4ee3..f49d98e 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsView.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.recents.views;
 
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -59,6 +61,7 @@
 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
 import com.android.systemui.recents.misc.ReferenceCountedTrigger;
 import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskStack;
 import com.android.systemui.stackdivider.WindowManagerProxy;
@@ -116,7 +119,6 @@
 
     public RecentsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
-        Resources res = context.getResources();
         setWillNotDraw(false);
         mHandler = new Handler();
         mTransitionHelper = new RecentsTransitionHelper(getContext(), mHandler);
@@ -491,23 +493,25 @@
 
         // Handle the case where we drop onto a dock region
         if (event.dropTarget instanceof TaskStack.DockState) {
-            final TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget;
+            TaskStack.DockState dockState = (TaskStack.DockState) event.dropTarget;
+            TaskStackLayoutAlgorithm stackLayout = mTaskStackView.getStackAlgorithm();
+            TaskStackViewScroller stackScroller = mTaskStackView.getScroller();
+            TaskViewTransform tmpTransform = new TaskViewTransform();
 
-            // Remove the task after it is docked
-            event.taskView.animate()
-                    .alpha(0f)
-                    .setDuration(150)
-                    .setInterpolator(mFastOutLinearInInterpolator)
-                    .setUpdateListener(null)
-                    .setListener(null)
-                    .withEndAction(new Runnable() {
-                        @Override
-                        public void run() {
-                            mTaskStackView.getStack().removeTask(event.task);
-                        }
-                    })
-                    .withLayer()
-                    .start();
+            // Remove the task view after it is docked
+            stackLayout.getStackTransform(event.task, stackScroller.getStackScroll(), tmpTransform,
+                    null);
+            tmpTransform.scale = event.taskView.getScaleX();
+            tmpTransform.rect.offset(event.taskView.getTranslationX(),
+                    event.taskView.getTranslationY());
+            mTaskStackView.updateTaskViewToTransform(event.taskView, tmpTransform,
+                    new TaskViewAnimation(150, mFastOutLinearInInterpolator,
+                            new AnimatorListenerAdapter() {
+                                @Override
+                                public void onAnimationEnd(Animator animation) {
+                                    mTaskStackView.getStack().removeTask(event.task);
+                                }
+                            }));
 
             // Dock the task and launch it
             SystemServicesProxy ssp = Recents.getSystemServices();
@@ -587,21 +591,23 @@
 
     private void showHistoryButton(final int duration,
             final ReferenceCountedTrigger postHideHistoryAnimationTrigger) {
-        mHistoryButton.setVisibility(View.VISIBLE);
-        mHistoryButton.setAlpha(0f);
         mHistoryButton.setText(getContext().getString(R.string.recents_history_label_format,
                 mStack.getHistoricalTasks().size()));
-        postHideHistoryAnimationTrigger.addLastDecrementRunnable(new Runnable() {
-            @Override
-            public void run() {
-                mHistoryButton.animate()
-                        .alpha(1f)
-                        .setDuration(duration)
-                        .setInterpolator(mFastOutSlowInInterpolator)
-                        .withLayer()
-                        .start();
-            }
-        });
+        if (mHistoryButton.getVisibility() == View.INVISIBLE) {
+            mHistoryButton.setVisibility(View.VISIBLE);
+            mHistoryButton.setAlpha(0f);
+            postHideHistoryAnimationTrigger.addLastDecrementRunnable(new Runnable() {
+                @Override
+                public void run() {
+                    mHistoryButton.animate()
+                            .alpha(1f)
+                            .setDuration(duration)
+                            .setInterpolator(mFastOutSlowInInterpolator)
+                            .withLayer()
+                            .start();
+                }
+            });
+        }
     }
 
     /**
@@ -615,20 +621,22 @@
 
     private void hideHistoryButton(int duration,
             final ReferenceCountedTrigger postHideStackAnimationTrigger) {
-        mHistoryButton.animate()
-                .alpha(0f)
-                .setDuration(duration)
-                .setInterpolator(mFastOutLinearInInterpolator)
-                .withEndAction(new Runnable() {
-                    @Override
-                    public void run() {
-                        mHistoryButton.setVisibility(View.INVISIBLE);
-                        postHideStackAnimationTrigger.decrement();
-                    }
-                })
-                .withLayer()
-                .start();
-        postHideStackAnimationTrigger.increment();
+        if (mHistoryButton.getVisibility() == View.VISIBLE) {
+            mHistoryButton.animate()
+                    .alpha(0f)
+                    .setDuration(duration)
+                    .setInterpolator(mFastOutLinearInInterpolator)
+                    .withEndAction(new Runnable() {
+                        @Override
+                        public void run() {
+                            mHistoryButton.setVisibility(View.INVISIBLE);
+                            postHideStackAnimationTrigger.decrement();
+                        }
+                    })
+                    .withLayer()
+                    .start();
+            postHideStackAnimationTrigger.increment();
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java
index 318801d..5fbf421 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/RecentsViewTouchHandler.java
@@ -26,7 +26,6 @@
 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
 import com.android.systemui.recents.events.ui.dragndrop.DragStartInitializeDropTargetsEvent;
-import com.android.systemui.recents.misc.ReferenceCountedTrigger;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskStack;
@@ -58,9 +57,6 @@
  */
 public class RecentsViewTouchHandler {
 
-    private static final String TAG = "RecentsViewTouchHandler";
-    private static final boolean DEBUG = false;
-
     private RecentsView mRv;
 
     private Task mDragTask;
@@ -128,7 +124,6 @@
         mTaskView.setTranslationX(x);
         mTaskView.setTranslationY(y);
 
-        RecentsConfiguration config = Recents.getConfiguration();
         if (!ssp.hasDockedTask()) {
             // Add the dock state drop targets (these take priority)
             TaskStack.DockState[] dockStates = getDockStatesForCurrentOrientation();
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
new file mode 100644
index 0000000..787e330
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackAnimationHelper.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright (C) 2015 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.systemui.recents.views;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.RectF;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import com.android.systemui.R;
+import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.RecentsActivityLaunchState;
+import com.android.systemui.recents.RecentsConfiguration;
+import com.android.systemui.recents.misc.ReferenceCountedTrigger;
+import com.android.systemui.recents.model.Task;
+import com.android.systemui.recents.model.TaskStack;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+
+import java.util.List;
+
+/**
+ * A helper class to create task view animations for {@link TaskView}s in a {@link TaskStackView},
+ * but not the contents of the {@link TaskView}s.
+ */
+public class TaskStackAnimationHelper {
+
+    /**
+     * Callbacks from the helper to coordinate view-content animations with view animations.
+     */
+    public interface Callbacks {
+        /**
+         * Callback to prepare for the start animation for the launch target {@link TaskView}.
+         */
+        void onPrepareLaunchTargetForEnterAnimation();
+
+        /**
+         * Callback to start the animation for the launch target {@link TaskView}.
+         */
+        void onStartLaunchTargetEnterAnimation(int duration, boolean screenPinningEnabled,
+                ReferenceCountedTrigger postAnimationTrigger);
+
+        /**
+         * Callback to start the animation for the launch target {@link TaskView} when it is
+         * launched from Recents.
+         */
+        void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
+                ReferenceCountedTrigger postAnimationTrigger);
+    }
+
+    private TaskStackView mStackView;
+
+    private Interpolator mFastOutSlowInInterpolator;
+    private Interpolator mFastOutLinearInInterpolator;
+    private Interpolator mQuintOutInterpolator;
+
+    private TaskViewTransform mTmpTransform = new TaskViewTransform();
+
+    public TaskStackAnimationHelper(Context context, TaskStackView stackView) {
+        mStackView = stackView;
+        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.fast_out_slow_in);
+        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.fast_out_linear_in);
+        mQuintOutInterpolator = AnimationUtils.loadInterpolator(context,
+                com.android.internal.R.interpolator.decelerate_quint);
+    }
+
+    /**
+     * Prepares the stack views and puts them in their initial animation state while visible, before
+     * the in-app enter animations start (after the window-transition completes).
+     */
+    public void prepareForEnterAnimation() {
+        RecentsConfiguration config = Recents.getConfiguration();
+        RecentsActivityLaunchState launchState = config.getLaunchState();
+        Resources res = mStackView.getResources();
+
+        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
+        TaskStackViewScroller stackScroller = mStackView.getScroller();
+        TaskStack stack = mStackView.getStack();
+        Task launchTargetTask = stack.getLaunchTarget();
+
+        // Break early if there are no tasks
+        if (stack.getStackTaskCount() == 0) {
+            return;
+        }
+
+        int offscreenY = stackLayout.mStackRect.bottom;
+        int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
+                R.dimen.recents_task_view_affiliate_group_enter_offset);
+
+        // Prepare each of the task views for their enter animation from front to back
+        List<TaskView> taskViews = mStackView.getTaskViews();
+        for (int i = taskViews.size() - 1; i >= 0; i--) {
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+            boolean currentTaskOccludesLaunchTarget = (launchTargetTask != null &&
+                    launchTargetTask.group.isTaskAboveTask(task, launchTargetTask));
+            boolean hideTask = (launchTargetTask != null &&
+                    launchTargetTask.isFreeformTask() && task.isFreeformTask());
+
+            // Get the current transform for the task, which will be used to position it offscreen
+            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
+                    null);
+
+            if (hideTask) {
+                tv.setVisibility(View.INVISIBLE);
+            } else if (launchState.launchedHasConfigurationChanged) {
+                // Just load the views as-is
+            } else if (launchState.launchedFromAppWithThumbnail) {
+                if (task.isLaunchTarget) {
+                    tv.onPrepareLaunchTargetForEnterAnimation();
+                } else if (currentTaskOccludesLaunchTarget) {
+                    // Move the task view slightly lower so we can animate it in
+                    RectF bounds = new RectF(mTmpTransform.rect);
+                    bounds.offset(0, taskViewAffiliateGroupEnterOffset);
+                    tv.setAlpha(0f);
+                    tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top,
+                            (int) bounds.right, (int) bounds.bottom);
+                }
+            } else if (launchState.launchedFromHome) {
+                // Move the task view off screen (below) so we can animate it in
+                RectF bounds = new RectF(mTmpTransform.rect);
+                bounds.offset(0, offscreenY);
+                tv.setLeftTopRightBottom((int) bounds.left, (int) bounds.top, (int) bounds.right,
+                        (int) bounds.bottom);
+            }
+        }
+    }
+
+    /**
+     * Starts the in-app enter animation, which animates the {@link TaskView}s to their final places
+     * depending on how Recents was triggered.
+     */
+    public void startEnterAnimation(final ReferenceCountedTrigger postAnimationTrigger) {
+        RecentsConfiguration config = Recents.getConfiguration();
+        RecentsActivityLaunchState launchState = config.getLaunchState();
+        Resources res = mStackView.getResources();
+
+        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
+        TaskStackViewScroller stackScroller = mStackView.getScroller();
+        TaskStack stack = mStackView.getStack();
+        Task launchTargetTask = stack.getLaunchTarget();
+
+        // Break early if there are no tasks
+        if (stack.getStackTaskCount() == 0) {
+            return;
+        }
+
+        int taskViewEnterFromAppDuration = res.getInteger(
+                R.integer.recents_task_enter_from_app_duration);
+        int taskViewEnterFromHomeDuration = res.getInteger(
+                R.integer.recents_task_enter_from_home_duration);
+        int taskViewEnterFromHomeStaggerDelay = res.getInteger(
+                R.integer.recents_task_enter_from_home_stagger_delay);
+
+        // Create enter animations for each of the views from front to back
+        List<TaskView> taskViews = mStackView.getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = taskViewCount - 1; i >= 0; i--) {
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+            boolean currentTaskOccludesLaunchTarget = false;
+            if (launchTargetTask != null) {
+                currentTaskOccludesLaunchTarget = launchTargetTask.group.isTaskAboveTask(task,
+                        launchTargetTask);
+            }
+
+            // Get the current transform for the task, which will be updated to the final transform
+            // to animate to depending on how recents was invoked
+            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
+                    null);
+
+            if (launchState.launchedFromAppWithThumbnail) {
+                if (task.isLaunchTarget) {
+                    tv.onStartLaunchTargetEnterAnimation(taskViewEnterFromAppDuration,
+                            mStackView.mScreenPinningEnabled, postAnimationTrigger);
+                } else {
+                    // Animate the task up if it was occluding the launch target
+                    if (currentTaskOccludesLaunchTarget) {
+                        TaskViewAnimation taskAnimation = new TaskViewAnimation(
+                                taskViewEnterFromAppDuration, PhoneStatusBar.ALPHA_IN,
+                                postAnimationTrigger.decrementOnAnimationEnd());
+                        postAnimationTrigger.increment();
+                        mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
+                    }
+                }
+
+            } else if (launchState.launchedFromHome) {
+                // Animate the tasks up
+                int frontIndex = (taskViewCount - i - 1);
+                int delay = frontIndex * taskViewEnterFromHomeStaggerDelay;
+                int duration = taskViewEnterFromHomeDuration +
+                        frontIndex * taskViewEnterFromHomeStaggerDelay;
+
+                TaskViewAnimation taskAnimation = new TaskViewAnimation(delay,
+                        duration, mQuintOutInterpolator,
+                        postAnimationTrigger.decrementOnAnimationEnd());
+                postAnimationTrigger.increment();
+                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
+            }
+        }
+    }
+
+    /**
+     * Starts an in-app animation to hide all the task views so that we can transition back home.
+     */
+    public void startExitToHomeAnimation(boolean animated,
+            ReferenceCountedTrigger postAnimationTrigger) {
+        Resources res = mStackView.getResources();
+        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
+        TaskStackViewScroller stackScroller = mStackView.getScroller();
+        TaskStack stack = mStackView.getStack();
+
+        // Break early if there are no tasks
+        if (stack.getStackTaskCount() == 0) {
+            return;
+        }
+
+        int offscreenY = stackLayout.mStackRect.bottom;
+        int taskViewExitToHomeDuration = res.getInteger(
+                R.integer.recents_task_exit_to_home_duration);
+
+        // Create the animations for each of the tasks
+        List<TaskView> taskViews = mStackView.getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = 0; i < taskViewCount; i++) {
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+            TaskViewAnimation taskAnimation = new TaskViewAnimation(
+                    animated ? taskViewExitToHomeDuration : 0, mFastOutLinearInInterpolator,
+                    postAnimationTrigger.decrementOnAnimationEnd());
+            postAnimationTrigger.increment();
+
+            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
+                    null);
+            mTmpTransform.rect.offset(0, offscreenY);
+            mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
+        }
+    }
+
+    /**
+     * Starts the animation for the launching task view, hiding any tasks that might occlude the
+     * window transition for the launching task.
+     */
+    public void startLaunchTaskAnimation(TaskView launchingTaskView, boolean screenPinningRequested,
+            final ReferenceCountedTrigger postAnimationTrigger) {
+        Resources res = mStackView.getResources();
+        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
+        TaskStackViewScroller stackScroller = mStackView.getScroller();
+
+        int taskViewExitToAppDuration = res.getInteger(
+                R.integer.recents_task_exit_to_app_duration);
+        int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
+                R.dimen.recents_task_view_affiliate_group_enter_offset);
+
+        Task launchingTask = launchingTaskView.getTask();
+        List<TaskView> taskViews = mStackView.getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = 0; i < taskViewCount; i++) {
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+            boolean currentTaskOccludesLaunchTarget = (launchingTask != null &&
+                    launchingTask.group.isTaskAboveTask(task, launchingTask));
+
+            if (tv == launchingTaskView) {
+                tv.setClipViewInStack(false);
+                tv.onStartLaunchTargetLaunchAnimation(taskViewExitToAppDuration,
+                        screenPinningRequested, postAnimationTrigger);
+            } else if (currentTaskOccludesLaunchTarget) {
+                // Animate this task out of view
+                TaskViewAnimation taskAnimation = new TaskViewAnimation(
+                        taskViewExitToAppDuration, mFastOutLinearInInterpolator,
+                        postAnimationTrigger.decrementOnAnimationEnd());
+                postAnimationTrigger.increment();
+
+                stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
+                        null);
+                mTmpTransform.alpha = 0f;
+                mTmpTransform.rect.offset(0, taskViewAffiliateGroupEnterOffset);
+                mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
+            }
+        }
+    }
+
+    /**
+     * Starts the delete animation for the specified {@link TaskView}.
+     */
+    public void startDeleteTaskAnimation(Task deleteTask, final TaskView deleteTaskView,
+            final ReferenceCountedTrigger postAnimationTrigger) {
+        Resources res = mStackView.getResources();
+        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
+        TaskStackViewScroller stackScroller = mStackView.getScroller();
+
+        int taskViewRemoveAnimDuration = res.getInteger(
+                R.integer.recents_animate_task_view_remove_duration);
+        int taskViewRemoveAnimTranslationXPx = res.getDimensionPixelSize(
+                R.dimen.recents_task_view_remove_anim_translation_x);
+
+        // Disabling clipping with the stack while the view is animating away
+        deleteTaskView.setClipViewInStack(false);
+
+        // Compose the new animation and transform and star the animation
+        TaskViewAnimation taskAnimation = new TaskViewAnimation(taskViewRemoveAnimDuration,
+                PhoneStatusBar.ALPHA_OUT, new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                postAnimationTrigger.decrement();
+
+                // Re-enable clipping with the stack (we will reuse this view)
+                deleteTaskView.setClipViewInStack(true);
+            }
+        });
+        postAnimationTrigger.increment();
+
+        stackLayout.getStackTransform(deleteTask, stackScroller.getStackScroll(), mTmpTransform,
+                null);
+        mTmpTransform.alpha = 0f;
+        mTmpTransform.rect.offset(taskViewRemoveAnimTranslationXPx, 0);
+        mStackView.updateTaskViewToTransform(deleteTaskView, mTmpTransform, taskAnimation);
+    }
+
+    /**
+     * Starts the animation to hide the {@link TaskView}s when the history is shown.  The history
+     * view's animation will be deferred until all the {@link TaskView}s are finished animating.
+     */
+    public void startShowHistoryAnimation(ReferenceCountedTrigger postAnimationTrigger) {
+        Resources res = mStackView.getResources();
+        TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
+        TaskStackViewScroller stackScroller = mStackView.getScroller();
+
+        int historyTransitionDuration = res.getInteger(
+                R.integer.recents_history_transition_duration);
+
+        List<TaskView> taskViews = mStackView.getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = taskViewCount - 1; i >= 0; i--) {
+            TaskView tv = taskViews.get(i);
+            Task task = tv.getTask();
+            TaskViewAnimation taskAnimation = new TaskViewAnimation(
+                    historyTransitionDuration, PhoneStatusBar.ALPHA_OUT,
+                    postAnimationTrigger.decrementOnAnimationEnd());
+            postAnimationTrigger.increment();
+
+            stackLayout.getStackTransform(task, stackScroller.getStackScroll(), mTmpTransform,
+                    null);
+            mTmpTransform.alpha = 0f;
+            mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
+        }
+    }
+
+    /**
+     * Starts the animation to show the {@link TaskView}s when the history is hidden.  The
+     * {@link TaskView} animations will be deferred until the history view has been animated away.
+     */
+    public void startHideHistoryAnimation(final ReferenceCountedTrigger postAnimationTrigger) {
+        final Resources res = mStackView.getResources();
+        final TaskStackLayoutAlgorithm stackLayout = mStackView.getStackAlgorithm();
+        final TaskStackViewScroller stackScroller = mStackView.getScroller();
+
+        final int historyTransitionDuration = res.getInteger(
+                R.integer.recents_history_transition_duration);
+
+        List<TaskView> taskViews = mStackView.getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = taskViewCount - 1; i >= 0; i--) {
+            final TaskView tv = taskViews.get(i);
+            postAnimationTrigger.addLastDecrementRunnable(new Runnable() {
+                @Override
+                public void run() {
+                    TaskViewAnimation taskAnimation = new TaskViewAnimation(
+                            historyTransitionDuration, PhoneStatusBar.ALPHA_IN);
+                    stackLayout.getStackTransform(tv.getTask(), stackScroller.getStackScroll(),
+                            mTmpTransform, null);
+                    mTmpTransform.alpha = 1f;
+                    mStackView.updateTaskViewToTransform(tv, mTmpTransform, taskAnimation);
+                }
+            });
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index 901799e..1ee22e9 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -22,7 +22,6 @@
 import android.graphics.Path;
 import android.graphics.Rect;
 import android.util.FloatProperty;
-import android.util.Log;
 import android.util.Property;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
@@ -209,7 +208,6 @@
     }
 
     Context mContext;
-    private TaskStackView mStackView;
     private Interpolator mLinearOutSlowInInterpolator;
     private StackState mState = StackState.SPLIT;
 
@@ -277,9 +275,12 @@
     // The freeform workspace layout
     FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
 
-    public TaskStackLayoutAlgorithm(Context context, TaskStackView stackView) {
+    // The transform to place TaskViews at the front and back of the stack respectively
+    TaskViewTransform mBackOfStackTransform = new TaskViewTransform();
+    TaskViewTransform mFrontOfStackTransform = new TaskViewTransform();
+
+    public TaskStackLayoutAlgorithm(Context context) {
         Resources res = context.getResources();
-        mStackView = stackView;
 
         mFocusedRange = new Range(res.getFloat(R.integer.recents_layout_focused_range_min),
                 res.getFloat(R.integer.recents_layout_focused_range_max));
@@ -315,7 +316,7 @@
      */
     public void setFocusState(float focusState) {
         mFocusState = focusState;
-        mStackView.requestSynchronizeStackViewsWithModel();
+        updateFrontBackTransforms();
     }
 
     /**
@@ -365,6 +366,7 @@
         mUnfocusedCurveInterpolator = new FreePathInterpolator(mUnfocusedCurve);
         mFocusedCurve = constructFocusedCurve();
         mFocusedCurveInterpolator = new FreePathInterpolator(mFocusedCurve);
+        updateFrontBackTransforms();
     }
 
     /**
@@ -453,7 +455,7 @@
         Utilities.cancelAnimationWithoutCallbacks(mFocusStateAnimator);
         if (mFocusState > STATE_UNFOCUSED) {
             float delta = (float) yMovement / (UNFOCUS_MULTIPLIER * mStackRect.height());
-            mFocusState -= Math.min(mFocusState, Math.abs(delta));
+            setFocusState(mFocusState - Math.min(mFocusState, Math.abs(delta)));
         }
     }
 
@@ -479,27 +481,23 @@
         RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
         RecentsDebugFlags debugFlags = Recents.getDebugFlags();
         if (launchState.launchedWithAltTab || debugFlags.isInitialStatePaging()) {
-            return 1f;
+            return STATE_FOCUSED;
         }
-        return 0f;
+        return STATE_UNFOCUSED;
     }
 
     /**
-     * Returns the task progress that would put the task just off the back of the stack.
+     * Returns the TaskViewTransform that would put the task just off the back of the stack.
      */
-    public float getStackBackTaskProgress(float stackScroll) {
-        float min = mUnfocusedRange.relativeMin +
-                mFocusState * (mFocusedRange.relativeMin - mUnfocusedRange.relativeMin);
-        return stackScroll + min;
+    public TaskViewTransform getBackOfStackTransform() {
+        return mBackOfStackTransform;
     }
 
     /**
-     * Returns the task progress that would put the task just off the front of the stack.
+     * Returns the TaskViewTransform that would put the task just off the front of the stack.
      */
-    public float getStackFrontTaskProgress(float stackScroll) {
-        float max = mUnfocusedRange.relativeMax +
-                mFocusState * (mFocusedRange.relativeMax - mUnfocusedRange.relativeMax);
-        return stackScroll + max;
+    public TaskViewTransform getFrontOfStackTransform() {
+        return mFrontOfStackTransform;
     }
 
     /**
@@ -748,4 +746,18 @@
         p.cubicTo(0.5f, 1f - peekHeightPct, cpoint2X, cpoint2Y, 1f, 0f);
         return p;
     }
+
+    /**
+     * Updates the current transforms that would put a TaskView at the front and back of the stack.
+     */
+    private void updateFrontBackTransforms() {
+        float min = mUnfocusedRange.relativeMin +
+                mFocusState * (mFocusedRange.relativeMin - mUnfocusedRange.relativeMin);
+        float max = mUnfocusedRange.relativeMax +
+                mFocusState * (mFocusedRange.relativeMax - mUnfocusedRange.relativeMax);
+        getStackTransform(min, 0f, mBackOfStackTransform, null);
+        getStackTransform(max, 0f, mFrontOfStackTransform, null);
+        mBackOfStackTransform.visible = true;
+        mFrontOfStackTransform.visible = true;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
index ba67ad8..33ceaa1 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -23,7 +23,6 @@
 import android.content.res.Resources;
 import android.graphics.Canvas;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.GradientDrawable;
 import android.os.Bundle;
@@ -79,7 +78,6 @@
 import com.android.systemui.recents.model.TaskStack;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -108,6 +106,8 @@
     private static final float HIDE_HISTORY_BUTTON_SCROLL_THRESHOLD = 0.3f;
 
     private static final int DEFAULT_SYNC_STACK_DURATION = 200;
+    private static final int DRAG_SCALE_DURATION = 175;
+    private static final float DRAG_SCALE_FACTOR = 1.05f;
 
     public static final Property<Drawable, Integer> DRAWABLE_ALPHA =
             new IntProperty<Drawable>("drawableAlpha") {
@@ -126,35 +126,34 @@
     TaskStackLayoutAlgorithm mLayoutAlgorithm;
     TaskStackViewScroller mStackScroller;
     TaskStackViewTouchHandler mTouchHandler;
+    TaskStackAnimationHelper mAnimationHelper;
     GradientDrawable mFreeformWorkspaceBackground;
     ObjectAnimator mFreeformWorkspaceBackgroundAnimator;
     ViewPool<TaskView, Task> mViewPool;
+
+    ArrayList<TaskView> mTaskViews = new ArrayList<>();
     ArrayList<TaskViewTransform> mCurrentTaskTransforms = new ArrayList<>();
+    TaskViewAnimation mDeferredTaskViewUpdateAnimation = null;
+
     DozeTrigger mUIDozeTrigger;
     Task mFocusedTask;
-    // Optimizations
-    int mStackViewsAnimationDuration;
+
     int mTaskCornerRadiusPx;
-    boolean mStackViewsDirty = true;
-    boolean mStackViewsClipDirty = true;
+
+    boolean mTaskViewsClipDirty = true;
     boolean mAwaitingFirstLayout = true;
     boolean mEnterAnimationComplete = false;
+    boolean mTouchExplorationEnabled;
+    boolean mScreenPinningEnabled;
 
     Rect mTaskStackBounds = new Rect();
     int[] mTmpVisibleRange = new int[2];
     Rect mTmpRect = new Rect();
-    RectF mTmpTaskRect = new RectF();
-    TaskViewTransform mTmpStackBackTransform = new TaskViewTransform();
-    TaskViewTransform mTmpStackFrontTransform = new TaskViewTransform();
     HashMap<Task, TaskView> mTmpTaskViewMap = new HashMap<>();
-    ArrayList<TaskView> mTaskViews = new ArrayList<>();
-    List<TaskView> mImmutableTaskViews = new ArrayList<>();
     List<TaskView> mTmpTaskViews = new ArrayList<>();
+    TaskViewTransform mTmpTransform = new TaskViewTransform();
     LayoutInflater mInflater;
 
-    boolean mTouchExplorationEnabled;
-    boolean mScreenPinningEnabled;
-
     Interpolator mFastOutSlowInInterpolator;
 
     // A convenience update listener to request updating clipping of tasks
@@ -162,7 +161,8 @@
             new ValueAnimator.AnimatorUpdateListener() {
                 @Override
                 public void onAnimationUpdate(ValueAnimator animation) {
-                    requestUpdateStackViewsClip();
+                    mTaskViewsClipDirty = true;
+                    invalidate();
                 }
             };
 
@@ -190,10 +190,11 @@
         setStack(stack);
         mViewPool = new ViewPool<>(context, this);
         mInflater = LayoutInflater.from(context);
-        mLayoutAlgorithm = new TaskStackLayoutAlgorithm(context, this);
+        mLayoutAlgorithm = new TaskStackLayoutAlgorithm(context);
         mStackScroller = new TaskStackViewScroller(context, mLayoutAlgorithm);
         mStackScroller.setCallbacks(this);
         mTouchHandler = new TaskStackViewTouchHandler(context, this, mStackScroller);
+        mAnimationHelper = new TaskStackAnimationHelper(context, this);
         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.fast_out_slow_in);
         mTaskCornerRadiusPx = res.getDimensionPixelSize(
@@ -266,12 +267,11 @@
                 mTaskViews.add((TaskView) v);
             }
         }
-        mImmutableTaskViews = Collections.unmodifiableList(mTaskViews);
     }
 
     /** Gets the list of task views */
     List<TaskView> getTaskViews() {
-        return mImmutableTaskViews;
+        return mTaskViews;
     }
 
     /**
@@ -330,44 +330,16 @@
 
         // Reset the stack state
         mStack.reset();
-        mStackViewsDirty = true;
-        mStackViewsClipDirty = true;
+        mTaskViewsClipDirty = true;
         mAwaitingFirstLayout = true;
         mEnterAnimationComplete = false;
-        if (mUIDozeTrigger != null) {
-            mUIDozeTrigger.stopDozing();
-            mUIDozeTrigger.resetTrigger();
-        }
+        mUIDozeTrigger.stopDozing();
+        mUIDozeTrigger.resetTrigger();
         mStackScroller.reset();
         mLayoutAlgorithm.reset();
         requestLayout();
     }
 
-    /** Requests that the views be synchronized with the model */
-    void requestSynchronizeStackViewsWithModel() {
-        requestSynchronizeStackViewsWithModel(0);
-    }
-    void requestSynchronizeStackViewsWithModel(int duration) {
-        if (!mStackViewsDirty) {
-            invalidate();
-            mStackViewsDirty = true;
-        }
-        if (mAwaitingFirstLayout) {
-            // Skip the animation if we are awaiting first layout
-            mStackViewsAnimationDuration = 0;
-        } else {
-            mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration);
-        }
-    }
-
-    /** Requests that the views clipping be updated. */
-    void requestUpdateStackViewsClip() {
-        if (!mStackViewsClipDirty) {
-            invalidate();
-            mStackViewsClipDirty = true;
-        }
-    }
-
     /** Returns the stack algorithm for this task stack. */
     public TaskStackLayoutAlgorithm getStackAlgorithm() {
         return mLayoutAlgorithm;
@@ -401,15 +373,15 @@
         TaskViewTransform frontTransform = null;
         for (int i = taskCount - 1; i >= 0; i--) {
             Task task = tasks.get(i);
+            TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(task, stackScroll,
+                    taskTransforms.get(i), frontTransform);
+
+            // For freeform tasks, only calculate the stack transform and skip the calculation of
+            // the visible stack indices
             if (task.isFreeformTask()) {
                 continue;
             }
 
-            TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(task, stackScroll,
-                    taskTransforms.get(i), frontTransform);
-            if (DEBUG) {
-                Log.d(TAG, "updateStackTransform: " + i + ", " + transform.visible);
-            }
             if (transform.visible) {
                 if (frontMostVisibleIndex < 0) {
                     frontMostVisibleIndex = i;
@@ -435,144 +407,155 @@
         return frontMostVisibleIndex != -1 && backMostVisibleIndex != -1;
     }
 
-    /** Synchronizes the views with the model */
-    boolean synchronizeStackViewsWithModel() {
-        if (mStackViewsDirty) {
-            // Get all the task transforms
-            ArrayList<Task> tasks = mStack.getStackTasks();
-            float stackScroll = mStackScroller.getStackScroll();
-            int[] visibleStackRange = mTmpVisibleRange;
-            boolean isValidVisibleStackRange = updateStackTransforms(mCurrentTaskTransforms, tasks,
-                    stackScroll, visibleStackRange);
-            boolean hasStackBackTransform = false;
-            boolean hasStackFrontTransform = false;
-            if (DEBUG) {
-                Log.d(TAG, "visibleRange: " + visibleStackRange[0] + " to " + visibleStackRange[1]);
-            }
+    /**
+     * Updates the children {@link TaskView}s to match the tasks in the current {@link TaskStack}.
+     * This call does not update the {@link TaskView}s to their position in the layout except when
+     * they are initially picked up from the pool, when they will be placed in a suitable initial
+     * position.
+     */
+    private void bindTaskViewsWithStack() {
+        final float stackScroll = mStackScroller.getStackScroll();
+        final int[] visibleStackRange = mTmpVisibleRange;
 
-            // Return all the invisible children to the pool
-            mTmpTaskViewMap.clear();
-            List<TaskView> taskViews = getTaskViews();
-            int lastFocusedTaskIndex = -1;
-            int taskViewCount = taskViews.size();
-            for (int i = taskViewCount - 1; i >= 0; i--) {
-                TaskView tv = taskViews.get(i);
-                Task task = tv.getTask();
-                int taskIndex = mStack.indexOfStackTask(task);
-                if (task.isFreeformTask() ||
-                        visibleStackRange[1] <= taskIndex && taskIndex <= visibleStackRange[0]) {
-                    mTmpTaskViewMap.put(task, tv);
-                } else {
-                    if (mTouchExplorationEnabled) {
-                        lastFocusedTaskIndex = taskIndex;
-                        resetFocusedTask(task);
-                    }
-                    if (DEBUG) {
-                        Log.d(TAG, "returning to pool: " + task.key);
-                    }
-                    mViewPool.returnViewToPool(tv);
+        // Get all the task transforms
+        final ArrayList<Task> tasks = mStack.getStackTasks();
+        final boolean isValidVisibleStackRange = updateStackTransforms(mCurrentTaskTransforms, tasks,
+                stackScroll, visibleStackRange);
+
+        // Return all the invisible children to the pool
+        mTmpTaskViewMap.clear();
+        final List<TaskView> taskViews = getTaskViews();
+        final int taskViewCount = taskViews.size();
+        int lastFocusedTaskIndex = -1;
+        for (int i = taskViewCount - 1; i >= 0; i--) {
+            final TaskView tv = taskViews.get(i);
+            final Task task = tv.getTask();
+            final int taskIndex = mStack.indexOfStackTask(task);
+
+            if (task.isFreeformTask() ||
+                    visibleStackRange[1] <= taskIndex && taskIndex <= visibleStackRange[0]) {
+                mTmpTaskViewMap.put(task, tv);
+            } else {
+                if (mTouchExplorationEnabled) {
+                    lastFocusedTaskIndex = taskIndex;
+                    resetFocusedTask(task);
                 }
+                mViewPool.returnViewToPool(tv);
             }
-
-            // Pick up all the freeform tasks
-            int firstVisStackIndex = isValidVisibleStackRange ? visibleStackRange[0] : 0;
-            for (int i = mStack.getStackTaskCount() - 1; i >= firstVisStackIndex; i--) {
-                Task task = tasks.get(i);
-                if (!task.isFreeformTask()) {
-                    continue;
-                }
-                TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(task, stackScroll,
-                        mCurrentTaskTransforms.get(i), null);
-                TaskView tv = mTmpTaskViewMap.get(task);
-                if (tv == null) {
-                    if (DEBUG) {
-                        Log.d(TAG, "picking up from pool: " + task.key);
-                    }
-                    tv = mViewPool.pickUpViewFromPool(task, task);
-                } else {
-                    // Reattach it in the right z order
-                    int taskIndex = mStack.indexOfStackTask(task);
-                    int insertIndex = findTaskViewInsertIndex(task, taskIndex);
-                    if (insertIndex != getTaskViews().indexOf(tv)){
-                        detachViewFromParent(tv);
-                        attachViewToParent(tv, insertIndex, tv.getLayoutParams());
-                    }
-                }
-
-                // Animate the task into place
-                tv.updateViewPropertiesToTaskTransform(transform, 0,
-                        mStackViewsAnimationDuration, mFastOutSlowInInterpolator,
-                        mRequestUpdateClippingListener);
-
-                // Update the task views list after adding the new task view
-                updateTaskViewsList();
-            }
-
-            // Pick up all the newly visible children and update all the existing children
-            for (int i = visibleStackRange[0];
-                    isValidVisibleStackRange && i >= visibleStackRange[1]; i--) {
-                Task task = tasks.get(i);
-                TaskViewTransform transform = mCurrentTaskTransforms.get(i);
-                TaskView tv = mTmpTaskViewMap.get(task);
-
-                if (tv == null) {
-                    tv = mViewPool.pickUpViewFromPool(task, task);
-                    if (mStackViewsAnimationDuration > 0) {
-                        // For items in the list, put them in start animating them from the
-                        // approriate ends of the list where they are expected to appear
-                        if (Float.compare(transform.p, 0f) <= 0) {
-                            if (!hasStackBackTransform) {
-                                hasStackBackTransform = true;
-                                mLayoutAlgorithm.getStackTransform(
-                                        mLayoutAlgorithm.getStackBackTaskProgress(0f), 0f,
-                                        mTmpStackBackTransform, null);
-                            }
-                            tv.updateViewPropertiesToTaskTransform(mTmpStackBackTransform, 0, 0,
-                                    mFastOutSlowInInterpolator, mRequestUpdateClippingListener);
-                        } else {
-                            if (!hasStackFrontTransform) {
-                                hasStackFrontTransform = true;
-                                mLayoutAlgorithm.getStackTransform(
-                                        mLayoutAlgorithm.getStackFrontTaskProgress(0f), 0f,
-                                        mTmpStackFrontTransform, null);
-                            }
-                            tv.updateViewPropertiesToTaskTransform(mTmpStackFrontTransform, 0, 0,
-                                    mFastOutSlowInInterpolator, mRequestUpdateClippingListener);
-                        }
-                    }
-                }
-
-                // Animate the task into place, the clip for stack tasks will be calculated in
-                // clipTaskViews()
-                tv.updateViewPropertiesToTaskTransform(transform,
-                        tv.getViewBounds().getClipBottom(), mStackViewsAnimationDuration,
-                        mFastOutSlowInInterpolator, mRequestUpdateClippingListener);
-            }
-
-            // Update the focus if the previous focused task was returned to the view pool
-            if (lastFocusedTaskIndex != -1) {
-                if (lastFocusedTaskIndex < visibleStackRange[1]) {
-                    setFocusedTask(visibleStackRange[1], false /* animated */,
-                            true /* requestViewFocus */);
-                } else {
-                    setFocusedTask(visibleStackRange[0], false /* animated */,
-                            true /* requestViewFocus */);
-                }
-            }
-
-            // Reset the request-synchronize params
-            mStackViewsAnimationDuration = 0;
-            mStackViewsDirty = false;
-            mStackViewsClipDirty = true;
-            return true;
         }
-        return false;
+
+        // Pick up all the newly visible children
+        int lastVisStackIndex = isValidVisibleStackRange ? visibleStackRange[1] : 0;
+        for (int i = mStack.getStackTaskCount() - 1; i >= lastVisStackIndex; i--) {
+            final Task task = tasks.get(i);
+            final TaskViewTransform transform = mCurrentTaskTransforms.get(i);
+
+            // Skip the invisible non-freeform stack tasks
+            if (i > visibleStackRange[0] && !task.isFreeformTask()) {
+                continue;
+            }
+
+            TaskView tv = mTmpTaskViewMap.get(task);
+            if (tv == null) {
+                tv = mViewPool.pickUpViewFromPool(task, task);
+                if (task.isFreeformTask()) {
+                    tv.updateViewPropertiesToTaskTransform(transform, TaskViewAnimation.IMMEDIATE,
+                            mRequestUpdateClippingListener);
+                } else {
+                    if (Float.compare(transform.p, 0f) <= 0) {
+                        tv.updateViewPropertiesToTaskTransform(
+                                mLayoutAlgorithm.getBackOfStackTransform(),
+                                TaskViewAnimation.IMMEDIATE, mRequestUpdateClippingListener);
+                    } else {
+                        tv.updateViewPropertiesToTaskTransform(
+                                mLayoutAlgorithm.getFrontOfStackTransform(),
+                                TaskViewAnimation.IMMEDIATE, mRequestUpdateClippingListener);
+                    }
+                }
+            } else {
+                // Reattach it in the right z order
+                final int taskIndex = mStack.indexOfStackTask(task);
+                final int insertIndex = findTaskViewInsertIndex(task, taskIndex);
+                if (insertIndex != getTaskViews().indexOf(tv)){
+                    detachViewFromParent(tv);
+                    attachViewToParent(tv, insertIndex, tv.getLayoutParams());
+                    updateTaskViewsList();
+                }
+            }
+        }
+
+        // Update the focus if the previous focused task was returned to the view pool
+        if (lastFocusedTaskIndex != -1) {
+            if (lastFocusedTaskIndex < visibleStackRange[1]) {
+                setFocusedTask(visibleStackRange[1], false /* scrollToTask */,
+                        true /* requestViewFocus */);
+            } else {
+                setFocusedTask(visibleStackRange[0], false /* scrollToTask */,
+                        true /* requestViewFocus */);
+            }
+        }
+    }
+
+    /**
+     * Cancels any existing {@link TaskView} animations, and updates each {@link TaskView} to its
+     * current position as defined by the {@link TaskStackLayoutAlgorithm}.
+     */
+    private void updateTaskViewsToLayout(TaskViewAnimation animation) {
+        // If we had a deferred animation, cancel that
+        mDeferredTaskViewUpdateAnimation = null;
+
+        // Cancel all task view animations
+        cancelAllTaskViewAnimations();
+
+        // Fetch the current set of TaskViews
+        bindTaskViewsWithStack();
+
+        // Animate them to their final transforms with the given animation
+        List<TaskView> taskViews = getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = 0; i < taskViewCount; i++) {
+            final TaskView tv = taskViews.get(i);
+            final int taskIndex = mStack.indexOfStackTask(tv.getTask());
+            final TaskViewTransform transform = mCurrentTaskTransforms.get(taskIndex);
+
+            updateTaskViewToTransform(tv, transform, animation);
+        }
+    }
+
+    /**
+     * Posts an update to synchronize the {@link TaskView}s with the stack on the next frame.
+     */
+    private void updateTaskViewsToLayoutOnNextFrame(TaskViewAnimation animation) {
+        mDeferredTaskViewUpdateAnimation = animation;
+        postInvalidateOnAnimation();
+    }
+
+    /**
+     * Called to update a specific {@link TaskView} to a given {@link TaskViewTransform} with a
+     * given set of {@link TaskViewAnimation} properties.
+     */
+    public void updateTaskViewToTransform(TaskView taskView, TaskViewTransform transform,
+            TaskViewAnimation animation) {
+        taskView.updateViewPropertiesToTaskTransform(transform, animation,
+                mRequestUpdateClippingListener);
+    }
+
+    /**
+     * Cancels all {@link TaskView} animations.
+     */
+    private void cancelAllTaskViewAnimations() {
+        List<TaskView> taskViews = getTaskViews();
+        int taskViewCount = taskViews.size();
+        for (int i = 0; i < taskViewCount; i++) {
+            final TaskView tv = taskViews.get(i);
+            tv.cancelTransformAnimation();
+        }
     }
 
     /**
      * Updates the clip for each of the task views from back to front.
      */
-    void clipTaskViews(boolean forceUpdate) {
+    private void clipTaskViews() {
         RecentsConfiguration config = Recents.getConfiguration();
 
         // Update the clip on each task child
@@ -606,12 +589,12 @@
                     }
                 }
             }
-            tv.getViewBounds().setClipBottom(clipBottom, forceUpdate);
+            tv.getViewBounds().setClipBottom(clipBottom);
             if (!config.useHardwareLayers) {
                 tv.mThumbnailView.updateThumbnailVisibility(clipBottom - tv.getPaddingBottom());
             }
         }
-        mStackViewsClipDirty = false;
+        mTaskViewsClipDirty = false;
     }
 
     /** Updates the min and max virtual scroll bounds */
@@ -642,16 +625,7 @@
      *
      * @return whether or not the stack will scroll as a part of this focus change
      */
-    private boolean setFocusedTask(int taskIndex, boolean scrollToTask, final boolean animated) {
-        return setFocusedTask(taskIndex, scrollToTask, animated, true);
-    }
-
-    /**
-     * Sets the focused task to the provided (bounded taskIndex).
-     *
-     * @return whether or not the stack will scroll as a part of this focus change
-     */
-    private boolean setFocusedTask(int taskIndex, boolean scrollToTask, final boolean animated,
+    private boolean setFocusedTask(int taskIndex, boolean scrollToTask,
             final boolean requestViewFocus) {
         // Find the next task to focus
         int newFocusedTaskIndex = mStack.getStackTaskCount() > 0 ?
@@ -672,7 +646,7 @@
                 public void run() {
                     TaskView tv = getChildViewForTask(newFocusedTask);
                     if (tv != null) {
-                        tv.setFocusedState(true, animated, requestViewFocus);
+                        tv.setFocusedState(true, requestViewFocus);
                     }
                 }
             };
@@ -687,10 +661,7 @@
 
                     // Cancel any running enter animations at this point when we scroll as well
                     if (!mEnterAnimationComplete) {
-                        final List<TaskView> taskViews = getTaskViews();
-                        for (TaskView tv : taskViews) {
-                            tv.cancelEnterRecentsAnimation();
-                        }
+                        cancelAllTaskViewAnimations();
                     }
                 } else {
                     focusTaskRunnable.run();
@@ -765,7 +736,8 @@
             }
         }
         if (newIndex != -1) {
-            boolean willScroll = setFocusedTask(newIndex, true, animated);
+            boolean willScroll = setFocusedTask(newIndex, true /* scrollToTask */,
+                    true /* requestViewFocus */);
             if (willScroll && cancelWindowAnimations) {
                 // As we iterate to the next/previous task, cancel any current/lagging window
                 // transition animations
@@ -781,7 +753,7 @@
         if (task != null) {
             TaskView tv = getChildViewForTask(task);
             if (tv != null) {
-                tv.setFocusedState(false, false /* animated */, false /* requestViewFocus */);
+                tv.setFocusedState(false, false /* requestViewFocus */);
             }
         }
         mFocusedTask = null;
@@ -886,12 +858,18 @@
 
     @Override
     public void computeScroll() {
-        mStackScroller.computeScroll();
-        // Synchronize the views
-        synchronizeStackViewsWithModel();
-        clipTaskViews(false /* forceUpdate */);
-        // Notify accessibility
-        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+        if (mStackScroller.computeScroll()) {
+            // Notify accessibility
+            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+        }
+        if (mDeferredTaskViewUpdateAnimation != null) {
+            updateTaskViewsToLayout(mDeferredTaskViewUpdateAnimation);
+            mTaskViewsClipDirty = true;
+            mDeferredTaskViewUpdateAnimation = null;
+        }
+        if (mTaskViewsClipDirty) {
+            clipTaskViews();
+        }
     }
 
     /** Computes the stack and task rects */
@@ -938,13 +916,12 @@
         // Compute our stack/task rects
         computeRects(mTaskStackBounds);
 
-        // If this is the first layout, then scroll to the front of the stack and synchronize the
-        // stack views immediately to load all the views
+        // If this is the first layout, then scroll to the front of the stack, then update the
+        // TaskViews with the stack so that we can lay them out
         if (mAwaitingFirstLayout) {
             mStackScroller.setStackScrollToInitialState();
-            requestSynchronizeStackViewsWithModel();
-            synchronizeStackViewsWithModel();
         }
+        bindTaskViewsWithStack();
 
         // Measure each of the TaskViews
         mTmpTaskViews.clear();
@@ -994,44 +971,25 @@
                     taskRect.right + mTmpRect.right, taskRect.bottom + mTmpRect.bottom);
         }
 
-        if (mAwaitingFirstLayout) {
-            mAwaitingFirstLayout = false;
-            onFirstLayout();
-        }
-
-        requestSynchronizeStackViewsWithModel();
         if (changed) {
             if (mStackScroller.isScrollOutOfBounds()) {
                 mStackScroller.boundScroll();
             }
-            synchronizeStackViewsWithModel();
-            requestUpdateStackViewsClip();
-            clipTaskViews(true /* forceUpdate */);
+        }
+        updateTaskViewsToLayout(TaskViewAnimation.IMMEDIATE);
+        clipTaskViews();
+
+        if (mAwaitingFirstLayout || !mEnterAnimationComplete) {
+            mAwaitingFirstLayout = false;
+            onFirstLayout();
+            return;
         }
     }
 
     /** Handler for the first layout. */
     void onFirstLayout() {
-        int offscreenY = mLayoutAlgorithm.mStackRect.bottom;
-
-        // Find the launch target task
-        Task launchTargetTask = mStack.getLaunchTarget();
-        List<TaskView> taskViews = getTaskViews();
-        int taskViewCount = taskViews.size();
-
-        // Prepare the first view for its enter animation
-        for (int i = taskViewCount - 1; i >= 0; i--) {
-            TaskView tv = taskViews.get(i);
-            Task task = tv.getTask();
-            boolean hideTask = false;
-            boolean occludesLaunchTarget = false;
-            if (launchTargetTask != null) {
-                occludesLaunchTarget = launchTargetTask.group.isTaskAboveTask(task,
-                        launchTargetTask);
-                hideTask = launchTargetTask.isFreeformTask() && task.isFreeformTask();
-            }
-            tv.prepareEnterRecentsAnimation(hideTask, occludesLaunchTarget, offscreenY);
-        }
+        // Setup the view for the enter animation
+        mAnimationHelper.prepareForEnterAnimation();
 
         // Animate in the freeform workspace
         animateFreeformWorkspaceBackgroundAlpha(
@@ -1044,7 +1002,7 @@
         RecentsActivityLaunchState launchState = config.getLaunchState();
         int focusedTaskIndex = launchState.getInitialFocusTaskIndex(mStack.getStackTaskCount());
         if (focusedTaskIndex != -1) {
-            setFocusedTask(focusedTaskIndex, false /* scrollToTask */, false /* animated */,
+            setFocusedTask(focusedTaskIndex, false /* scrollToTask */,
                     false /* requestViewFocus */);
         }
 
@@ -1057,92 +1015,6 @@
         }
     }
 
-    /** Requests this task stacks to start it's enter-recents animation */
-    public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) {
-        if (mStack.getStackTaskCount() > 0) {
-            // Find the launch target task
-            Task launchTargetTask = mStack.getLaunchTarget();
-            List<TaskView> taskViews = getTaskViews();
-            int taskViewCount = taskViews.size();
-
-            // Animate all the task views into view
-            for (int i = taskViewCount - 1; i >= 0; i--) {
-                TaskView tv = taskViews.get(i);
-                Task task = tv.getTask();
-                ctx.currentTaskTransform = new TaskViewTransform();
-                ctx.currentStackViewIndex = i;
-                ctx.currentStackViewCount = taskViewCount;
-                ctx.currentTaskRect = mLayoutAlgorithm.mTaskRect;
-                ctx.currentTaskOccludesLaunchTarget = (launchTargetTask != null) &&
-                        launchTargetTask.group.isTaskAboveTask(task, launchTargetTask);
-                ctx.isScreenPinningEnabled = mScreenPinningEnabled;
-                ctx.updateListener = mRequestUpdateClippingListener;
-                mLayoutAlgorithm.getStackTransform(task, mStackScroller.getStackScroll(),
-                        ctx.currentTaskTransform, null);
-                tv.startEnterRecentsAnimation(ctx);
-            }
-
-            // Add a runnable to the post animation ref counter to clear all the views
-            ctx.postAnimationTrigger.addLastDecrementRunnable(new Runnable() {
-                @Override
-                public void run() {
-                    // Poke the dozer to restart the trigger after the animation completes
-                    mUIDozeTrigger.poke();
-
-                    // Update the focused state here -- since we only set the focused task without
-                    // requesting view focus in onFirstLayout(), actually request view focus and
-                    // animate the focused state if we are alt-tabbing now, after the window enter
-                    // animation is completed
-                    if (mFocusedTask != null) {
-                        RecentsConfiguration config = Recents.getConfiguration();
-                        RecentsActivityLaunchState launchState = config.getLaunchState();
-                        setFocusedTask(mStack.indexOfStackTask(mFocusedTask),
-                                false /* scrollToTask */, launchState.launchedWithAltTab);
-                    }
-                }
-            });
-        }
-    }
-
-    /** Requests this task stack to start it's exit-recents animation. */
-    public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
-        // Stop any scrolling
-        mStackScroller.stopScroller();
-        mStackScroller.stopBoundScrollAnimation();
-        // Animate all the task views out of view
-        ctx.offscreenTranslationY = mLayoutAlgorithm.mStackRect.bottom;
-        // Dismiss the freeform workspace background
-        int taskViewExitToHomeDuration = getResources().getInteger(
-                R.integer.recents_task_exit_to_home_duration);
-        animateFreeformWorkspaceBackgroundAlpha(0, taskViewExitToHomeDuration,
-                mFastOutSlowInInterpolator);
-
-        List<TaskView> taskViews = getTaskViews();
-        int taskViewCount = taskViews.size();
-        for (int i = 0; i < taskViewCount; i++) {
-            TaskView tv = taskViews.get(i);
-            tv.startExitToHomeAnimation(ctx);
-        }
-    }
-
-    /** Animates a task view in this stack as it launches. */
-    public void startLaunchTaskAnimation(TaskView tv, Runnable r, boolean lockToTask) {
-        Task launchTargetTask = tv.getTask();
-        List<TaskView> taskViews = getTaskViews();
-        int taskViewCount = taskViews.size();
-        for (int i = 0; i < taskViewCount; i++) {
-            TaskView t = taskViews.get(i);
-            if (t == tv) {
-                t.setClipViewInStack(false);
-                t.startLaunchTaskAnimation(r, true, true, lockToTask);
-            } else {
-                boolean occludesLaunchTarget = launchTargetTask.group.isTaskAboveTask(t.getTask(),
-                        launchTargetTask);
-                t.startLaunchTaskAnimation(null, false, occludesLaunchTarget, lockToTask);
-            }
-        }
-    }
-
     public boolean isTransformedTouchPointInView(float x, float y, TaskView tv) {
         final float[] point = new float[2];
         point[0] = x;
@@ -1178,11 +1050,14 @@
      * Launches the freeform tasks.
      */
     public boolean launchFreeformTasks() {
-        Task frontTask = mStack.getStackFrontMostTask();
-        if (frontTask != null && frontTask.isFreeformTask()) {
-            EventBus.getDefault().send(new LaunchTaskEvent(getChildViewForTask(frontTask),
-                    frontTask, null, INVALID_STACK_ID, false));
-            return true;
+        ArrayList<Task> tasks = mStack.getFreeformTasks();
+        if (!tasks.isEmpty()) {
+            Task frontTask = tasks.get(tasks.size() - 1);
+            if (frontTask != null && frontTask.isFreeformTask()) {
+                EventBus.getDefault().send(new LaunchTaskEvent(getChildViewForTask(frontTask),
+                        frontTask, null, INVALID_STACK_ID, false));
+                return true;
+            }
         }
         return false;
     }
@@ -1195,7 +1070,8 @@
         updateLayout(true);
 
         // Animate all the tasks into place
-        requestSynchronizeStackViewsWithModel(DEFAULT_SYNC_STACK_DURATION);
+        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+                mFastOutSlowInInterpolator));
     }
 
     @Override
@@ -1242,9 +1118,6 @@
                 mStackScroller.setStackScroll(mStackScroller.getStackScroll() + stackScrollOffset);
                 mStackScroller.boundScroll();
             }
-
-            // Animate all the tasks into place
-            requestSynchronizeStackViewsWithModel(DEFAULT_SYNC_STACK_DURATION);
         } else {
             // Remove the view associated with this task, we can't rely on updateTransforms
             // to work here because the task is no longer in the list
@@ -1255,11 +1128,12 @@
 
             // Update the min/max scroll and animate other task views into their new positions
             updateLayout(true);
-
-            // Animate all the tasks into place
-            requestSynchronizeStackViewsWithModel(DEFAULT_SYNC_STACK_DURATION);
         }
 
+        // Animate all the tasks into place
+        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+                mFastOutSlowInInterpolator));
+
         // Update the new front most task's action button
         if (mScreenPinningEnabled && newFrontMostTask != null) {
             TaskView frontTv = getChildViewForTask(newFrontMostTask);
@@ -1303,19 +1177,15 @@
 
         // Reset the view properties and view state
         tv.resetViewProperties();
-        tv.setFocusedState(false, false /* animated */, false /* requestViewFocus */);
+        tv.setFocusedState(false, false /* requestViewFocus */);
         tv.setClipViewInStack(false);
         if (mScreenPinningEnabled) {
-            tv.hideActionButton();
+            tv.hideActionButton(false /* fadeOut */, 0 /* duration */, false /* scaleDown */, null);
         }
     }
 
     @Override
     public void prepareViewToLeavePool(TaskView tv, Task task, boolean isNewView) {
-        // It is possible for a view to be returned to the view pool before it is laid out,
-        // which means that we will need to relayout the view when it is first used next.
-        boolean requiresRelayout = tv.getWidth() <= 0 && !isNewView;
-
         // Rebind the task and request that this task's data be filled into the TaskView
         tv.onTaskBound(task);
 
@@ -1334,9 +1204,6 @@
             addView(tv, insertIndex);
         } else {
             attachViewToParent(tv, insertIndex, tv.getLayoutParams());
-            if (requiresRelayout) {
-                tv.requestLayout();
-            }
         }
         // Update the task views list after adding the new task view
         updateTaskViewsList();
@@ -1346,7 +1213,7 @@
         tv.setTouchEnabled(true);
         tv.setClipViewInStack(true);
         if (mFocusedTask == task) {
-            tv.setFocusedState(true, false /* animated */, false /* requestViewFocus */);
+            tv.setFocusedState(true, false /* requestViewFocus */);
         }
 
         // Restore the action button visibility if it is the front most task view
@@ -1364,16 +1231,15 @@
 
     @Override
     public void onTaskViewClipStateChanged(TaskView tv) {
-        requestUpdateStackViewsClip();
+        clipTaskViews();
     }
 
     /**** TaskStackViewScroller.TaskStackViewScrollerCallbacks ****/
 
     @Override
-    public void onScrollChanged(float prevScroll, float curScroll) {
+    public void onScrollChanged(float prevScroll, float curScroll, TaskViewAnimation animation) {
         mUIDozeTrigger.poke();
-        requestSynchronizeStackViewsWithModel();
-        postInvalidateOnAnimation();
+        updateTaskViewsToLayoutOnNextFrame(animation);
 
         if (shouldShowHistoryButton() &&
                 prevScroll > SHOW_HISTORY_BUTTON_SCROLL_THRESHOLD &&
@@ -1400,12 +1266,7 @@
                 final TaskView tv = getChildViewForTask(t);
                 if (tv != null) {
                     // For visible children, defer removing the task until after the animation
-                    tv.startDeleteTaskAnimation(new Runnable() {
-                        @Override
-                        public void run() {
-                            removeTaskViewFromStack(tv);
-                        }
-                    }, 0);
+                    tv.dismissTask();
                 } else {
                     // Otherwise, remove the task from the stack immediately
                     mStack.removeTask(t);
@@ -1420,9 +1281,8 @@
     }
 
     public final void onBusEvent(LaunchTaskStartedEvent event) {
-        event.getAnimationTrigger().increment();
-        startLaunchTaskAnimation(event.taskView, event.getAnimationTrigger().decrementAsRunnable(),
-                event.screenPinningRequested);
+        mAnimationHelper.startLaunchTaskAnimation(event.taskView, event.screenPinningRequested,
+                event.getAnimationTrigger());
     }
 
     public final void onBusEvent(DismissRecentsToHomeAnimationStarted event) {
@@ -1431,9 +1291,7 @@
         mStackScroller.stopBoundScrollAnimation();
 
         // Start the task animations
-        ViewAnimation.TaskViewExitContext context = new ViewAnimation.TaskViewExitContext(
-                event.getAnimationTrigger());
-        startExitToHomeAnimation(context);
+        mAnimationHelper.startExitToHomeAnimation(event.animated, event.getAnimationTrigger());
 
         // Dismiss the freeform workspace background
         int taskViewExitToHomeDuration = getResources().getInteger(
@@ -1454,9 +1312,8 @@
 
     public final void onBusEvent(final DismissTaskViewEvent event) {
         // For visible children, defer removing the task until after the animation
-        event.getAnimationTrigger().increment();
-        event.taskView.startDeleteTaskAnimation(
-                event.getAnimationTrigger().decrementAsRunnable(), 0);
+        mAnimationHelper.startDeleteTaskAnimation(event.task, event.taskView,
+                event.getAnimationTrigger());
     }
 
     public final void onBusEvent(TaskViewDismissedEvent event) {
@@ -1489,6 +1346,14 @@
             mStackScroller.animateScroll(mStackScroller.getStackScroll(),
                     mLayoutAlgorithm.mInitialScrollP, null);
         }
+
+        // Enlarge the dragged view slightly
+        float finalScale = event.taskView.getScaleX() * DRAG_SCALE_FACTOR;
+        mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(),
+                mTmpTransform, null);
+        mTmpTransform.scale = finalScale;
+        updateTaskViewToTransform(event.taskView, mTmpTransform,
+                new TaskViewAnimation(DRAG_SCALE_DURATION, mFastOutSlowInInterpolator));
     }
 
     public final void onBusEvent(DragStartInitializeDropTargetsEvent event) {
@@ -1534,8 +1399,6 @@
                 }
             });
         }
-        event.getAnimationTrigger().increment();
-        event.taskView.animate().withEndAction(event.getAnimationTrigger().decrementAsRunnable());
 
         // We translated the view but we need to animate it back from the current layout-space rect
         // to its final layout-space rect
@@ -1549,8 +1412,15 @@
         event.taskView.setLeftTopRightBottom(taskViewRect.left, taskViewRect.top,
                 taskViewRect.right, taskViewRect.bottom);
 
-        // Animate the tack view back into position
-        requestSynchronizeStackViewsWithModel(250);
+        // Animate all the TaskViews back into position
+        mLayoutAlgorithm.getStackTransform(event.task, getScroller().getStackScroll(),
+                mTmpTransform, null);
+        event.getAnimationTrigger().increment();
+        updateTaskViewsToLayout(new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION,
+                mFastOutSlowInInterpolator));
+        updateTaskViewToTransform(event.taskView, mTmpTransform,
+                new TaskViewAnimation(DEFAULT_SYNC_STACK_DURATION, mFastOutSlowInInterpolator,
+                        event.getAnimationTrigger().decrementOnAnimationEnd()));
     }
 
     public final void onBusEvent(StackViewScrolledEvent event) {
@@ -1562,7 +1432,6 @@
             // Cancel the previous task's window transition before animating the focused state
             EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(null));
         }
-        mLayoutAlgorithm.animateFocusState(mLayoutAlgorithm.getDefaultFocusState());
     }
 
     public final void onBusEvent(EnterRecentsWindowAnimationCompletedEvent event) {
@@ -1570,9 +1439,7 @@
 
         if (mStack.getStackTaskCount() > 0) {
             // Start the task enter animations
-            ViewAnimation.TaskViewEnterContext context = new ViewAnimation.TaskViewEnterContext(
-                    event.getAnimationTrigger());
-            startEnterRecentsAnimation(context);
+            mAnimationHelper.startEnterAnimation(event.getAnimationTrigger());
 
             // Add a runnable to the post animation ref counter to clear all the views
             event.addPostAnimationCallback(new Runnable() {
@@ -1609,48 +1476,11 @@
     }
 
     public final void onBusEvent(ShowHistoryEvent event) {
-        // The history view's animation will be deferred until all the stack task views are animated
-        // away
-        int historyTransitionDuration =
-                getResources().getInteger(R.integer.recents_history_transition_duration);
-        List<TaskView> taskViews = getTaskViews();
-        int taskViewCount = taskViews.size();
-        for (int i = taskViewCount - 1; i >= 0; i--) {
-            TaskView tv = taskViews.get(i);
-            tv.animate()
-                    .alpha(0f)
-                    .setDuration(historyTransitionDuration)
-                    .setUpdateListener(null)
-                    .setListener(null)
-                    .withLayer()
-                    .withEndAction(event.getAnimationTrigger().decrementAsRunnable())
-                    .start();
-            event.getAnimationTrigger().increment();
-        }
+        mAnimationHelper.startShowHistoryAnimation(event.getAnimationTrigger());
     }
 
     public final void onBusEvent(HideHistoryEvent event) {
-        // The stack task view animations will be deferred until the history view has been animated
-        // away
-        final int historyTransitionDuration =
-                getResources().getInteger(R.integer.recents_history_transition_duration);
-        List<TaskView> taskViews = getTaskViews();
-        int taskViewCount = taskViews.size();
-        for (int i = taskViewCount - 1; i >= 0; i--) {
-            final TaskView tv = taskViews.get(i);
-            event.addPostAnimationCallback(new Runnable() {
-                @Override
-                public void run() {
-                    tv.animate()
-                            .alpha(1f)
-                            .setDuration(historyTransitionDuration)
-                            .setUpdateListener(null)
-                            .setListener(null)
-                            .withLayer()
-                            .start();
-                }
-            });
-        }
+        mAnimationHelper.startHideHistoryAnimation(event.getAnimationTrigger());
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
index 56942a8..32f02ac 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
@@ -34,7 +34,7 @@
     private static final boolean DEBUG = false;
 
     public interface TaskStackViewScrollerCallbacks {
-        void onScrollChanged(float prevScroll, float curScroll);
+        void onScrollChanged(float prevScroll, float curScroll, TaskViewAnimation animation);
     }
 
     Context mContext;
@@ -57,7 +57,6 @@
         mLayoutAlgorithm = layoutAlgorithm;
         mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.linear_out_slow_in);
-        setStackScroll(getStackScroll());
     }
 
     /** Resets the task scroller. */
@@ -75,12 +74,22 @@
         return mStackScrollP;
     }
 
-    /** Sets the current stack scroll */
+    /**
+     * Sets the current stack scroll immediately.
+     */
     public void setStackScroll(float s) {
+        setStackScroll(s, TaskViewAnimation.IMMEDIATE);
+    }
+
+    /**
+     * Sets the current stack scroll, but indicates to the callback the preferred animation to
+     * update to this new scroll.
+     */
+    public void setStackScroll(float s, TaskViewAnimation animation) {
         float prevStackScroll = mStackScrollP;
         mStackScrollP = s;
         if (mCb != null) {
-            mCb.onScrollChanged(prevStackScroll, mStackScrollP);
+            mCb.onScrollChanged(prevStackScroll, mStackScrollP, animation);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
index d861d34..5852d07 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -295,6 +295,7 @@
             Rect freeformRect = mSv.mLayoutAlgorithm.mFreeformRect;
             if (freeformRect.top <= y && y <= freeformRect.bottom) {
                 if (mSv.launchFreeformTasks()) {
+                    // TODO: Animate Recents away as we launch the freeform tasks
                     return;
                 }
             }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
index 3f7b99d..b9fefde 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskView.java
@@ -31,7 +31,9 @@
 import android.graphics.PorterDuffColorFilter;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.util.Log;
+import android.util.FloatProperty;
+import android.util.IntProperty;
+import android.util.Property;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewOutlineProvider;
@@ -39,10 +41,10 @@
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
+
 import com.android.systemui.R;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsActivity;
-import com.android.systemui.recents.RecentsActivityLaunchState;
 import com.android.systemui.recents.RecentsConfiguration;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.LaunchTaskEvent;
@@ -50,28 +52,57 @@
 import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
 import com.android.systemui.recents.events.ui.dragndrop.DragEndEvent;
 import com.android.systemui.recents.events.ui.dragndrop.DragStartEvent;
+import com.android.systemui.recents.misc.ReferenceCountedTrigger;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.recents.model.Task;
 import com.android.systemui.recents.model.TaskStack;
 import com.android.systemui.statusbar.phone.PhoneStatusBar;
 
+import java.util.ArrayList;
+
 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
 
 /* A task view */
 public class TaskView extends FrameLayout implements Task.TaskCallbacks,
-        View.OnClickListener, View.OnLongClickListener {
-
-    private final static String TAG = "TaskView";
-    private final static boolean DEBUG = false;
+        TaskStackAnimationHelper.Callbacks, View.OnClickListener, View.OnLongClickListener {
 
     /** The TaskView callbacks */
     interface TaskViewCallbacks {
         void onTaskViewClipStateChanged(TaskView tv);
     }
 
+    /**
+     * The dim overlay is generally calculated from the task progress, but occasionally (like when
+     * launching) needs to be animated independently of the task progress.
+     */
+    public static final Property<TaskView, Integer> DIM =
+            new IntProperty<TaskView>("dim") {
+                @Override
+                public void setValue(TaskView tv, int dim) {
+                    tv.setDim(dim);
+                }
+
+                @Override
+                public Integer get(TaskView tv) {
+                    return tv.getDim();
+                }
+            };
+
+    public static final Property<TaskView, Float> TASK_PROGRESS =
+            new FloatProperty<TaskView>("taskProgress") {
+                @Override
+                public void setValue(TaskView tv, float p) {
+                    tv.setTaskProgress(p);
+                }
+
+                @Override
+                public Float get(TaskView tv) {
+                    return tv.getTaskProgress();
+                }
+            };
+
     float mTaskProgress;
-    ObjectAnimator mTaskProgressAnimator;
     float mMaxDimScale;
     int mDimAlpha;
     AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(3f);
@@ -81,9 +112,11 @@
 
     Task mTask;
     boolean mTaskDataLoaded;
-    boolean mClipViewInStack;
+    boolean mClipViewInStack = true;
     AnimateableViewBounds mViewBounds;
-    private AnimatorSet mClipAnimation;
+
+    private AnimatorSet mTransformAnimation;
+    private ArrayList<Animator> mTmpAnimators = new ArrayList<>();
 
     View mContent;
     TaskViewThumbnail mThumbnailView;
@@ -94,18 +127,6 @@
     Point mDownTouchPos = new Point();
 
     Interpolator mFastOutSlowInInterpolator;
-    Interpolator mFastOutLinearInInterpolator;
-    Interpolator mQuintOutInterpolator;
-
-    // Optimizations
-    ValueAnimator.AnimatorUpdateListener mUpdateDimListener =
-            new ValueAnimator.AnimatorUpdateListener() {
-                @Override
-                public void onAnimationUpdate(ValueAnimator animation) {
-                    setTaskProgress((Float) animation.getAnimatedValue());
-                }
-            };
-
 
     public TaskView(Context context) {
         this(context, null);
@@ -124,17 +145,10 @@
         RecentsConfiguration config = Recents.getConfiguration();
         Resources res = context.getResources();
         mMaxDimScale = res.getInteger(R.integer.recents_max_task_stack_view_dim) / 255f;
-        mClipViewInStack = true;
         mViewBounds = new AnimateableViewBounds(this, res.getDimensionPixelSize(
                 R.dimen.recents_task_view_rounded_corners_radius));
         mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                 com.android.internal.R.interpolator.fast_out_slow_in);
-        mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context,
-                com.android.internal.R.interpolator.fast_out_linear_in);
-        mQuintOutInterpolator = AnimationUtils.loadInterpolator(context,
-                com.android.internal.R.interpolator.decelerate_quint);
-        setTaskProgress(getTaskProgress());
-        setDim(getDim());
         if (config.fakeShadows) {
             setBackground(new FakeShadowDrawable(res, config));
         }
@@ -228,294 +242,69 @@
         invalidateOutline();
     }
 
-    /** Synchronizes this view's properties with the task's transform */
-    void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int clipBottom,
-            int duration, Interpolator interpolator,
-            ValueAnimator.AnimatorUpdateListener updateCallback) {
+    void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform,
+            TaskViewAnimation toAnimation, ValueAnimator.AnimatorUpdateListener updateCallback) {
         RecentsConfiguration config = Recents.getConfiguration();
-        Utilities.cancelAnimationWithoutCallbacks(mClipAnimation);
+        Utilities.cancelAnimation(mTransformAnimation);
 
-        // Apply the transform
-        toTransform.applyToTaskView(this, duration, interpolator, false,
-                !config.fakeShadows, updateCallback);
-
-        // Update the clipping
-        if (duration > 0) {
-            mClipAnimation = new AnimatorSet();
-            mClipAnimation.playTogether(
-                    ObjectAnimator.ofInt(mViewBounds, AnimateableViewBounds.CLIP_BOTTOM,
-                            mViewBounds.getClipBottom(), clipBottom),
-                    ObjectAnimator.ofInt(this, TaskViewTransform.LEFT, getLeft(),
-                            (int) toTransform.rect.left),
-                    ObjectAnimator.ofInt(this, TaskViewTransform.TOP, getTop(),
-                            (int) toTransform.rect.top),
-                    ObjectAnimator.ofInt(this, TaskViewTransform.RIGHT, getRight(),
-                            (int) toTransform.rect.right),
-                    ObjectAnimator.ofInt(this, TaskViewTransform.BOTTOM, getBottom(),
-                            (int) toTransform.rect.bottom),
-                    ObjectAnimator.ofFloat(mThumbnailView, TaskViewThumbnail.BITMAP_SCALE,
-                            mThumbnailView.getBitmapScale(), toTransform.thumbnailScale));
-            mClipAnimation.setStartDelay(toTransform.startDelay);
-            mClipAnimation.setDuration(duration);
-            mClipAnimation.setInterpolator(interpolator);
-            mClipAnimation.start();
-        } else {
-            mViewBounds.setClipBottom(clipBottom, false /* forceUpdate */);
+        // Compose the animations for the transform
+        mTmpAnimators.clear();
+        boolean requiresHwLayers = toTransform.applyToTaskView(this, mTmpAnimators, toAnimation,
+                !config.fakeShadows);
+        if (toAnimation.isImmediate()) {
             mThumbnailView.setBitmapScale(toTransform.thumbnailScale);
-            setLeftTopRightBottom((int) toTransform.rect.left, (int) toTransform.rect.top,
-                    (int) toTransform.rect.right, (int) toTransform.rect.bottom);
-        }
-        if (!config.useHardwareLayers) {
-            mThumbnailView.updateThumbnailVisibility(clipBottom - getPaddingBottom());
-        }
-
-        // Update the task progress
-        Utilities.cancelAnimationWithoutCallbacks(mTaskProgressAnimator);
-        if (duration <= 0) {
             setTaskProgress(toTransform.p);
+            if (toAnimation.listener != null) {
+                toAnimation.listener.onAnimationEnd(null);
+            }
         } else {
-            mTaskProgressAnimator = ObjectAnimator.ofFloat(this, "taskProgress", toTransform.p);
-            mTaskProgressAnimator.setDuration(duration);
-            mTaskProgressAnimator.addUpdateListener(mUpdateDimListener);
-            mTaskProgressAnimator.start();
+            if (Float.compare(mThumbnailView.getBitmapScale(), toTransform.thumbnailScale) != 0) {
+                mTmpAnimators.add(ObjectAnimator.ofFloat(mThumbnailView,
+                        TaskViewThumbnail.BITMAP_SCALE, mThumbnailView.getBitmapScale(),
+                        toTransform.thumbnailScale));
+            }
+            if (Float.compare(getTaskProgress(), toTransform.p) != 0) {
+                mTmpAnimators.add(ObjectAnimator.ofFloat(this, TASK_PROGRESS, getTaskProgress(),
+                        toTransform.p));
+            }
+            ValueAnimator updateCallbackAnim = ValueAnimator.ofInt(0, 1);
+            updateCallbackAnim.addUpdateListener(updateCallback);
+            mTmpAnimators.add(updateCallbackAnim);
+
+            // Create the animator
+            mTransformAnimation = toAnimation.createAnimator(mTmpAnimators);
+            if (requiresHwLayers) {
+                setLayerType(View.LAYER_TYPE_HARDWARE, null);
+                mTransformAnimation.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        setLayerType(View.LAYER_TYPE_NONE, null);
+                    }
+                });
+            }
+            mTransformAnimation.start();
         }
     }
 
     /** Resets this view's properties */
     void resetViewProperties() {
+        Utilities.cancelAnimation(mTransformAnimation);
         setDim(0);
         setVisibility(View.VISIBLE);
         getViewBounds().reset();
         TaskViewTransform.reset(this);
-        if (mActionButtonView != null) {
-            mActionButtonView.setScaleX(1f);
-            mActionButtonView.setScaleY(1f);
-            mActionButtonView.setAlpha(1f);
-            mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
-        }
+
+        mActionButtonView.setScaleX(1f);
+        mActionButtonView.setScaleY(1f);
+        mActionButtonView.setAlpha(1f);
+        mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
     }
 
-    /** Prepares this task view for the enter-recents animations.  This is called earlier in the
-     * first layout because the actual animation into recents may take a long time. */
-    void prepareEnterRecentsAnimation(boolean hideTask, boolean occludesLaunchTarget,
-            int offscreenY) {
-        RecentsConfiguration config = Recents.getConfiguration();
-        RecentsActivityLaunchState launchState = config.getLaunchState();
-        int initialDim = getDim();
-        if (hideTask) {
-            setVisibility(View.INVISIBLE);
-        } else if (launchState.launchedHasConfigurationChanged) {
-            // Just load the views as-is
-        } else if (launchState.launchedFromAppWithThumbnail) {
-            if (mTask.isLaunchTarget) {
-                // Set the dim to 0 so we can animate it in
-                initialDim = 0;
-                // Hide the action button
-                mActionButtonView.setAlpha(0f);
-            } else if (occludesLaunchTarget) {
-                // Move the task view off screen (below) so we can animate it in
-                setTranslationY(offscreenY);
-            }
-
-        } else if (launchState.launchedFromHome) {
-            // Move the task view off screen (below) so we can animate it in
-            setTranslationY(offscreenY);
-            setTranslationZ(0);
-            setScaleX(1f);
-            setScaleY(1f);
-        }
-        // Apply the current dim
-        setDim(initialDim);
-    }
-
-    /** Animates this task view as it enters recents */
-    void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) {
-        RecentsConfiguration config = Recents.getConfiguration();
-        RecentsActivityLaunchState launchState = config.getLaunchState();
-        Resources res = mContext.getResources();
-        final TaskViewTransform transform = ctx.currentTaskTransform;
-        final int taskViewEnterFromAppDuration = res.getInteger(
-                R.integer.recents_task_enter_from_app_duration);
-        final int taskViewEnterFromHomeDuration = res.getInteger(
-                R.integer.recents_task_enter_from_home_duration);
-        final int taskViewEnterFromHomeStaggerDelay = res.getInteger(
-                R.integer.recents_task_enter_from_home_stagger_delay);
-        final int taskViewAffiliateGroupEnterOffset = res.getDimensionPixelSize(
-                R.dimen.recents_task_view_affiliate_group_enter_offset);
-
-        if (launchState.launchedFromAppWithThumbnail) {
-            if (mTask.isLaunchTarget) {
-                ctx.postAnimationTrigger.increment();
-                // Start the dim animation
-                animateDimToProgress(taskViewEnterFromAppDuration,
-                        ctx.postAnimationTrigger.decrementOnAnimationEnd());
-
-                // Start the action button animation
-                if (ctx.isScreenPinningEnabled) {
-                    showActionButton(true /* fadeIn */,
-                            taskViewEnterFromAppDuration /* fadeInDuration */);
-                }
-            } else {
-                // Animate the task up if it was occluding the launch target
-                if (ctx.currentTaskOccludesLaunchTarget) {
-                    setTranslationY(taskViewAffiliateGroupEnterOffset);
-                    setAlpha(0f);
-                    animate().alpha(1f)
-                            .translationY(0)
-                            .setUpdateListener(null)
-                            .setListener(new AnimatorListenerAdapter() {
-                                private boolean hasEnded;
-
-                                // We use the animation listener instead of withEndAction() to
-                                // ensure that onAnimationEnd() is called when the animator is
-                                // cancelled
-                                @Override
-                                public void onAnimationEnd(Animator animation) {
-                                    if (hasEnded) return;
-                                    ctx.postAnimationTrigger.decrement();
-                                    hasEnded = true;
-                                }
-                            })
-                            .setInterpolator(mFastOutSlowInInterpolator)
-                            .setDuration(taskViewEnterFromHomeDuration)
-                            .start();
-                    ctx.postAnimationTrigger.increment();
-                }
-            }
-
-        } else if (launchState.launchedFromHome) {
-            // Animate the tasks up
-            int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1);
-            int delay = frontIndex * taskViewEnterFromHomeStaggerDelay;
-
-            setScaleX(transform.scale);
-            setScaleY(transform.scale);
-            if (!config.fakeShadows) {
-                animate().translationZ(transform.translationZ);
-            }
-            animate()
-                    .translationY(0)
-                    .setStartDelay(delay)
-                    .setUpdateListener(ctx.updateListener)
-                    .setListener(new AnimatorListenerAdapter() {
-                        private boolean hasEnded;
-
-                        // We use the animation listener instead of withEndAction() to ensure that
-                        // onAnimationEnd() is called when the animator is cancelled
-                        @Override
-                        public void onAnimationEnd(Animator animation) {
-                            if (hasEnded) return;
-                            ctx.postAnimationTrigger.decrement();
-                            hasEnded = true;
-                        }
-                    })
-                    .setInterpolator(mQuintOutInterpolator)
-                    .setDuration(taskViewEnterFromHomeDuration +
-                            frontIndex * taskViewEnterFromHomeStaggerDelay)
-                    .start();
-            ctx.postAnimationTrigger.increment();
-        }
-    }
-
-    public void cancelEnterRecentsAnimation() {
-        animate().cancel();
-    }
-
-    /** Animates this task view as it leaves recents by pressing home. */
-    void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) {
-        int taskViewExitToHomeDuration = getResources().getInteger(
-                R.integer.recents_task_exit_to_home_duration);
-        animate()
-                .translationY(ctx.offscreenTranslationY)
-                .setStartDelay(0)
-                .setUpdateListener(null)
-                .setListener(null)
-                .setInterpolator(mFastOutLinearInInterpolator)
-                .setDuration(taskViewExitToHomeDuration)
-                .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable())
-                .start();
-        ctx.postAnimationTrigger.increment();
-    }
-
-    /** Animates this task view as it exits recents */
-    void startLaunchTaskAnimation(final Runnable postAnimRunnable, boolean isLaunchingTask,
-            boolean occludesLaunchTarget, boolean lockToTask) {
-        final int taskViewExitToAppDuration = mContext.getResources().getInteger(
-                R.integer.recents_task_exit_to_app_duration);
-        final int taskViewAffiliateGroupEnterOffset = mContext.getResources().getDimensionPixelSize(
-                R.dimen.recents_task_view_affiliate_group_enter_offset);
-
-        if (isLaunchingTask) {
-            // Animate the dim
-            if (mDimAlpha > 0) {
-                ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0);
-                anim.setDuration(taskViewExitToAppDuration);
-                anim.setInterpolator(mFastOutLinearInInterpolator);
-                anim.start();
-            }
-
-            // Animate the action button away
-            if (!lockToTask) {
-                float toScale = 0.9f;
-                mActionButtonView.animate()
-                        .scaleX(toScale)
-                        .scaleY(toScale);
-            }
-            mActionButtonView.animate()
-                    .alpha(0f)
-                    .setStartDelay(0)
-                    .setDuration(taskViewExitToAppDuration)
-                    .setInterpolator(PhoneStatusBar.ALPHA_OUT)
-                    .withEndAction(postAnimRunnable)
-                    .withLayer()
-                    .start();
-        } else {
-            // Hide the dismiss button
-            mHeaderView.startLaunchTaskDismissAnimation(postAnimRunnable);
-            // If this is another view in the task grouping and is in front of the launch task,
-            // animate it away first
-            if (occludesLaunchTarget) {
-                animate().alpha(0f)
-                    .translationY(getTranslationY() + taskViewAffiliateGroupEnterOffset)
-                    .setStartDelay(0)
-                    .setUpdateListener(null)
-                    .setListener(null)
-                    .setInterpolator(mFastOutLinearInInterpolator)
-                    .setDuration(taskViewExitToAppDuration)
-                    .start();
-            }
-        }
-    }
-
-    /** Animates the deletion of this task view */
-    void startDeleteTaskAnimation(final Runnable r, int delay) {
-        int taskViewRemoveAnimDuration = getResources().getInteger(
-                R.integer.recents_animate_task_view_remove_duration);
-        int taskViewRemoveAnimTranslationXPx = getResources().getDimensionPixelSize(
-                R.dimen.recents_task_view_remove_anim_translation_x);
-
-        // Disabling clipping with the stack while the view is animating away
-        setClipViewInStack(false);
-
-        animate().translationX(taskViewRemoveAnimTranslationXPx)
-            .alpha(0f)
-            .setStartDelay(delay)
-            .setUpdateListener(null)
-            .setListener(null)
-            .setInterpolator(mFastOutSlowInInterpolator)
-            .setDuration(taskViewRemoveAnimDuration)
-            .withEndAction(new Runnable() {
-                @Override
-                public void run() {
-                    if (r != null) {
-                        r.run();
-                    }
-
-                    // Re-enable clipping with the stack (we will reuse this view)
-                    setClipViewInStack(true);
-                }
-            })
-            .start();
+    /**
+     * Cancels any current transform animations.
+     */
+    public void cancelTransformAnimation() {
+        Utilities.cancelAnimation(mTransformAnimation);
     }
 
     /** Enables/disables handling touch on this task view. */
@@ -600,12 +389,8 @@
             }
         } else {
             float dimAlpha = mDimAlpha / 255.0f;
-            if (mThumbnailView != null) {
-                mThumbnailView.setDimAlpha(dimAlpha);
-            }
-            if (mHeaderView != null) {
-                mHeaderView.setDimAlpha(dim);
-            }
+            mThumbnailView.setDimAlpha(dimAlpha);
+            mHeaderView.setDimAlpha(dim);
         }
     }
 
@@ -615,18 +400,18 @@
     }
 
     /** Animates the dim to the task progress. */
-    void animateDimToProgress(int duration, Animator.AnimatorListener postAnimRunnable) {
+    void animateDimToProgress(int duration, Animator.AnimatorListener animListener) {
         // Animate the dim into view as well
         int toDim = getDimFromTaskProgress();
         if (toDim != getDim()) {
-            ObjectAnimator anim = ObjectAnimator.ofInt(TaskView.this, "dim", toDim);
+            ObjectAnimator anim = ObjectAnimator.ofInt(this, DIM, getDim(), toDim);
             anim.setDuration(duration);
-            if (postAnimRunnable != null) {
-                anim.addListener(postAnimRunnable);
+            if (animListener != null) {
+                anim.addListener(animListener);
             }
             anim.start();
         } else {
-            postAnimRunnable.onAnimationEnd(null);
+            animListener.onAnimationEnd(null);
         }
     }
 
@@ -647,14 +432,7 @@
     /**
      * Explicitly sets the focused state of this task.
      */
-    public void setFocusedState(boolean isFocused, boolean animated, boolean requestViewFocus) {
-        if (DEBUG) {
-            Log.d(TAG, "setFocusedState: " + mTask.title + " focused: " + isFocused +
-                    " animated: " + animated + " requestViewFocus: " + requestViewFocus +
-                    " isFocused(): " + isFocused() +
-                    " isAccessibilityFocused(): " + isAccessibilityFocused());
-        }
-
+    public void setFocusedState(boolean isFocused, boolean requestViewFocus) {
         SystemServicesProxy ssp = Recents.getSystemServices();
         if (isFocused) {
             if (requestViewFocus && !isFocused()) {
@@ -681,28 +459,101 @@
 
         if (fadeIn) {
             if (mActionButtonView.getAlpha() < 1f) {
-                mActionButtonView.setAlpha(0f);
-                mActionButtonView.animate().alpha(1f)
+                mActionButtonView.animate()
+                        .alpha(1f)
+                        .scaleX(1f)
+                        .scaleY(1f)
                         .setDuration(fadeInDuration)
                         .setInterpolator(PhoneStatusBar.ALPHA_IN)
                         .withLayer()
                         .start();
             }
         } else {
+            mActionButtonView.setScaleX(1f);
+            mActionButtonView.setScaleY(1f);
             mActionButtonView.setAlpha(1f);
+            mActionButtonView.setTranslationZ(mActionButtonTranslationZ);
         }
     }
 
     /**
      * Immediately hides the action button.
+     *
+     * @param fadeOut whether or not to animate the action button out.
      */
-    public void hideActionButton() {
-        mActionButtonView.setVisibility(View.INVISIBLE);
+    public void hideActionButton(boolean fadeOut, int fadeOutDuration, boolean scaleDown,
+            final Animator.AnimatorListener animListener) {
+        if (fadeOut) {
+            if (mActionButtonView.getAlpha() > 0f) {
+                if (scaleDown) {
+                    float toScale = 0.9f;
+                    mActionButtonView.animate()
+                            .scaleX(toScale)
+                            .scaleY(toScale);
+                }
+                mActionButtonView.animate()
+                        .alpha(0f)
+                        .setDuration(fadeOutDuration)
+                        .setInterpolator(PhoneStatusBar.ALPHA_OUT)
+                        .withEndAction(new Runnable() {
+                            @Override
+                            public void run() {
+                                if (animListener != null) {
+                                    animListener.onAnimationEnd(null);
+                                }
+                                mActionButtonView.setVisibility(View.INVISIBLE);
+                            }
+                        })
+                        .withLayer()
+                        .start();
+            }
+        } else {
+            mActionButtonView.setAlpha(0f);
+            mActionButtonView.setVisibility(View.INVISIBLE);
+            if (animListener != null) {
+                animListener.onAnimationEnd(null);
+            }
+        }
+    }
+
+    /**** TaskStackAnimationHelper.Callbacks Implementation ****/
+
+    @Override
+    public void onPrepareLaunchTargetForEnterAnimation() {
+        // These values will be animated in when onStartLaunchTargetEnterAnimation() is called
+        setDim(0);
+        mActionButtonView.setAlpha(0f);
+    }
+
+    @Override
+    public void onStartLaunchTargetEnterAnimation(int duration, boolean screenPinningEnabled,
+            ReferenceCountedTrigger postAnimationTrigger) {
+        postAnimationTrigger.increment();
+        animateDimToProgress(duration, postAnimationTrigger.decrementOnAnimationEnd());
+
+        if (screenPinningEnabled) {
+            showActionButton(true /* fadeIn */, duration /* fadeInDuration */);
+        }
+    }
+
+    @Override
+    public void onStartLaunchTargetLaunchAnimation(int duration, boolean screenPinningRequested,
+            ReferenceCountedTrigger postAnimationTrigger) {
+        if (mDimAlpha > 0) {
+            ObjectAnimator anim = ObjectAnimator.ofInt(this, DIM, getDim(), 0);
+            anim.setDuration(duration);
+            anim.setInterpolator(PhoneStatusBar.ALPHA_OUT);
+            anim.start();
+        }
+
+        postAnimationTrigger.increment();
+        hideActionButton(true /* fadeOut */, duration,
+                !screenPinningRequested /* scaleDown */,
+                postAnimationTrigger.decrementOnAnimationEnd());
     }
 
     /**** TaskCallbacks Implementation ****/
 
-    /** Binds this task view to the task */
     public void onTaskBound(Task t) {
         mTask = t;
         mTask.addCallback(this);
@@ -710,22 +561,18 @@
 
     @Override
     public void onTaskDataLoaded(Task task) {
-        if (mThumbnailView != null && mHeaderView != null) {
-            // Bind each of the views to the new task data
-            mThumbnailView.rebindToTask(mTask);
-            mHeaderView.rebindToTask(mTask);
-        }
+        // Bind each of the views to the new task data
+        mThumbnailView.rebindToTask(mTask);
+        mHeaderView.rebindToTask(mTask);
         mTaskDataLoaded = true;
     }
 
     @Override
     public void onTaskDataUnloaded() {
-        if (mThumbnailView != null && mHeaderView != null) {
-            // Unbind each of the views from the task data and remove the task callback
-            mTask.removeCallback(this);
-            mThumbnailView.unbindFromTask();
-            mHeaderView.unbindFromTask();
-        }
+        // Unbind each of the views from the task data and remove the task callback
+        mTask.removeCallback(this);
+        mThumbnailView.unbindFromTask();
+        mHeaderView.unbindFromTask();
         mTaskDataLoaded = false;
     }
 
@@ -761,17 +608,6 @@
             // Start listening for drag events
             setClipViewInStack(false);
 
-            // Enlarge the view slightly
-            final float finalScale = getScaleX() * 1.05f;
-            animate()
-                    .scaleX(finalScale)
-                    .scaleY(finalScale)
-                    .setDuration(175)
-                    .setUpdateListener(null)
-                    .setListener(null)
-                    .setInterpolator(mFastOutSlowInInterpolator)
-                    .start();
-
             mDownTouchPos.x += ((1f - getScaleX()) * getWidth()) / 2;
             mDownTouchPos.y += ((1f - getScaleY()) * getHeight()) / 2;
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewAnimation.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewAnimation.java
new file mode 100644
index 0000000..0e7f677
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewAnimation.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2016 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.systemui.recents.views;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+
+import java.util.List;
+
+/**
+ * The animation properties to animate a {@link TaskView} to a given {@TaskViewTransform}.
+ */
+public class TaskViewAnimation {
+
+    public static final TaskViewAnimation IMMEDIATE = new TaskViewAnimation(0,
+            new LinearInterpolator());
+
+    public final int startDelay;
+    public final int duration;
+    public final Interpolator interpolator;
+    public final Animator.AnimatorListener listener;
+
+    public TaskViewAnimation(int duration, Interpolator interpolator) {
+        this(0 /* startDelay */, duration, interpolator, null);
+    }
+
+    public TaskViewAnimation(int duration, Interpolator interpolator,
+            Animator.AnimatorListener listener) {
+        this(0 /* startDelay */, duration, interpolator, listener);
+    }
+
+    public TaskViewAnimation(int startDelay, int duration, Interpolator interpolator,
+            Animator.AnimatorListener listener) {
+        this.startDelay = startDelay;
+        this.duration = duration;
+        this.interpolator = interpolator;
+        this.listener = listener;
+    }
+
+    /**
+     * Creates a new {@link AnimatorSet} that will animate the given animators with the current
+     * animation properties.
+     */
+    public AnimatorSet createAnimator(List<Animator> animators) {
+        AnimatorSet anim = new AnimatorSet();
+        anim.setStartDelay(startDelay);
+        anim.setDuration(duration);
+        anim.setInterpolator(interpolator);
+        if (listener != null) {
+            anim.addListener(listener);
+        }
+        anim.playTogether(animators);
+        return anim;
+    }
+
+    /**
+     * Returns whether this animation has any duration.
+     */
+    public boolean isImmediate() {
+        return duration <= 0;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
index 9a2ffe7..e8b7574 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewHeader.java
@@ -291,22 +291,6 @@
         mMoveTaskButton.setOnClickListener(null);
     }
 
-    /** Animates this task bar dismiss button when launching a task. */
-    void startLaunchTaskDismissAnimation(final Runnable postAnimationRunanble) {
-        if (mDismissButton.getVisibility() == View.VISIBLE) {
-            int taskViewExitToAppDuration = mContext.getResources().getInteger(
-                    R.integer.recents_task_exit_to_app_duration);
-            mDismissButton.animate().cancel();
-            mDismissButton.animate()
-                    .alpha(0f)
-                    .setStartDelay(0)
-                    .setInterpolator(mFastOutSlowInInterpolator)
-                    .setDuration(taskViewExitToAppDuration)
-                    .withEndAction(postAnimationRunanble)
-                    .start();
-        }
-    }
-
     /** Animates this task bar if the user does not interact with the stack after a certain time. */
     void startNoUserInteractionAnimation() {
         if (mDismissButton.getVisibility() != View.VISIBLE) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
index 3ee50ac..14bab64 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
@@ -16,16 +16,19 @@
 
 package com.android.systemui.recents.views;
 
-import android.animation.ValueAnimator;
+import android.animation.Animator;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
 import android.graphics.RectF;
 import android.util.IntProperty;
 import android.util.Property;
 import android.view.View;
-import android.view.ViewPropertyAnimator;
-import android.view.animation.Interpolator;
 
+import java.util.ArrayList;
 
-/* The transform state for a task view */
+/**
+ * The visual properties for a {@link TaskView}.
+ */
 public class TaskViewTransform {
 
     public static final Property<View, Integer> LEFT =
@@ -80,9 +83,6 @@
                 }
             };
 
-    // TODO: Move this out of the transform
-    public int startDelay = 0;
-
     public float translationZ = 0;
     public float scale = 1f;
     public float alpha = 1f;
@@ -94,15 +94,10 @@
     // This is a window-space rect used for positioning the task in the stack and freeform workspace
     public RectF rect = new RectF();
 
-    public TaskViewTransform() {
-        // Do nothing
-    }
-
     /**
      * Resets the current transform.
      */
     public void reset() {
-        startDelay = 0;
         translationZ = 0;
         scale = 1f;
         alpha = 1f;
@@ -116,50 +111,34 @@
     public boolean hasAlphaChangedFrom(float v) {
         return (Float.compare(alpha, v) != 0);
     }
+
     public boolean hasScaleChangedFrom(float v) {
         return (Float.compare(scale, v) != 0);
     }
+
     public boolean hasTranslationZChangedFrom(float v) {
         return (Float.compare(translationZ, v) != 0);
     }
 
-    /** Applies this transform to a view. */
-    public void applyToTaskView(TaskView v, int duration, Interpolator interp, boolean allowLayers,
-            boolean allowShadows, ValueAnimator.AnimatorUpdateListener updateCallback) {
-        // Check to see if any properties have changed, and update the task view
-        if (duration > 0) {
-            ViewPropertyAnimator anim = v.animate();
-            boolean requiresLayers = false;
+    public boolean hasRectChangedFrom(View v) {
+        return ((int) rect.left != v.getLeft()) || ((int) rect.right != v.getRight()) ||
+                ((int) rect.top != v.getTop()) || ((int) rect.bottom != v.getBottom());
+    }
 
-            // Animate to the final state
-            if (allowShadows && hasTranslationZChangedFrom(v.getTranslationZ())) {
-                anim.translationZ(translationZ);
-            }
-            if (hasScaleChangedFrom(v.getScaleX())) {
-                anim.scaleX(scale)
-                    .scaleY(scale);
-                requiresLayers = true;
-            }
-            if (hasAlphaChangedFrom(v.getAlpha())) {
-                // Use layers if we animate alpha
-                anim.alpha(alpha);
-                requiresLayers = true;
-            }
-            if (requiresLayers && allowLayers) {
-                anim.withLayer();
-            }
-            if (updateCallback != null) {
-                anim.setUpdateListener(updateCallback);
-            } else {
-                anim.setUpdateListener(null);
-            }
-            anim.setListener(null);
-            anim.setStartDelay(startDelay)
-                    .setDuration(duration)
-                    .setInterpolator(interp)
-                    .start();
-        } else {
-            // Set the changed properties
+    /**
+     * Applies this transform to a view.
+     *
+     * @return whether hardware layers are required for this animation.
+     */
+    public boolean applyToTaskView(TaskView v, ArrayList<Animator> animators,
+            TaskViewAnimation taskAnimation, boolean allowShadows) {
+        // Return early if not visible
+        boolean requiresHwLayers = false;
+        if (!visible) {
+            return requiresHwLayers;
+        }
+
+        if (taskAnimation.isImmediate()) {
             if (allowShadows && hasTranslationZChangedFrom(v.getTranslationZ())) {
                 v.setTranslationZ(translationZ);
             }
@@ -170,29 +149,45 @@
             if (hasAlphaChangedFrom(v.getAlpha())) {
                 v.setAlpha(alpha);
             }
+            if (hasRectChangedFrom(v)) {
+                v.setLeftTopRightBottom((int) rect.left, (int) rect.top, (int) rect.right,
+                        (int) rect.bottom);
+            }
+        } else {
+            if (allowShadows && hasTranslationZChangedFrom(v.getTranslationZ())) {
+                animators.add(ObjectAnimator.ofFloat(v, View.TRANSLATION_Z, v.getTranslationZ(),
+                        translationZ));
+            }
+            if (hasScaleChangedFrom(v.getScaleX())) {
+                animators.add(ObjectAnimator.ofPropertyValuesHolder(v,
+                        PropertyValuesHolder.ofFloat(View.SCALE_X, v.getScaleX(), scale),
+                        PropertyValuesHolder.ofFloat(View.SCALE_Y, v.getScaleX(), scale)));
+            }
+            if (hasAlphaChangedFrom(v.getAlpha())) {
+                animators.add(ObjectAnimator.ofFloat(v, View.ALPHA, v.getAlpha(), alpha));
+                requiresHwLayers = true;
+            }
+            if (hasRectChangedFrom(v)) {
+                animators.add(ObjectAnimator.ofPropertyValuesHolder(v,
+                        PropertyValuesHolder.ofInt(LEFT, v.getLeft(), (int) rect.left),
+                        PropertyValuesHolder.ofInt(TOP, v.getTop(), (int) rect.top),
+                        PropertyValuesHolder.ofInt(RIGHT, v.getRight(), (int) rect.right),
+                        PropertyValuesHolder.ofInt(BOTTOM, v.getBottom(), (int) rect.bottom)));
+            }
         }
+        return requiresHwLayers;
     }
 
     /** Reset the transform on a view. */
     public static void reset(TaskView v) {
-        // Cancel any running animations and reset the translation in case something else (like a
-        // dismiss animation) changes it
-        v.animate().cancel();
         v.setTranslationX(0f);
         v.setTranslationY(0f);
         v.setTranslationZ(0f);
         v.setScaleX(1f);
         v.setScaleY(1f);
         v.setAlpha(1f);
-        v.getViewBounds().setClipBottom(0, false /* forceUpdate */);
+        v.getViewBounds().setClipBottom(0);
         v.setLeftTopRightBottom(0, 0, 0, 0);
         v.mThumbnailView.setBitmapScale(1f);
     }
-
-    @Override
-    public String toString() {
-        return "TaskViewTransform delay: " + startDelay + " z: " + translationZ +
-                " scale: " + scale + " alpha: " + alpha + " visible: " + visible +
-                " rect: " + rect + " p: " + p;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/ViewAnimation.java b/packages/SystemUI/src/com/android/systemui/recents/views/ViewAnimation.java
deleted file mode 100644
index eaef51c..0000000
--- a/packages/SystemUI/src/com/android/systemui/recents/views/ViewAnimation.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2014 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.systemui.recents.views;
-
-import android.animation.ValueAnimator;
-import android.graphics.Rect;
-import com.android.systemui.recents.misc.ReferenceCountedTrigger;
-
-/* Common code related to view animations */
-public class ViewAnimation {
-
-    /* The animation context for a task view animation into Recents */
-    public static class TaskViewEnterContext {
-        // A trigger to run some logic when all the animations complete.  This works around the fact
-        // that it is difficult to coordinate ViewPropertyAnimators
-        public ReferenceCountedTrigger postAnimationTrigger;
-        // An update listener to notify as the enter animation progresses (used for the home transition)
-        ValueAnimator.AnimatorUpdateListener updateListener;
-
-        // These following properties are updated for each task view we start the enter animation on
-
-        // Whether or not screen pinning is enabled
-        boolean isScreenPinningEnabled;
-        // Whether or not the current task occludes the launch target
-        boolean currentTaskOccludesLaunchTarget;
-        // The task rect for the current stack
-        Rect currentTaskRect;
-        // The transform of the current task view
-        TaskViewTransform currentTaskTransform;
-        // The view index of the current task view
-        int currentStackViewIndex;
-        // The total number of task views
-        int currentStackViewCount;
-
-        public TaskViewEnterContext(ReferenceCountedTrigger t) {
-            postAnimationTrigger = t;
-        }
-    }
-
-    /* The animation context for a task view animation out of Recents */
-    public static class TaskViewExitContext {
-        // A trigger to run some logic when all the animations complete.  This works around the fact
-        // that it is difficult to coordinate ViewPropertyAnimators
-        ReferenceCountedTrigger postAnimationTrigger;
-
-        // The translationY to apply to a TaskView to move it off the bottom of the task stack
-        int offscreenTranslationY;
-
-        public TaskViewExitContext(ReferenceCountedTrigger t) {
-            postAnimationTrigger = t;
-        }
-    }
-
-}