Updating freeform layout to be static at the top of recents.

Change-Id: I5118d03c115080e05447d325097419b9a1c6f8b4
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java b/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java
index ea6821f..515c3bd 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java
@@ -189,6 +189,7 @@
     public float computePOffsetForScaledHeight(int height, Rect bounds) {
         int top = bounds.top;
         int bottom = bounds.bottom;
+        height = Math.min(height, bottom - top);
 
         if (bounds.height() == 0) {
             return 0;
@@ -231,6 +232,7 @@
     public float computePOffsetForHeight(int height, Rect bounds) {
         int top = bounds.top;
         int bottom = bounds.bottom;
+        height = Math.min(height, bottom - top);
 
         if (bounds.height() == 0) {
             return 0;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
index bdd3440..2fe5e98 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -235,6 +235,15 @@
         return null;
     }
 
+    /**
+     * Returns whether this device has freeform workspaces.
+     */
+    public boolean hasFreeformWorkspaceSupport() {
+        if (mPm == null) return false;
+
+        return mPm.hasSystemFeature(PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT);
+    }
+
     /** Returns whether the recents is currently running */
     public boolean isRecentsTopMost(ActivityManager.RunningTaskInfo topTask,
             MutableBoolean isHomeTopMost) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java
index ce993c5..a97a2a8 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/FreeformWorkspaceLayoutAlgorithm.java
@@ -17,10 +17,11 @@
 package com.android.systemui.recents.views;
 
 import android.util.Log;
+import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.recents.model.Task;
 
-import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 /**
  * The layout logic for the contents of the freeform workspace.
@@ -33,6 +34,7 @@
     // The number of cells in the freeform workspace
     private int mFreeformCellXCount;
     private int mFreeformCellYCount;
+
     // The width and height of the cells in the freeform workspace
     private int mFreeformCellWidth;
     private int mFreeformCellHeight;
@@ -44,22 +46,26 @@
      * Updates the layout for each of the freeform workspace tasks.  This is called after the stack
      * layout is updated.
      */
-    public void update(ArrayList<Task> freeformTasks, TaskStackLayoutAlgorithm stackLayout) {
+    public void update(List<Task> freeformTasks, TaskStackLayoutAlgorithm stackLayout) {
+        mTaskIndexMap.clear();
+
         int numFreeformTasks = stackLayout.mNumFreeformTasks;
         if (!freeformTasks.isEmpty()) {
             // Calculate the cell width/height depending on the number of freeform tasks
             mFreeformCellXCount = Math.max(2, (int) Math.ceil(Math.sqrt(numFreeformTasks)));
-            mFreeformCellYCount = Math.max(2, (int) Math.ceil((float) numFreeformTasks / mFreeformCellXCount));
-            mFreeformCellWidth = stackLayout.mFreeformRect.width() / mFreeformCellXCount;
+            mFreeformCellYCount = Math.max(2, (int) Math.ceil((float) numFreeformTasks /
+                    mFreeformCellXCount));
             // For now, make the cells square
+            mFreeformCellWidth = Math.min(stackLayout.mFreeformRect.width() / mFreeformCellXCount,
+                    stackLayout.mFreeformRect.height() / mFreeformCellYCount);
             mFreeformCellHeight = mFreeformCellWidth;
 
             // Put each of the tasks in the progress map at a fixed index (does not need to actually
             // map to a scroll position, just by index)
             int taskCount = freeformTasks.size();
-            for (int i = 0; i < taskCount; i++) {
+            for (int i = taskCount - 1; i >= 0; i--) {
                 Task task = freeformTasks.get(i);
-                mTaskIndexMap.put(task.key, i);
+                mTaskIndexMap.put(task.key, taskCount - i - 1);
             }
 
             if (DEBUG) {
@@ -74,24 +80,23 @@
     /**
      * Returns whether the transform is available for the given task.
      */
-    public boolean isTransformAvailable(Task task, float stackScroll,
-            TaskStackLayoutAlgorithm stackLayout) {
-        if (stackLayout.mNumFreeformTasks == 0 || task == null ||
-                !mTaskIndexMap.containsKey(task.key)) {
+    public boolean isTransformAvailable(Task task, TaskStackLayoutAlgorithm stackLayout) {
+        if (stackLayout.mNumFreeformTasks == 0 || task == null) {
             return false;
         }
-        return stackScroll > stackLayout.mStackEndScrollP;
+        return mTaskIndexMap.containsKey(task.key);
     }
 
     /**
      * Returns the transform for the given task.  Any rect returned will be offset by the actual
      * transform for the freeform workspace.
      */
-    public TaskViewTransform getTransform(Task task, float stackScroll,
-            TaskViewTransform transformOut, TaskStackLayoutAlgorithm stackLayout) {
-        if (Float.compare(stackScroll, stackLayout.mStackEndScrollP) > 0) {
+    public TaskViewTransform getTransform(Task task, TaskViewTransform transformOut,
+            TaskStackLayoutAlgorithm stackLayout) {
+        if (mTaskIndexMap.containsKey(task.key)) {
             // This is a freeform task, so lay it out in the freeform workspace
             int taskIndex = mTaskIndexMap.get(task.key);
+            int topOffset = (stackLayout.mFreeformRect.top - stackLayout.mTaskRect.top);
             int x = taskIndex % mFreeformCellXCount;
             int y = taskIndex / mFreeformCellXCount;
             float scale = (float) mFreeformCellWidth / stackLayout.mTaskRect.width();
@@ -99,8 +104,13 @@
             int scaleYOffset = (int) (((1f - scale) * stackLayout.mTaskRect.height()) / 2);
             transformOut.scale = scale * 0.9f;
             transformOut.translationX = x * mFreeformCellWidth - scaleXOffset;
-            transformOut.translationY = y * mFreeformCellHeight - scaleYOffset;
+            transformOut.translationY = topOffset + y * mFreeformCellHeight - scaleYOffset;
+            transformOut.translationZ = stackLayout.mMaxTranslationZ;
+            transformOut.rect.set(stackLayout.mTaskRect);
+            transformOut.rect.offset(transformOut.translationX, transformOut.translationY);
+            Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
             transformOut.visible = true;
+            transformOut.p = 0;
             return transformOut;
         }
         return null;
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 45d626e..a0713d7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -24,12 +24,14 @@
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsConfiguration;
 import com.android.systemui.recents.misc.ParametricCurve;
+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 java.util.ArrayList;
 import java.util.HashMap;
+import java.util.LinkedList;
 
 
 /**
@@ -44,14 +46,8 @@
     private static final float STACK_PEEK_MIN_SCALE = 0.85f;
     // The scale of the last task
     private static final float SINGLE_TASK_SCALE = 0.95f;
-    // The percentage of the height of the stack that we want to show the last task at
-    private static final float VISIBLE_LAST_TASK_HEIGHT_PCT = 0.45f;
     // The percentage of height of task to show between tasks
     private static final float VISIBLE_TASK_HEIGHT_BETWEEN_TASKS = 0.5f;
-    // The percentage between the maxStackScroll and the maxScroll where a given scroll will still
-    // snap back to the maxStackScroll instead of to the maxScroll (which shows the freeform
-    // workspace)
-    private static final float SNAP_TO_MAX_STACK_SCROLL_FACTOR = 0.3f;
 
     // A report of the visibility state of the stack
     public class VisibilityReport {
@@ -67,14 +63,36 @@
 
     Context mContext;
 
-    // This is the view bounds inset exactly by the search bar, but without the bottom inset
-    // see RecentsConfiguration.getTaskStackBounds()
-    public Rect mStackRect = new Rect();
-    // This is the task view bounds for layout (untransformed), the rect is top-aligned to the top
-    // of the stack rect
+    /*
+    +-------------------+
+    | SEARCH            |
+    +-------------------+
+    |+-----------------+|
+    || FREEFORM        ||
+    ||                 ||
+    ||                 ||
+    |+-----------------+|
+    |   +-----------+   |
+    | +---------------+ |
+    | |               | |
+    |+-----------------+|
+    || STACK           ||
+    +-------------------+
+     */
+
+    // The task bounds (untransformed) for layout.  This rect is anchored at mTaskRoot.
     public Rect mTaskRect = new Rect();
-    // The bounds of the freeform workspace, the rect is top-aligned to the top of the stack rect
+    // The freeform workspace bounds, inset from the top by the search bar, and is a fixed height
     public Rect mFreeformRect = new Rect();
+    // The freeform stack bounds, inset from the top by the search bar and freeform workspace, and
+    // runs to the bottom of the screen
+    private Rect mFreeformStackRect = new Rect();
+    // The stack bounds, inset from the top by the search bar, and runs to
+    // the bottom of the screen
+    private Rect mStackRect = new Rect();
+    // The current stack rect, can either by mFreeformStackRect or mStackRect depending on whether
+    // there is a freeform workspace
+    public Rect mCurrentStackRect;
     // This is the current system insets
     public Rect mSystemInsets = new Rect();
 
@@ -83,13 +101,6 @@
     // The largest scroll progress, at this value, the front most task will be visible above the
     // navigation bar
     float mMaxScrollP;
-    // The scroll progress at which bottom of the first task of the stack is aligned with the bottom
-    // of the stack
-    float mStackEndScrollP;
-    // The scroll progress that we actually want to scroll the user to when they want to go to the
-    // end of the stack (it accounts for the nav bar, so that the bottom of the task is offset from
-    // the bottom of the stack)
-    float mPreferredStackEndScrollP;
     // The initial progress that the scroller is set when you first enter recents
     float mInitialScrollP;
     // The task progress for the front-most task in the stack
@@ -109,13 +120,6 @@
     // The relative progress to ensure that the offset from the bottom of the stack to the bottom
     // of the task is respected
     float mStackBottomPOffset;
-    // The freeform workspace gap
-    int mFreeformWorkspaceGapOffset;
-    float mFreeformWorkspaceGapPOffset;
-    // The relative progress to ensure that the freeform workspace height + gap + stack bottom
-    // padding is respected
-    int mFreeformWorkspaceOffset;
-    float mFreeformWorkspacePOffset;
 
     // The last computed task counts
     int mNumStackTasks;
@@ -130,9 +134,6 @@
     // The freeform workspace layout
     FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
 
-    // Temporary task view transform
-    TaskViewTransform mTmpTransform = new TaskViewTransform();
-
     // Log function
     static ParametricCurve sCurve;
 
@@ -190,53 +191,57 @@
     }
 
     /**
-     * Computes the stack and task rects.
+     * Computes the stack and task rects.  The given task stack bounds is the whole bounds not
+     * including the search bar.
      */
     public void initialize(Rect taskStackBounds) {
+        SystemServicesProxy ssp = Recents.getSystemServices();
         RecentsConfiguration config = Recents.getConfiguration();
         int widthPadding = (int) (config.taskStackWidthPaddingPct * taskStackBounds.width());
         int heightPadding = mContext.getResources().getDimensionPixelSize(
                 R.dimen.recents_stack_top_padding);
 
-        // Compute the stack rect, inset from the given task stack bounds
-        mStackRect.set(taskStackBounds.left + widthPadding, taskStackBounds.top + heightPadding,
-                taskStackBounds.right - widthPadding, taskStackBounds.bottom);
+        // The freeform height is the visible height (not including system insets) - padding above
+        // freeform and below stack - gap between the freeform and stack
         mStackBottomOffset = mSystemInsets.bottom + heightPadding;
-
-        // Compute the task rect, align it to the top-center square in the stack rect
+        int ffHeight = (taskStackBounds.height() - 2 * heightPadding - mStackBottomOffset) / 2;
+        mFreeformRect.set(taskStackBounds.left + widthPadding,
+                taskStackBounds.top + heightPadding,
+                taskStackBounds.right - widthPadding,
+                taskStackBounds.top + heightPadding + ffHeight);
+        mFreeformStackRect.set(taskStackBounds.left + widthPadding,
+                taskStackBounds.top + heightPadding + ffHeight + heightPadding,
+                taskStackBounds.right - widthPadding,
+                taskStackBounds.bottom);
+        mStackRect.set(taskStackBounds.left + widthPadding,
+                taskStackBounds.top + heightPadding,
+                taskStackBounds.right - widthPadding,
+                taskStackBounds.bottom);
+        // Anchor the task rect to the top-center of the non-freeform stack rect
         int size = Math.min(mStackRect.width(), mStackRect.height() - mStackBottomOffset);
-        int xOffset = (mStackRect.width() - size) / 2;
-        mTaskRect.set(mStackRect.left + xOffset, mStackRect.top,
-                mStackRect.right - xOffset, mStackRect.top + size);
-
-        // Compute the freeform rect, align it to the top-left of the stack rect
-        mFreeformRect.set(mStackRect);
-        mFreeformRect.bottom = taskStackBounds.bottom - mStackBottomOffset;
+        mTaskRect.set(mStackRect.left, mStackRect.top,
+                mStackRect.left + size, mStackRect.top + size);
+        mCurrentStackRect = ssp.hasFreeformWorkspaceSupport() ? mFreeformStackRect : mStackRect;
 
         // Compute the progress offsets
         int withinAffiliationOffset = mContext.getResources().getDimensionPixelSize(
                 R.dimen.recents_task_bar_height);
         int betweenAffiliationOffset = (int) (VISIBLE_TASK_HEIGHT_BETWEEN_TASKS * mTaskRect.height());
         mWithinAffiliationPOffset = sCurve.computePOffsetForScaledHeight(withinAffiliationOffset,
-                mStackRect);
+                mCurrentStackRect);
         mBetweenAffiliationPOffset = sCurve.computePOffsetForScaledHeight(betweenAffiliationOffset,
-                mStackRect);
+                mCurrentStackRect);
         mTaskHeightPOffset = sCurve.computePOffsetForScaledHeight(mTaskRect.height(),
-                mStackRect);
+                mCurrentStackRect);
         mTaskHalfHeightPOffset = sCurve.computePOffsetForScaledHeight(mTaskRect.height() / 2,
-                mStackRect);
-        mStackBottomPOffset = sCurve.computePOffsetForHeight(mStackBottomOffset, mStackRect);
-        mFreeformWorkspaceGapOffset = mStackBottomOffset;
-        mFreeformWorkspaceGapPOffset = sCurve.computePOffsetForHeight(mFreeformWorkspaceGapOffset,
-                mStackRect);
-        mFreeformWorkspaceOffset = mFreeformWorkspaceGapOffset + mFreeformRect.height() +
-                mStackBottomOffset;
-        mFreeformWorkspacePOffset = sCurve.computePOffsetForHeight(mFreeformWorkspaceOffset,
-                mStackRect);
+                mCurrentStackRect);
+        mStackBottomPOffset = sCurve.computePOffsetForHeight(mStackBottomOffset, mCurrentStackRect);
 
         if (DEBUG) {
             Log.d(TAG, "initialize");
             Log.d(TAG, "\tarclength: " + sCurve.getArcLength());
+            Log.d(TAG, "\tmFreeformRect: " + mFreeformRect);
+            Log.d(TAG, "\tmFreeformStackRect: " + mFreeformStackRect);
             Log.d(TAG, "\tmStackRect: " + mStackRect);
             Log.d(TAG, "\tmTaskRect: " + mTaskRect);
             Log.d(TAG, "\tmSystemInsets: " + mSystemInsets);
@@ -246,20 +251,9 @@
             Log.d(TAG, "\tmTaskHeightPOffset: " + mTaskHeightPOffset);
             Log.d(TAG, "\tmTaskHalfHeightPOffset: " + mTaskHalfHeightPOffset);
             Log.d(TAG, "\tmStackBottomPOffset: " + mStackBottomPOffset);
-            Log.d(TAG, "\tmFreeformWorkspacePOffset: " + mFreeformWorkspacePOffset);
-            Log.d(TAG, "\tmFreeformWorkspaceGapPOffset: " + mFreeformWorkspaceGapPOffset);
 
-            Log.d(TAG, "\ty at p=0: " + sCurve.pToX(0f, mStackRect));
-            Log.d(TAG, "\ty at p=1: " + sCurve.pToX(1f, mStackRect));
-
-            for (int height = 0; height <= 2000; height += 50) {
-                float p = sCurve.computePOffsetForScaledHeight(height, mStackRect);
-                float p2 = sCurve.computePOffsetForHeight(height, mStackRect);
-                Log.d(TAG, "offset: " + height + ", " +
-                        p + " => " + (mStackRect.bottom - sCurve.pToX(1f - p, mStackRect)) /
-                                sCurve.pToScale(1f - p) + ", " +
-                        p2 + " => " + (mStackRect.bottom - sCurve.pToX(1f - p2, mStackRect)));
-            }
+            Log.d(TAG, "\ty at p=0: " + sCurve.pToX(0f, mCurrentStackRect));
+            Log.d(TAG, "\ty at p=1: " + sCurve.pToX(1f, mCurrentStackRect));
         }
     }
 
@@ -279,7 +273,7 @@
         ArrayList<Task> tasks = stack.getTasks();
         if (tasks.isEmpty()) {
             mFrontMostTaskP = 0;
-            mMinScrollP = mMaxScrollP = mStackEndScrollP = mPreferredStackEndScrollP = 0;
+            mMinScrollP = mMaxScrollP = 0;
             mNumStackTasks = mNumFreeformTasks = 0;
             return;
         }
@@ -307,6 +301,10 @@
                 Task task = stackTasks.get(i);
                 mTaskProgressMap.put(task.key, pAtFrontMostTaskTop);
 
+                if (DEBUG) {
+                    Log.d(TAG, "Update: " + task.activityLabel + " p: " + pAtFrontMostTaskTop);
+                }
+
                 if (i < (taskCount - 1)) {
                     // Increment the peek height
                     float pPeek = task.group.isFrontMostTask(task) ?
@@ -316,37 +314,24 @@
             }
 
             mFrontMostTaskP = pAtFrontMostTaskTop;
-            // Set the stack end scroll progress to the point at which the bottom of the front-most
-            // task is aligned to the bottom of the stack
-            mStackEndScrollP = alignToStackBottom(pAtFrontMostTaskTop, mTaskHeightPOffset);
             if (mNumStackTasks > 1) {
-                // Set the preferred stack end scroll progress to the point where the bottom of the
-                // front-most task is offset by the navbar and padding from the bottom of the stack
-                mPreferredStackEndScrollP = mStackEndScrollP + mStackBottomPOffset;
-
+                // Set the stack end scroll progress to the point at which the bottom of the front-most
+                // task is aligned to the bottom of the stack
+                mMaxScrollP = alignToStackBottom(pAtFrontMostTaskTop,
+                        mStackBottomPOffset + mTaskHeightPOffset);
                 // Basically align the back-most task such that the last two tasks would be visible
-                mMinScrollP = alignToStackBottom(pAtBackMostTaskTop, 2 *
-                        mBetweenAffiliationPOffset);
+                mMinScrollP = alignToStackBottom(pAtBackMostTaskTop,
+                        mStackBottomPOffset + mTaskHeightPOffset);
             } else {
                 // When there is a single item, then just make all the stack progresses the same
-                mPreferredStackEndScrollP = mStackEndScrollP;
-                mMinScrollP = mStackEndScrollP;
+                mMinScrollP = mMaxScrollP = 0;
             }
-        } else {
-            // TODO: In the case where there is only freeform tasks, then the scrolls should be
-            // set to zero
         }
 
         if (!freeformTasks.isEmpty()) {
-            // The max scroll includes the freeform workspace offset. As the scroll progress exceeds
-            // mStackEndScrollP up to mMaxScrollP, the stack will translate upwards and the freeform
-            // workspace will be visible
             mFreeformLayoutAlgorithm.update(freeformTasks, this);
-            mMaxScrollP = mStackEndScrollP + mFreeformWorkspacePOffset;
-            mInitialScrollP = isInitialStateFreeform(stack) ?
-                    mMaxScrollP : mPreferredStackEndScrollP;
+            mInitialScrollP = mMaxScrollP;
         } else {
-            mMaxScrollP = mPreferredStackEndScrollP;
             mInitialScrollP = Math.max(mMinScrollP, mMaxScrollP - mTaskHalfHeightPOffset);
         }
 
@@ -354,7 +339,6 @@
             Log.d(TAG, "mNumStackTasks: " + mNumStackTasks);
             Log.d(TAG, "mNumFreeformTasks: " + mNumFreeformTasks);
             Log.d(TAG, "mMinScrollP: " + mMinScrollP);
-            Log.d(TAG, "mStackEndScrollP: " + mStackEndScrollP);
             Log.d(TAG, "mMaxScrollP: " + mMaxScrollP);
         }
     }
@@ -369,28 +353,28 @@
             return new VisibilityReport(1, 1);
         }
 
-        // If there are freeform tasks, then they will be the only ones visible
-        int freeformTaskCount = 0;
-        for (Task t : tasks) {
-            if (t.isFreeformTask()) {
-                freeformTaskCount++;
-            }
-        }
-        if (freeformTaskCount > 0) {
-            return new VisibilityReport(freeformTaskCount, freeformTaskCount);
+        // Quick return when there are no stack tasks
+        if (mNumStackTasks == 0) {
+            return new VisibilityReport(Math.max(mNumFreeformTasks, 1),
+                    Math.max(mNumFreeformTasks, 1));
         }
 
         // Otherwise, walk backwards in the stack and count the number of tasks and visible
-        // thumbnails
+        // thumbnails and add that to the total freeform task count
         int taskHeight = mTaskRect.height();
         int taskBarHeight = mContext.getResources().getDimensionPixelSize(
                 R.dimen.recents_task_bar_height);
-        int numVisibleTasks = 1;
-        int numVisibleThumbnails = 1;
-        float progress = mTaskProgressMap.get(tasks.get(tasks.size() - 1).key) - mInitialScrollP;
-        int prevScreenY = sCurve.pToX(progress, mStackRect);
+        int numVisibleTasks = Math.max(mNumFreeformTasks, 1);
+        int numVisibleThumbnails = Math.max(mNumFreeformTasks, 1);
+        Task firstNonFreeformTask = tasks.get(tasks.size() - mNumFreeformTasks - 1);
+        float progress = mTaskProgressMap.get(firstNonFreeformTask.key) - mInitialScrollP;
+        int prevScreenY = sCurve.pToX(progress, mCurrentStackRect);
         for (int i = tasks.size() - 2; i >= 0; i--) {
             Task task = tasks.get(i);
+            if (task.isFreeformTask()) {
+                continue;
+            }
+
             progress = mTaskProgressMap.get(task.key) - mInitialScrollP;
             if (progress < 0) {
                 break;
@@ -399,7 +383,7 @@
             if (isFrontMostTaskInGroup) {
                 float scaleAtP = sCurve.pToScale(progress);
                 int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2);
-                int screenY = sCurve.pToX(progress, mStackRect) + scaleYOffsetAtP;
+                int screenY = sCurve.pToX(progress, mCurrentStackRect) + scaleYOffsetAtP;
                 boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight;
                 if (hasVisibleThumbnail) {
                     numVisibleThumbnails++;
@@ -431,17 +415,8 @@
      */
     public TaskViewTransform getStackTransform(Task task, float stackScroll,
             TaskViewTransform transformOut, TaskViewTransform prevTransform) {
-        if (mFreeformLayoutAlgorithm.isTransformAvailable(task, stackScroll, this)) {
-            mFreeformLayoutAlgorithm.getTransform(task, stackScroll, transformOut, this);
-            if (transformOut.visible) {
-                getFreeformWorkspaceBounds(stackScroll, mTmpTransform);
-                transformOut.translationY += mTmpTransform.translationY;
-                transformOut.translationZ = mMaxTranslationZ;
-                transformOut.rect.set(mTaskRect);
-                transformOut.rect.offset(0, transformOut.translationY);
-                Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
-                transformOut.p = 0;
-            }
+        if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) {
+            mFreeformLayoutAlgorithm.getTransform(task, transformOut, this);
             return transformOut;
         } else {
             // Return early if we have an invalid index
@@ -463,32 +438,25 @@
             // modulate some values directly
             float pTaskRelative = mMinScrollP - stackScroll;
             float scale = (mNumFreeformTasks > 0) ? 1f : SINGLE_TASK_SCALE;
-            int topOffset = (mStackRect.height() - mTaskRect.height()) / 2;
+            int topOffset = (mCurrentStackRect.top - mTaskRect.top) +
+                    (mCurrentStackRect.height() - mTaskRect.height()) / 2;
             transformOut.scale = scale;
-            transformOut.translationX = 0;
-            transformOut.translationY = (int) (topOffset + (pTaskRelative * mStackRect.height()));
+            transformOut.translationX = (mStackRect.width() - mTaskRect.width()) / 2;
+            transformOut.translationY = (int) (topOffset + (pTaskRelative * mCurrentStackRect.height()));
             transformOut.translationZ = mMaxTranslationZ;
             transformOut.rect.set(mTaskRect);
-            transformOut.rect.offset(0, transformOut.translationY);
+            transformOut.rect.offset(transformOut.translationX, transformOut.translationY);
             Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
             transformOut.visible = true;
             transformOut.p = pTaskRelative;
             return transformOut;
 
         } else {
-            // Once we scroll past the preferred stack end scroll, then we should start translating
-            // the cards in screen space and lock their final state at the end stack progress
-            int overscrollYOffset = 0;
-            if (mNumFreeformTasks > 0 && stackScroll > mStackEndScrollP) {
-                float stackOverscroll = (stackScroll - mPreferredStackEndScrollP) /
-                        (mFreeformWorkspacePOffset - mFreeformWorkspaceGapPOffset);
-               overscrollYOffset = (int) (Math.max(0, stackOverscroll) *
-                       (mFreeformWorkspaceOffset - mFreeformWorkspaceGapPOffset));
-                stackScroll = Math.min(mPreferredStackEndScrollP, stackScroll);
-            }
-
             float pTaskRelative = taskProgress - stackScroll;
             float pBounded = Math.max(0, Math.min(pTaskRelative, 1f));
+            if (DEBUG) {
+                Log.d(TAG, "getStackTransform (normal): " + taskProgress + ", " + stackScroll);
+            }
 
             // If the task top is outside of the bounds below the screen, then immediately reset it
             if (pTaskRelative > 1f) {
@@ -508,18 +476,18 @@
             float scale = sCurve.pToScale(pBounded);
             int scaleYOffset = (int) (((1f - scale) * mTaskRect.height()) / 2);
             transformOut.scale = scale;
-            transformOut.translationX = 0;
-            transformOut.translationY = sCurve.pToX(pBounded, mStackRect) - mStackRect.top -
-                    scaleYOffset - overscrollYOffset;
+            transformOut.translationX = (mStackRect.width() - mTaskRect.width()) / 2;
+            transformOut.translationY = (mCurrentStackRect.top - mTaskRect.top) +
+                    (sCurve.pToX(pBounded, mCurrentStackRect) - mCurrentStackRect.top) -
+                    scaleYOffset;
             transformOut.translationZ = Math.max(mMinTranslationZ,
                     mMinTranslationZ + (pBounded * (mMaxTranslationZ - mMinTranslationZ)));
             transformOut.rect.set(mTaskRect);
-            transformOut.rect.offset(0, transformOut.translationY);
+            transformOut.rect.offset(transformOut.translationX, transformOut.translationY);
             Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
             transformOut.visible = true;
             transformOut.p = pTaskRelative;
             if (DEBUG) {
-                Log.d(TAG, "getStackTransform (normal): " + taskProgress + ", " + stackScroll);
                 Log.d(TAG, "\t" + transformOut);
             }
 
@@ -528,61 +496,6 @@
     }
 
     /**
-     * Returns whether this stack should be initialized to show the freeform workspace or not.
-     */
-    public boolean isInitialStateFreeform(TaskStack stack) {
-        Task launchTarget = stack.getLaunchTarget();
-        if (launchTarget != null) {
-            return launchTarget.isFreeformTask();
-        }
-        Task frontTask = stack.getFrontMostTask();
-        if (frontTask != null) {
-            return frontTask.isFreeformTask();
-        }
-        return false;
-    }
-
-    /**
-     * Update/get the transform
-     */
-    public TaskViewTransform getFreeformWorkspaceBounds(float stackScroll,
-            TaskViewTransform transformOut) {
-        transformOut.reset();
-        if (mNumFreeformTasks == 0) {
-            return transformOut;
-        }
-
-        if (stackScroll > mStackEndScrollP) {
-            // mStackEndScroll is the point at which the first stack task is bottom aligned with the
-            // stack, so we offset from on the stack rect height.
-            float stackOverscroll = (Math.max(0, stackScroll - mStackEndScrollP)) /
-                    mFreeformWorkspacePOffset;
-            int overscrollYOffset = (int) (stackOverscroll * mFreeformWorkspaceOffset);
-            transformOut.scale = 1f;
-            transformOut.alpha = 1f;
-            transformOut.translationY = mStackRect.height() + mFreeformWorkspaceGapOffset -
-                    overscrollYOffset;
-            transformOut.rect.set(mFreeformRect);
-            transformOut.rect.offset(0, transformOut.translationY);
-            Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
-            transformOut.visible = true;
-        }
-        return transformOut;
-    }
-
-    /**
-     * Returns the preferred maximum scroll position for a stack at the given {@param scroll}.
-     */
-    public float getPreferredMaxScrollPosition(float scroll) {
-        float maxStackScrollBounds = mStackEndScrollP + SNAP_TO_MAX_STACK_SCROLL_FACTOR *
-                (mMaxScrollP - mStackEndScrollP);
-        if (scroll < maxStackScrollBounds) {
-            return mPreferredStackEndScrollP;
-        }
-        return mMaxScrollP;
-    }
-
-    /**
      * Returns the untransformed task view bounds.
      */
     public Rect getUntransformedTaskViewBounds() {
@@ -604,7 +517,7 @@
      * screen along the arc-length proportionally (1/arclength).
      */
     public float getDeltaPForY(int downY, int y) {
-        float deltaP = (float) (y - downY) / mStackRect.height() * (1f / sCurve.getArcLength());
+        float deltaP = (float) (y - downY) / mCurrentStackRect.height() * (1f / sCurve.getArcLength());
         return -deltaP;
     }
 
@@ -613,7 +526,7 @@
      * of the curve, map back to the screen y.
      */
     public int getYForDeltaP(float downScrollP, float p) {
-        int y = (int) ((p - downScrollP) * mStackRect.height() * sCurve.getArcLength());
+        int y = (int) ((p - downScrollP) * mCurrentStackRect.height() * sCurve.getArcLength());
         return -y;
     }
 
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 e7f07f7..14d75a8 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -281,6 +281,7 @@
 
     /**
      * Gets the stack transforms of a list of tasks, and returns the visible range of tasks.
+     * This call ignores freeform tasks.
      */
     private boolean updateStackTransforms(ArrayList<TaskViewTransform> taskTransforms,
                                        ArrayList<Task> tasks,
@@ -306,8 +307,16 @@
         // Update the stack transforms
         TaskViewTransform prevTransform = null;
         for (int i = taskCount - 1; i >= 0; i--) {
-            TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(tasks.get(i),
-                    stackScroll, taskTransforms.get(i), prevTransform);
+            Task task = tasks.get(i);
+            if (task.isFreeformTask()) {
+                continue;
+            }
+
+            TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(task, stackScroll,
+                    taskTransforms.get(i), prevTransform);
+            if (DEBUG) {
+                Log.d(TAG, "updateStackTransform: " + i + ", " + transform.visible);
+            }
             if (transform.visible) {
                 if (frontMostVisibleIndex < 0) {
                     frontMostVisibleIndex = i;
@@ -327,7 +336,7 @@
 
             if (boundTranslationsToRect) {
                 transform.translationY = Math.min(transform.translationY,
-                        mLayoutAlgorithm.mStackRect.bottom);
+                        mLayoutAlgorithm.mCurrentStackRect.bottom);
             }
             prevTransform = transform;
         }
@@ -344,13 +353,13 @@
             // Get all the task transforms
             ArrayList<Task> tasks = mStack.getTasks();
             float stackScroll = mStackScroller.getStackScroll();
-            int[] visibleRange = mTmpVisibleRange;
-            boolean isValidVisibleRange = updateStackTransforms(mCurrentTaskTransforms, tasks,
-                    stackScroll, visibleRange, false);
+            int[] visibleStackRange = mTmpVisibleRange;
+            boolean isValidVisibleStackRange = updateStackTransforms(mCurrentTaskTransforms, tasks,
+                    stackScroll, visibleStackRange, false);
             boolean hasStackBackTransform = false;
             boolean hasStackFrontTransform = false;
             if (DEBUG) {
-                Log.d(TAG, "visibleRange: " + visibleRange[0] + " to " + visibleRange[1]);
+                Log.d(TAG, "visibleRange: " + visibleStackRange[0] + " to " + visibleStackRange[1]);
             }
 
             // Return all the invisible children to the pool
@@ -363,7 +372,8 @@
                 TaskView tv = taskViews.get(i);
                 Task task = tv.getTask();
                 int taskIndex = mStack.indexOfTask(task);
-                if (visibleRange[1] <= taskIndex && taskIndex <= visibleRange[0]) {
+                if (task.isFreeformTask() ||
+                        visibleStackRange[1] <= taskIndex && taskIndex <= visibleStackRange[0]) {
                     mTmpTaskViewMap.put(task, tv);
                 } else {
                     if (tv.isFocusedTask()) {
@@ -371,16 +381,43 @@
                         lastFocusedTaskIndex = taskIndex;
                         resetFocusedTask();
                     }
+                    if (DEBUG) {
+                        Log.d(TAG, "returning to pool: " + task.key);
+                    }
                     mViewPool.returnViewToPool(tv);
                 }
             }
 
+            // Pick up all the freeform tasks
+            int firstVisStackIndex = isValidVisibleStackRange ? visibleStackRange[0] : 0;
+            for (int i = mStack.getTaskCount() - 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);
+                    if (mLayersDisabled) {
+                        tv.disableLayersForOneFrame();
+                    }
+                }
+
+                // Animate the task into place
+                tv.updateViewPropertiesToTaskTransform(transform,
+                        mStackViewsAnimationDuration, mRequestUpdateClippingListener);
+            }
+
             // Pick up all the newly visible children and update all the existing children
-            for (int i = visibleRange[0]; isValidVisibleRange && i >= visibleRange[1]; i--) {
+            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);
-                int taskIndex = mStack.indexOfTask(task);
 
                 if (tv == null) {
                     tv = mViewPool.pickUpViewFromPool(task, task);
@@ -409,29 +446,19 @@
                 }
 
                 // Animate the task into place
-                tv.updateViewPropertiesToTaskTransform(mCurrentTaskTransforms.get(taskIndex),
+                tv.updateViewPropertiesToTaskTransform(transform,
                         mStackViewsAnimationDuration, mRequestUpdateClippingListener);
             }
 
             // Update the focus if the previous focused task was returned to the view pool
             if (lastFocusedTaskIndex != -1) {
-                if (lastFocusedTaskIndex < visibleRange[1]) {
-                    setFocusedTask(visibleRange[1], false, wasLastFocusedTaskAnimated);
+                if (lastFocusedTaskIndex < visibleStackRange[1]) {
+                    setFocusedTask(visibleStackRange[1], false, wasLastFocusedTaskAnimated);
                 } else {
-                    setFocusedTask(visibleRange[0], false, wasLastFocusedTaskAnimated);
+                    setFocusedTask(visibleStackRange[0], false, wasLastFocusedTaskAnimated);
                 }
             }
 
-            // Update the freeform workspace
-            mLayoutAlgorithm.getFreeformWorkspaceBounds(stackScroll, mTmpTransform);
-            if (mTmpTransform.visible) {
-                mTmpTransform.rect.roundOut(mTmpRect);
-                mFreeformWorkspaceBackground.setAlpha(255);
-                mFreeformWorkspaceBackground.setBounds(mTmpRect);
-            } else {
-                mFreeformWorkspaceBackground.setAlpha(0);
-            }
-
             // Reset the request-synchronize params
             mStackViewsAnimationDuration = 0;
             mStackViewsDirty = false;
@@ -491,6 +518,14 @@
         // Compute the min and max scroll values
         mLayoutAlgorithm.update(mStack);
 
+        // Update the freeform workspace
+        SystemServicesProxy ssp = Recents.getSystemServices();
+        if (ssp.hasFreeformWorkspaceSupport()) {
+            mTmpRect.set(mLayoutAlgorithm.mFreeformRect);
+            mFreeformWorkspaceBackground.setAlpha(255);
+            mFreeformWorkspaceBackground.setBounds(mTmpRect);
+        }
+
         // Debug logging
         if (boundScrollToNewMinMax) {
             mStackScroller.boundScroll();
@@ -762,7 +797,7 @@
 
     /** Handler for the first layout. */
     void onFirstLayout() {
-        int offscreenY = mLayoutAlgorithm.mStackRect.bottom;
+        int offscreenY = mLayoutAlgorithm.mCurrentStackRect.bottom;
 
         // Find the launch target task
         Task launchTargetTask = mStack.getLaunchTarget();
@@ -863,7 +898,7 @@
         mStackScroller.stopScroller();
         mStackScroller.stopBoundScrollAnimation();
         // Animate all the task views out of view
-        ctx.offscreenTranslationY = mLayoutAlgorithm.mStackRect.bottom;
+        ctx.offscreenTranslationY = mLayoutAlgorithm.mCurrentStackRect.bottom;
 
         List<TaskView> taskViews = getTaskViews();
         int taskViewCount = taskViews.size();
@@ -929,6 +964,18 @@
         }
     }
 
+    /**
+     * Launches the freeform tasks.
+     */
+    public boolean launchFreeformTasks() {
+        Task frontTask = mStack.getFrontMostTask();
+        if (frontTask != null && frontTask.isFreeformTask()) {
+            onTaskViewClicked(getChildViewForTask(frontTask), frontTask, false);
+            return true;
+        }
+        return false;
+    }
+
     /**** TaskStackCallbacks Implementation ****/
 
     @Override
@@ -975,6 +1022,19 @@
 
             // Animate all the tasks into place
             requestSynchronizeStackViewsWithModel(200);
+        } 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
+            TaskView tv = getChildViewForTask(removedTask);
+            if (tv != null) {
+                mViewPool.returnViewToPool(tv);
+            }
+
+            // Update the min/max scroll and animate other task views into their new positions
+            updateMinMaxScroll(true);
+
+            // Animate all the tasks into place
+            requestSynchronizeStackViewsWithModel(200);
         }
 
         // Update the new front most task
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 6b92aed..3a2ed0f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
@@ -122,7 +122,7 @@
     /** Returns the bounded stack scroll */
     float getBoundedStackScroll(float scroll) {
         return Math.max(mLayoutAlgorithm.mMinScrollP,
-                Math.min(mLayoutAlgorithm.getPreferredMaxScrollPosition(scroll), scroll));
+                Math.min(mLayoutAlgorithm.mMaxScrollP, scroll));
     }
 
     /** Returns the amount that the absolute value of how much the scroll is out of bounds. */
@@ -194,7 +194,7 @@
     // TODO: Remove
     @Deprecated
     int progressToScrollRange(float p) {
-        return (int) (p * mLayoutAlgorithm.mStackRect.height());
+        return (int) (p * mLayoutAlgorithm.mCurrentStackRect.height());
     }
 
     /** Called from the view draw, computes the next scroll. */
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 9e6fb7b..59c9708 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -19,6 +19,7 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Rect;
 import android.util.Log;
 import android.view.InputDevice;
 import android.view.MotionEvent;
@@ -29,9 +30,11 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.systemui.R;
 import com.android.systemui.recents.Constants;
+import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.events.EventBus;
 import com.android.systemui.recents.events.activity.HideRecentsEvent;
 import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
+import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.statusbar.FlingAnimationUtils;
 
@@ -222,49 +225,12 @@
                 int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                 int y = (int) ev.getY(activePointerIndex);
                 int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
-                float curScrollP = mScroller.getStackScroll();
                 if (mIsScrolling) {
-                    boolean hasFreeformTasks = mSv.mStack.hasFreeformTasks();
-                    if (hasFreeformTasks && velocity > 0 &&
-                            curScrollP > layoutAlgorithm.mStackEndScrollP) {
-                        // Snap to workspace
-                        float finalY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
-                                layoutAlgorithm.mPreferredStackEndScrollP);
-                        mScrollFlingAnimator = ValueAnimator.ofInt(y, (int) finalY);
-                        mScrollFlingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-                            @Override
-                            public void onAnimationUpdate(ValueAnimator animation) {
-                                float deltaP = layoutAlgorithm.getDeltaPForY(mDownY,
-                                        (Integer) animation.getAnimatedValue());
-                                float scroll = mDownScrollP + deltaP;
-                                mScroller.setStackScroll(scroll);
-                            }
-                        });
-                        mFlingAnimUtils.apply(mScrollFlingAnimator, y, finalY, velocity);
-                        mScrollFlingAnimator.start();
-                    } else if (hasFreeformTasks && velocity < 0 &&
-                            curScrollP > (layoutAlgorithm.mStackEndScrollP -
-                                    layoutAlgorithm.mTaskHalfHeightPOffset)) {
-                        // Snap to stack
-                        float finalY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
-                                layoutAlgorithm.mMaxScrollP);
-                        mScrollFlingAnimator = ValueAnimator.ofInt(y, (int) finalY);
-                        mScrollFlingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
-                            @Override
-                            public void onAnimationUpdate(ValueAnimator animation) {
-                                float deltaP = layoutAlgorithm.getDeltaPForY(mDownY,
-                                        (Integer) animation.getAnimatedValue());
-                                float scroll = mDownScrollP + deltaP;
-                                mScroller.setStackScroll(scroll);
-                            }
-                        });
-                        mFlingAnimUtils.apply(mScrollFlingAnimator, y, finalY, velocity);
-                        mScrollFlingAnimator.start();
-                    } else if (mScroller.isScrollOutOfBounds()) {
+                    if (mScroller.isScrollOutOfBounds()) {
                         mScroller.animateBoundScroll();
                     } else if (Math.abs(velocity) > mMinimumVelocity) {
                         float minY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
-                                layoutAlgorithm.mPreferredStackEndScrollP);
+                                layoutAlgorithm.mMaxScrollP);
                         float maxY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
                                 layoutAlgorithm.mMinScrollP);
                         mScroller.fling(mDownScrollP, mDownY, y, velocity, (int) minY, (int) maxY,
@@ -313,6 +279,17 @@
             return;
         }
 
+        // If tapping on the freeform workspace background, just launch the first freeform task
+        SystemServicesProxy ssp = Recents.getSystemServices();
+        if (ssp.hasFreeformWorkspaceSupport()) {
+            Rect freeformRect = mSv.mLayoutAlgorithm.mFreeformRect;
+            if (freeformRect.top <= y && y <= freeformRect.bottom) {
+                if (mSv.launchFreeformTasks()) {
+                    return;
+                }
+            }
+        }
+
         // The user intentionally tapped on the background, which is like a tap on the "desktop".
         // Hide recents and transition to the launcher.
         EventBus.getDefault().send(new HideRecentsEvent(false, true));