Merge "Fixing touch handling on resized tasks."
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 00f484d..a4137b9 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -232,6 +232,9 @@
     <!-- The size of the lock-to-app button icon. -->
     <dimen name="recents_lock_to_app_icon_size">28dp</dimen>
 
+    <!-- The amount to allow the stack to overscroll. -->
+    <dimen name="recents_stack_overscroll">24dp</dimen>
+
     <!-- Space reserved for the cards behind the top card in the top stack -->
     <dimen name="top_stack_peek_amount">12dp</dimen>
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Constants.java b/packages/SystemUI/src/com/android/systemui/recents/Constants.java
index 6668df9..ebfacac 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Constants.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Constants.java
@@ -64,8 +64,6 @@
         }
 
         public static class TaskStackView {
-            public static final int TaskStackMinOverscrollRange = 32;
-            public static final int TaskStackMaxOverscrollRange = 128;
             public static final int FilterStartDelay = 25;
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
index b05e1fe..23836ab 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsImpl.java
@@ -469,7 +469,7 @@
         TaskStackViewLayoutAlgorithm algo = mDummyStackView.getStackAlgorithm();
         Rect taskStackBounds = new Rect(mTaskStackBounds);
         algo.setSystemInsets(systemInsets);
-        algo.computeRects(windowRect.width(), windowRect.height(), taskStackBounds);
+        algo.computeRects(taskStackBounds);
         Rect taskViewBounds = algo.getUntransformedTaskViewBounds();
         if (!taskViewBounds.equals(mLastTaskViewBounds)) {
             mLastTaskViewBounds.set(taskViewBounds);
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 8e85bfc..ea6821f 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java
@@ -48,6 +48,7 @@
 
     float[] xp;
     float[] px;
+    float mLength;
 
     CurveFunction mFn;
     ParametricCurveFunction mScaleFn;
@@ -79,6 +80,7 @@
             dx[xStep] = (float) Math.hypot(fx[xStep] - fx[xStep - 1], step);
             pLength += dx[xStep];
         }
+        mLength = pLength;
         // Approximate p(x), a function of cumulative progress with x, normalized to 0..1
         float p = 0;
         px[0] = 0f;
@@ -260,4 +262,11 @@
         }
         return 1f - xToP(maxX, bounds);
     }
+
+    /**
+     * Returns the length of this curve.
+     */
+    public float getArcLength() {
+        return mLength;
+    }
 }
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 0793180..60051b8 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
@@ -170,6 +170,12 @@
         }
     }
 
+    public boolean isFreeformTask() {
+        // Temporarily disable:
+        return false;
+        // return SystemServicesProxy.isFreeformStack(key.stackId);
+    }
+
     /** Notifies the callback listeners that this task has been loaded */
     public void notifyTaskDataLoaded(Bitmap thumbnail, Drawable applicationIcon) {
         this.applicationIcon = applicationIcon;
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java
index e04699c1..b7c1de3 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/SwipeHelper.java
@@ -68,6 +68,7 @@
 
     private float mInitialTouchPos;
     private boolean mDragging;
+    private float mSnapBackTranslationX;
 
     private View mCurrView;
     private boolean mCanCurrViewBeDimissed;
@@ -92,6 +93,10 @@
         mDensityScale = densityScale;
     }
 
+    public void setSnapBackTranslationX(float translationX) {
+        mSnapBackTranslationX = translationX;
+    }
+
     public void setPagingTouchSlop(float pagingTouchSlop) {
         mPagingTouchSlop = pagingTouchSlop;
     }
@@ -267,7 +272,7 @@
 
     private void snapChild(final View view, float velocity) {
         final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view);
-        ValueAnimator anim = createTranslationAnimation(view, 0);
+        ValueAnimator anim = createTranslationAnimation(view, mSnapBackTranslationX);
         int duration = SNAP_ANIM_LEN;
         anim.setDuration(duration);
         anim.setInterpolator(mLinearOutSlowInInterpolator);
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 757e695..21ceb33 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -124,7 +124,7 @@
         mConfig = RecentsConfiguration.getInstance();
         mViewPool = new ViewPool<>(context, this);
         mInflater = LayoutInflater.from(context);
-        mLayoutAlgorithm = new TaskStackViewLayoutAlgorithm(context, mConfig);
+        mLayoutAlgorithm = new TaskStackViewLayoutAlgorithm(context);
         mFilterAlgorithm = new TaskStackViewFilterAlgorithm(this, mViewPool);
         mStackScroller = new TaskStackViewScroller(context, mLayoutAlgorithm);
         mStackScroller.setCallbacks(this);
@@ -633,10 +633,9 @@
     }
 
     /** Computes the stack and task rects */
-    public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds,
-                             boolean launchedWithAltTab, boolean launchedFromHome) {
+    public void computeRects(Rect taskStackBounds) {
         // Compute the rects in the stack algorithm
-        mLayoutAlgorithm.computeRects(windowWidth, windowHeight, taskStackBounds);
+        mLayoutAlgorithm.computeRects(taskStackBounds);
 
         // Update the scroll bounds
         updateMinMaxScroll(false);
@@ -674,9 +673,7 @@
         int height = MeasureSpec.getSize(heightMeasureSpec);
 
         // Compute our stack/task rects
-        RecentsActivityLaunchState launchState = mConfig.getLaunchState();
-        computeRects(width, height, mTaskStackBounds, launchState.launchedWithAltTab,
-                launchState.launchedFromHome);
+        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
diff --git a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
index 9a5d9bd..d81f3f6 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewLayoutAlgorithm.java
@@ -21,6 +21,7 @@
 import android.graphics.Rect;
 import android.util.Log;
 import com.android.systemui.R;
+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.Utilities;
@@ -32,14 +33,15 @@
 
 /**
  * The layout logic for a TaskStackView.
+ *
  */
 public class TaskStackViewLayoutAlgorithm {
 
-    private static final boolean DEBUG = false;
     private static final String TAG = "TaskStackViewLayoutAlgorithm";
+    private static final boolean DEBUG = false;
 
     // The min scale of the last task at the top of the curve
-    private static final float STACK_PEEK_MIN_SCALE = 0.75f;
+    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 height of task to show between tasks
@@ -58,14 +60,17 @@
     }
 
     Context mContext;
-    RecentsConfiguration mConfig;
 
     // 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
     public Rect mTaskRect = new Rect();
+
+    // The bounds of the freeform workspace, the rect is top-aligned to the top of the stack rect
+    public Rect mFreeformRect = new Rect();
     // This is the current system insets
     public Rect mSystemInsets = new Rect();
 
@@ -74,8 +79,13 @@
     // 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 the stack scroll ends and the overscroll begins.  This serves
+    // as the point at which we can show the freeform space.
+    float mMaxStackScrollP;
     // The initial progress that the scroller is set
     float mInitialScrollP;
+    // The task progress for the front-most task in the stack
+    float mFrontMostTaskP;
 
     // The relative progress to ensure that the height between affiliated tasks is respected
     float mWithinAffiliationPOffset;
@@ -89,11 +99,21 @@
     // The relative progress to ensure that the offset from the bottom of the stack to the bottom
     // of the task is respected
     float mTaskBottomPOffset;
+    // The relative progress to ensure that the freeform workspace height is respected
+    float mFreeformWorkspacePOffset;
     // The front-most task bottom offset
     int mTaskBottomOffset;
 
-    // The last computed task count
-    int mNumTasks;
+    // The number of cells in the freeform workspace
+    int mFreeformCellXCount;
+    int mFreeformCellYCount;
+    // The width and height of the cells in the freeform workspace
+    int mFreeformCellWidth;
+    int mFreeformCellHeight;
+
+    // The last computed task counts
+    int mNumStackTasks;
+    int mNumFreeformTasks;
     // The min/max z translations
     int mMinTranslationZ;
     int mMaxTranslationZ;
@@ -104,12 +124,11 @@
     // Log function
     static ParametricCurve sCurve;
 
-    public TaskStackViewLayoutAlgorithm(Context context, RecentsConfiguration config) {
+    public TaskStackViewLayoutAlgorithm(Context context) {
         Resources res = context.getResources();
         mMinTranslationZ = res.getDimensionPixelSize(R.dimen.recents_task_view_z_min);
         mMaxTranslationZ = res.getDimensionPixelSize(R.dimen.recents_task_view_z_max);
         mContext = context;
-        mConfig = config;
         if (sCurve == null) {
             sCurve = new ParametricCurve(new ParametricCurve.CurveFunction() {
                 // The large the XScale, the longer the flat area of the curve
@@ -153,23 +172,25 @@
     }
 
     /**
-     * Computes the stack and task rects
+     * Computes the stack and task rects.
      */
-    public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds) {
-        int widthPadding = (int) (mConfig.taskStackWidthPaddingPct * taskStackBounds.width());
+    public void computeRects(Rect taskStackBounds) {
+        RecentsConfiguration config = RecentsConfiguration.getInstance();
+        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, windowHeight);
+                taskStackBounds.right - widthPadding, taskStackBounds.bottom);
         mTaskBottomOffset = mSystemInsets.bottom + heightPadding;
 
         // Compute the task rect, align it to the top-center square in the stack rect
-        int size = Math.min(mStackRect.width(), taskStackBounds.height() - mTaskBottomOffset);
+        int size = Math.min(mStackRect.width(), mStackRect.height() - mTaskBottomOffset);
         int xOffset = (mStackRect.width() - size) / 2;
         mTaskRect.set(mStackRect.left + xOffset, mStackRect.top,
                 mStackRect.right - xOffset, mStackRect.top + size);
+        mFreeformRect.set(mTaskRect);
 
         // Compute the progress offsets
         int withinAffiliationOffset = mContext.getResources().getDimensionPixelSize(
@@ -184,9 +205,12 @@
         mTaskHalfHeightPOffset = sCurve.computePOffsetForScaledHeight(mTaskRect.height() / 2,
                 mStackRect);
         mTaskBottomPOffset = sCurve.computePOffsetForHeight(mTaskBottomOffset, mStackRect);
+        mFreeformWorkspacePOffset = sCurve.computePOffsetForHeight(mFreeformRect.height(),
+                mStackRect);
 
         if (DEBUG) {
             Log.d(TAG, "computeRects");
+            Log.d(TAG, "\tarclength: " + sCurve.getArcLength());
             Log.d(TAG, "\tmStackRect: " + mStackRect);
             Log.d(TAG, "\tmTaskRect: " + mTaskRect);
             Log.d(TAG, "\tmSystemInsets: " + mSystemInsets);
@@ -222,29 +246,38 @@
 
         // Clear the progress map
         mTaskProgressMap.clear();
-        mNumTasks = tasks.size();
 
         // Return early if we have no tasks
         if (tasks.isEmpty()) {
-            mMinScrollP = mMaxScrollP = 0;
+            mMinScrollP = mMaxScrollP = mMaxStackScrollP = 0;
+            mNumStackTasks = mNumFreeformTasks = 0;
             return;
         }
 
-        // We calculate the progress by taking the progress of the element from the bottom of the
-        // screen
-        if (mNumTasks == 1) {
-            // Just center the task in the visible stack rect
-            mMinScrollP = mMaxScrollP = mInitialScrollP = 0f;
-            mTaskProgressMap.put(tasks.get(0).key, 0f);
-        } else {
-            // Update the tasks from back to front with the new progresses. We set the initial
-            // progress to the progress at which the top of the last task is near the center of the
-            // visible stack rect.
+        // Filter the set of freeform and stack tasks
+        ArrayList<Task> freeformTasks = new ArrayList<>();
+        ArrayList<Task> stackTasks = new ArrayList<>();
+        for (int i = 0; i < tasks.size(); i++) {
+            Task task = tasks.get(i);
+            if (task.isFreeformTask()) {
+                freeformTasks.add(task);
+            } else {
+                stackTasks.add(task);
+            }
+        }
+        mNumStackTasks = stackTasks.size();
+        mNumFreeformTasks = freeformTasks.size();
+
+        // TODO: In the case where there is only freeform tasks, then the scrolls should be set to
+        // zero
+
+        if (!stackTasks.isEmpty()) {
+            // Update the for each task from back to front.
             float pAtBackMostTaskTop = 0;
             float pAtFrontMostTaskTop = pAtBackMostTaskTop;
-            int taskCount = tasks.size();
+            int taskCount = stackTasks.size();
             for (int i = 0; i < taskCount; i++) {
-                Task task = tasks.get(i);
+                Task task = stackTasks.get(i);
                 mTaskProgressMap.put(task.key, pAtFrontMostTaskTop);
 
                 if (i < (taskCount - 1)) {
@@ -255,15 +288,47 @@
                 }
             }
 
-            // Set the max scroll progress to the point at which the bottom of the front-most task
-            // is aligned to the bottom of the stack (including nav bar and stack padding)
-            mMaxScrollP = pAtFrontMostTaskTop - 1f + mTaskBottomPOffset + mTaskHeightPOffset;
+            mFrontMostTaskP = pAtFrontMostTaskTop;
+            // Set the max scroll progress to the point at which the top of the front-most task
+            // is aligned to the bottom of the stack (offset by nav bar, padding, and task height)
+            mMaxStackScrollP = getBottomAlignedScrollProgress(pAtFrontMostTaskTop,
+                    mTaskBottomPOffset + mTaskHeightPOffset);
             // Basically align the back-most task such that its progress is the same as the top of
-            // the front most task at the max scroll
-            mMinScrollP = pAtBackMostTaskTop - 1f + mTaskBottomPOffset + mTaskHeightPOffset;
-            // The offset the inital scroll position to the front of the stack, with half the front
-            // task height visible
-            mInitialScrollP = Math.max(mMinScrollP, mMaxScrollP - mTaskHalfHeightPOffset);
+            // the front most task at the max stack scroll
+            mMinScrollP = getBottomAlignedScrollProgress(pAtBackMostTaskTop,
+                    mTaskBottomPOffset + mTaskHeightPOffset);
+        }
+
+        if (!freeformTasks.isEmpty()) {
+            // Calculate the cell width/height depending on the number of freeform tasks
+            mFreeformCellXCount = Math.max(2, (int) Math.ceil(Math.sqrt(mNumFreeformTasks)));
+            mFreeformCellYCount = Math.max(2, (int) Math.ceil((float) mNumFreeformTasks / mFreeformCellXCount));
+            mFreeformCellWidth = mFreeformRect.width() / mFreeformCellXCount;
+            mFreeformCellHeight = mFreeformRect.height() / mFreeformCellYCount;
+
+            // 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++) {
+                Task task = freeformTasks.get(i);
+                mTaskProgressMap.put(task.key, (mFrontMostTaskP + 1) + i);
+            }
+
+            // The max scroll includes the freeform workspace offset. As the scroll progress exceeds
+            // mMaxStackScrollP up to mMaxScrollP, the stack will translate upwards and the freeform
+            // workspace will be visible
+            mMaxScrollP = mMaxStackScrollP + mFreeformWorkspacePOffset;
+            mInitialScrollP = mMaxScrollP;
+        } else {
+            mMaxScrollP = mMaxStackScrollP;
+            mInitialScrollP = Math.max(mMinScrollP, mMaxStackScrollP - mTaskHalfHeightPOffset);
+        }
+        if (DEBUG) {
+            Log.d(TAG, "mNumStackTasks: " + mNumStackTasks);
+            Log.d(TAG, "mNumFreeformTasks: " + mNumFreeformTasks);
+            Log.d(TAG, "mMinScrollP: " + mMinScrollP);
+            Log.d(TAG, "mMaxStackScrollP: " + mMaxStackScrollP);
+            Log.d(TAG, "mMaxScrollP: " + mMaxScrollP);
         }
     }
 
@@ -272,11 +337,24 @@
      * computeMinMaxScroll() is called first.
      */
     public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) {
+        // Ensure minimum visibility count
         if (tasks.size() <= 1) {
             return new VisibilityReport(1, 1);
         }
 
-        // Walk backwards in the task stack and count the number of tasks and visible thumbnails
+        // 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);
+        }
+
+        // Otherwise, walk backwards in the stack and count the number of tasks and visible
+        // thumbnails
         int taskHeight = mTaskRect.height();
         int taskBarHeight = mContext.getResources().getDimensionPixelSize(
                 R.dimen.recents_task_bar_height);
@@ -338,18 +416,38 @@
     /** Update/get the transform */
     public TaskViewTransform getStackTransform(float taskProgress, float stackScroll,
             TaskViewTransform transformOut, TaskViewTransform prevTransform) {
-        if (DEBUG) {
-            Log.d(TAG, "getStackTransform: " + stackScroll);
+        float stackOverscroll = (stackScroll - mMaxStackScrollP) / mFreeformWorkspacePOffset;
+        int overscrollYOffset = 0;
+        if (mNumFreeformTasks > 0) {
+            overscrollYOffset = (int) (Math.max(0, stackOverscroll) * mFreeformRect.height());
         }
 
-        if (mNumTasks == 1) {
+        if ((mNumFreeformTasks > 0) && (stackScroll > mMaxStackScrollP) &&
+                (taskProgress > mFrontMostTaskP)) {
+            // This is a freeform task, so lay it out in the freeform workspace
+            int taskIndex = Math.round(taskProgress - (mFrontMostTaskP + 1));
+            int x = taskIndex % mFreeformCellXCount;
+            int y = taskIndex / mFreeformCellXCount;
+            int frontTaskBottom = mStackRect.height() - mTaskBottomOffset;
+            float scale = (float) mFreeformCellWidth / mTaskRect.width();
+            int scaleXOffset = (int) (((1f - scale) * mTaskRect.width()) / 2);
+            int scaleYOffset = (int) (((1f - scale) * mTaskRect.height()) / 2);
+            transformOut.scale = scale;
+            transformOut.translationX = x * mFreeformCellWidth - scaleXOffset;
+            transformOut.translationY = frontTaskBottom - overscrollYOffset +
+                    (y * mFreeformCellHeight) - scaleYOffset;
+            transformOut.visible = true;
+            return transformOut;
+
+        } else if (mNumStackTasks == 1) {
             // Center the task in the stack, changing the scale will not follow the curve, but just
             // modulate some values directly
-            float pTaskRelative = -stackScroll;
+            float pTaskRelative = mMinScrollP - stackScroll;
             float scale = SINGLE_TASK_SCALE;
             int topOffset = (mStackRect.height() - mTaskBottomOffset - mTaskRect.height()) / 2;
             transformOut.scale = scale;
-            transformOut.translationY = (int) (topOffset + (pTaskRelative * mStackRect.height()));
+            transformOut.translationY = (int) (topOffset + (pTaskRelative * mStackRect.height())) -
+                    overscrollYOffset;
             transformOut.translationZ = mMaxTranslationZ;
             transformOut.rect.set(mTaskRect);
             transformOut.rect.offset(0, transformOut.translationY);
@@ -357,8 +455,12 @@
             transformOut.visible = true;
             transformOut.p = pTaskRelative;
             return transformOut;
+
         } else {
             float pTaskRelative = taskProgress - stackScroll;
+            if (mNumFreeformTasks > 0) {
+                pTaskRelative = Math.min(mMaxStackScrollP, pTaskRelative);
+            }
             float pBounded = Math.max(0, Math.min(pTaskRelative, 1f));
             // If the task top is outside of the bounds below the screen, then immediately reset it
             if (pTaskRelative > 1f) {
@@ -379,7 +481,7 @@
             int scaleYOffset = (int) (((1f - scale) * mTaskRect.height()) / 2);
             transformOut.scale = scale;
             transformOut.translationY = sCurve.pToX(pBounded, mStackRect) - mStackRect.top -
-                    scaleYOffset;
+                    scaleYOffset - overscrollYOffset;
             transformOut.translationZ = Math.max(mMinTranslationZ,
                     mMinTranslationZ + (pBounded * (mMaxTranslationZ - mMinTranslationZ)));
             transformOut.rect.set(mTaskRect);
@@ -392,6 +494,27 @@
     }
 
     /**
+     * Update/get the transform
+     */
+    public TaskViewTransform getFreeformWorkspaceBounds(float stackScroll,
+            TaskViewTransform transformOut) {
+        transformOut.reset();
+        if (mNumFreeformTasks == 0) {
+            return transformOut;
+        }
+
+        if (stackScroll > mMaxStackScrollP) {
+            float stackOverscroll = (stackScroll - mMaxStackScrollP) / mFreeformWorkspacePOffset;
+            int overscrollYOffset = (int) (stackOverscroll * mFreeformRect.height());
+            int frontTaskBottom = mStackRect.height() - mTaskBottomOffset;
+            transformOut.visible = true;
+            transformOut.alpha =
+            transformOut.translationY = frontTaskBottom - overscrollYOffset;
+        }
+        return transformOut;
+    }
+
+    /**
      * Returns the untransformed task view bounds.
      */
     public Rect getUntransformedTaskViewBounds() {
@@ -406,4 +529,29 @@
         if (!mTaskProgressMap.containsKey(t.key)) return 0f;
         return mTaskProgressMap.get(t.key);
     }
+
+    /**
+     * Maps a movement in screen y, relative to {@param downY}, to a movement in along the arc
+     * length of the curve.  We know the curve is mostly flat, so we just map the length of the
+     * 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());
+        return -deltaP;
+    }
+
+    /**
+     * This is the inverse of {@link #getDeltaPForY}.  Given a movement along the arc length
+     * 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());
+        return -y;
+    }
+
+    private float getBottomAlignedScrollProgress(float p, float pOffsetFromBottom) {
+        // At scroll progress == p, then p is at the top of the stack
+        // At scroll progress == p + 1, then p is at the bottom of the stack
+        return p - (1 - pOffsetFromBottom);
+    }
 }
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 3d3b13d..15fcab4 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewScroller.java
@@ -21,6 +21,7 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.util.Log;
 import android.view.animation.AnimationUtils;
 import android.view.animation.Interpolator;
 import android.widget.OverScroller;
@@ -29,8 +30,12 @@
 
 /* The scrolling logic for a TaskStackView */
 public class TaskStackViewScroller {
+
+    private static final String TAG = "TaskStackViewScroller";
+    private static final boolean DEBUG = false;
+
     public interface TaskStackViewScrollerCallbacks {
-        public void onScrollChanged(float p);
+        void onScrollChanged(float p);
     }
 
     Context mContext;
@@ -38,6 +43,8 @@
     TaskStackViewScrollerCallbacks mCb;
 
     float mStackScrollP;
+    float mFlingDownScrollP;
+    int mFlingDownY;
 
     OverScroller mScroller;
     ObjectAnimator mScrollAnimator;
@@ -77,11 +84,6 @@
         }
     }
 
-    /** Sets the current stack scroll without calling the callback. */
-    void setStackScrollRaw(float s) {
-        mStackScrollP = s;
-    }
-
     /**
      * Sets the current stack scroll to the initial state when you first enter recents.
      * @return whether the stack progress changed.
@@ -92,6 +94,20 @@
         return Float.compare(prevStackScrollP, mStackScrollP) != 0;
     }
 
+    /**
+     * Starts a fling that is coordinated with the {@link TaskStackViewTouchHandler}.
+     */
+    public void fling(float downScrollP, int downY, int y, int velY, int minY, int maxY,
+            int overscroll) {
+        if (DEBUG) {
+            Log.d(TAG, "fling: " + downScrollP + ", downY: " + downY + ", y: " + y +
+                    ", velY: " + velY + ", minY: " + minY + ", maxY: " + maxY);
+        }
+        mFlingDownScrollP = downScrollP;
+        mFlingDownY = downY;
+        mScroller.fling(0, y, 0, velY, 0, 0, minY, maxY, 0, overscroll);
+    }
+
     /** Bounds the current scroll if necessary */
     public boolean boundScroll() {
         float curScroll = getStackScroll();
@@ -174,21 +190,20 @@
 
     /**** OverScroller ****/
 
+    // TODO: Remove
+    @Deprecated
     int progressToScrollRange(float p) {
         return (int) (p * mLayoutAlgorithm.mStackRect.height());
     }
 
-    float scrollRangeToProgress(int s) {
-        return (float) s / mLayoutAlgorithm.mStackRect.height();
-    }
-
     /** Called from the view draw, computes the next scroll. */
     boolean computeScroll() {
         if (mScroller.computeScrollOffset()) {
-            float scroll = scrollRangeToProgress(mScroller.getCurrY());
-            setStackScrollRaw(scroll);
-            if (mCb != null) {
-                mCb.onScrollChanged(scroll);
+            float deltaP = mLayoutAlgorithm.getDeltaPForY(mFlingDownY, mScroller.getCurrY());
+            float scroll = mFlingDownScrollP + deltaP;
+            setStackScroll(scroll);
+            if (DEBUG) {
+                Log.d(TAG, "computeScroll: " + scroll);
             }
             return true;
         }
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 3a1a987..08889c5 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackViewTouchHandler.java
@@ -17,6 +17,7 @@
 package com.android.systemui.recents.views;
 
 import android.content.Context;
+import android.content.res.Resources;
 import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.VelocityTracker;
@@ -34,7 +35,11 @@
 
 /* Handles touch events for a TaskStackView. */
 class TaskStackViewTouchHandler implements SwipeHelper.Callback {
-    static int INACTIVE_POINTER_ID = -1;
+
+    private static final String TAG = "TaskStackViewTouchHandler";
+    private static final boolean DEBUG = true;
+
+    private static int INACTIVE_POINTER_ID = -1;
 
     Context mContext;
     TaskStackView mSv;
@@ -42,21 +47,16 @@
     VelocityTracker mVelocityTracker;
 
     boolean mIsScrolling;
-
-    float mInitialP;
-    float mLastP;
-    float mTotalPMotion;
-    int mInitialMotionX, mInitialMotionY;
-    int mLastMotionX, mLastMotionY;
+    float mDownScrollP;
+    int mDownX, mDownY;
     int mActivePointerId = INACTIVE_POINTER_ID;
+    int mOverscrollSize;
     TaskView mActiveTaskView = null;
 
     int mMinimumVelocity;
     int mMaximumVelocity;
     // The scroll touch slop is used to calculate when we start scrolling
     int mScrollTouchSlop;
-    // The page touch slop is used to calculate when we start swiping
-    float mPagingTouchSlop;
     // Used to calculate when a tap is outside a task view rectangle.
     final int mWindowTouchSlop;
 
@@ -65,18 +65,20 @@
 
     public TaskStackViewTouchHandler(Context context, TaskStackView sv,
             TaskStackViewScroller scroller) {
-        mContext = context;
+        Resources res = context.getResources();
         ViewConfiguration configuration = ViewConfiguration.get(context);
+        mContext = context;
         mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
         mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
         mScrollTouchSlop = configuration.getScaledTouchSlop();
-        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
         mWindowTouchSlop = configuration.getScaledWindowTouchSlop();
         mSv = sv;
         mScroller = scroller;
 
-        float densityScale = context.getResources().getDisplayMetrics().density;
-        mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale, mPagingTouchSlop);
+        float densityScale = res.getDisplayMetrics().density;
+        mOverscrollSize = res.getDimensionPixelSize(R.dimen.recents_stack_overscroll);
+        mSwipeHelper = new SwipeHelper(context, SwipeHelper.X, this, densityScale,
+                configuration.getScaledPagingTouchSlop());
         mSwipeHelper.setMinAlpha(1f);
     }
 
@@ -88,11 +90,6 @@
             mVelocityTracker.clear();
         }
     }
-    void initVelocityTrackerIfNotExists() {
-        if (mVelocityTracker == null) {
-            mVelocityTracker = VelocityTracker.obtain();
-        }
-    }
     void recycleVelocityTracker() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
@@ -115,175 +112,70 @@
         return null;
     }
 
-    /** Constructs a simulated motion event for the current stack scroll. */
-    MotionEvent createMotionEventForStackScroll(MotionEvent ev) {
-        MotionEvent pev = MotionEvent.obtainNoHistory(ev);
-        pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll()));
-        return pev;
-    }
-
     /** Touch preprocessing for handling below */
     public boolean onInterceptTouchEvent(MotionEvent ev) {
-        // Return early if we have no children
-        boolean hasTaskViews = (mSv.getTaskViews().size() > 0);
-        if (!hasTaskViews) {
-            return false;
-        }
-
         // Pass through to swipe helper if we are swiping
         mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
         if (mInterceptedBySwipeHelper) {
             return true;
         }
 
-        TaskStackViewLayoutAlgorithm layoutAlgorithm = mSv.mLayoutAlgorithm;
-        boolean wasScrolling = mScroller.isScrolling() ||
-                (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning());
-        int action = ev.getAction();
-        switch (action & MotionEvent.ACTION_MASK) {
-            case MotionEvent.ACTION_DOWN: {
-                // Save the touch down info
-                mInitialMotionX = mLastMotionX = (int) ev.getX();
-                mInitialMotionY = mLastMotionY = (int) ev.getY();
-                mInitialP = mLastP = layoutAlgorithm.sCurve.xToP(mLastMotionY,
-                        layoutAlgorithm.mStackRect);
-                mActivePointerId = ev.getPointerId(0);
-                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
-                // Stop the current scroll if it is still flinging
-                mScroller.stopScroller();
-                mScroller.stopBoundScrollAnimation();
-                // Initialize the velocity tracker
-                initOrResetVelocityTracker();
-                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
-                break;
-            }
-            case MotionEvent.ACTION_POINTER_DOWN: {
-                final int index = ev.getActionIndex();
-                mActivePointerId = ev.getPointerId(index);
-                mLastMotionX = (int) ev.getX(index);
-                mLastMotionY = (int) ev.getY(index);
-                mLastP = layoutAlgorithm.sCurve.xToP(mLastMotionY, layoutAlgorithm.mStackRect);
-                break;
-            }
-            case MotionEvent.ACTION_MOVE: {
-                if (mActivePointerId == INACTIVE_POINTER_ID) break;
-
-                // Initialize the velocity tracker if necessary
-                initVelocityTrackerIfNotExists();
-                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
-
-                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
-                int y = (int) ev.getY(activePointerIndex);
-                int x = (int) ev.getX(activePointerIndex);
-                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
-                    // Save the touch move info
-                    mIsScrolling = true;
-                    // Disallow parents from intercepting touch events
-                    final ViewParent parent = mSv.getParent();
-                    if (parent != null) {
-                        parent.requestDisallowInterceptTouchEvent(true);
-                    }
-                }
-
-                mLastMotionX = x;
-                mLastMotionY = y;
-                mLastP = layoutAlgorithm.sCurve.xToP(mLastMotionY, layoutAlgorithm.mStackRect);
-                break;
-            }
-            case MotionEvent.ACTION_POINTER_UP: {
-                int pointerIndex = ev.getActionIndex();
-                int pointerId = ev.getPointerId(pointerIndex);
-                if (pointerId == mActivePointerId) {
-                    // Select a new active pointer id and reset the motion state
-                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
-                    mActivePointerId = ev.getPointerId(newPointerIndex);
-                    mLastMotionX = (int) ev.getX(newPointerIndex);
-                    mLastMotionY = (int) ev.getY(newPointerIndex);
-                    mLastP = layoutAlgorithm.sCurve.xToP(mLastMotionY, layoutAlgorithm.mStackRect);
-                    mVelocityTracker.clear();
-                }
-                break;
-            }
-            case MotionEvent.ACTION_CANCEL:
-            case MotionEvent.ACTION_UP: {
-                // Animate the scroll back if we've cancelled
-                mScroller.animateBoundScroll();
-                // Reset the drag state and the velocity tracker
-                mIsScrolling = false;
-                mActivePointerId = INACTIVE_POINTER_ID;
-                mActiveTaskView = null;
-                mTotalPMotion = 0;
-                recycleVelocityTracker();
-                break;
-            }
-        }
-
-        return wasScrolling || mIsScrolling;
+        return handleTouchEvent(ev);
     }
 
     /** Handles touch events once we have intercepted them */
     public boolean onTouchEvent(MotionEvent ev) {
-        // Short circuit if we have no children
-        boolean hasTaskViews = (mSv.getTaskViews().size() > 0);
-        if (!hasTaskViews) {
-            return false;
-        }
-
         // Pass through to swipe helper if we are swiping
         if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
             return true;
         }
 
-        // Update the velocity tracker
-        initVelocityTrackerIfNotExists();
+        handleTouchEvent(ev);
+        return true;
+    }
+
+    private boolean handleTouchEvent(MotionEvent ev) {
+        // Short circuit if we have no children
+        if (mSv.getTaskViews().size() == 0) {
+            return false;
+        }
 
         TaskStackViewLayoutAlgorithm layoutAlgorithm = mSv.mLayoutAlgorithm;
         int action = ev.getAction();
         switch (action & MotionEvent.ACTION_MASK) {
             case MotionEvent.ACTION_DOWN: {
                 // Save the touch down info
-                mInitialMotionX = mLastMotionX = (int) ev.getX();
-                mInitialMotionY = mLastMotionY = (int) ev.getY();
-                mInitialP = mLastP = layoutAlgorithm.sCurve.xToP(mLastMotionY,
-                        layoutAlgorithm.mStackRect);
+                mDownX = (int) ev.getX();
+                mDownY = (int) ev.getY();
+                mDownScrollP = mScroller.getStackScroll();
                 mActivePointerId = ev.getPointerId(0);
-                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
+                mActiveTaskView = findViewAtPoint(mDownX, mDownY);
+
                 // Stop the current scroll if it is still flinging
                 mScroller.stopScroller();
                 mScroller.stopBoundScrollAnimation();
+
                 // Initialize the velocity tracker
                 initOrResetVelocityTracker();
-                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
-                // Disallow parents from intercepting touch events
-                final ViewParent parent = mSv.getParent();
-                if (parent != null) {
-                    parent.requestDisallowInterceptTouchEvent(true);
-                }
+                mVelocityTracker.addMovement(ev);
                 break;
             }
             case MotionEvent.ACTION_POINTER_DOWN: {
                 final int index = ev.getActionIndex();
+                mDownX = (int) ev.getX();
+                mDownY = (int) ev.getY();
+                mDownScrollP = mScroller.getStackScroll();
                 mActivePointerId = ev.getPointerId(index);
-                mLastMotionX = (int) ev.getX(index);
-                mLastMotionY = (int) ev.getY(index);
-                mLastP = layoutAlgorithm.sCurve.xToP(mLastMotionY,
-                        layoutAlgorithm.mStackRect);
+                mVelocityTracker.addMovement(ev);
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
-                if (mActivePointerId == INACTIVE_POINTER_ID) break;
-
-                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
-
                 int activePointerIndex = ev.findPointerIndex(mActivePointerId);
-                int x = (int) ev.getX(activePointerIndex);
                 int y = (int) ev.getY(activePointerIndex);
-                int yTotal = Math.abs(y - mInitialMotionY);
-                float curP = layoutAlgorithm.sCurve.xToP(y, layoutAlgorithm.mStackRect);
-                float deltaP = mLastP - curP;
                 if (!mIsScrolling) {
-                    if (yTotal > mScrollTouchSlop) {
+                    if (Math.abs(y - mDownY) > mScrollTouchSlop) {
                         mIsScrolling = true;
+
                         // Disallow parents from intercepting touch events
                         final ViewParent parent = mSv.getParent();
                         if (parent != null) {
@@ -292,54 +184,14 @@
                     }
                 }
                 if (mIsScrolling) {
-                    float curStackScroll = mScroller.getStackScroll();
-                    float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP);
-                    if (Float.compare(overScrollAmount, 0f) != 0) {
-                        // Bound the overscroll to a fixed amount, and inversely scale the y-movement
-                        // relative to how close we are to the max overscroll
-                        float maxOverScroll = mContext.getResources().getFloat(
-                                R.dimen.recents_stack_overscroll_percentage);
-                        deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount)
-                                / maxOverScroll));
-                    }
-                    mScroller.setStackScroll(curStackScroll + deltaP);
-                }
-                mLastMotionX = x;
-                mLastMotionY = y;
-                mLastP = layoutAlgorithm.sCurve.xToP(mLastMotionY, layoutAlgorithm.mStackRect);
-                mTotalPMotion += Math.abs(deltaP);
-                break;
-            }
-            case MotionEvent.ACTION_UP: {
-                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
-                int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
-                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
-                    float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity);
-                    int overscrollRange = (int) (Math.min(1f, overscrollRangePct) *
-                            (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange -
-                                    Constants.Values.TaskStackView.TaskStackMinOverscrollRange));
-                    mScroller.mScroller.fling(0,
-                            mScroller.progressToScrollRange(mScroller.getStackScroll()),
-                            0, velocity,
-                            0, 0,
-                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP),
-                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP),
-                            0, Constants.Values.TaskStackView.TaskStackMinOverscrollRange +
-                                    overscrollRange);
-                    // Invalidate to kick off computeScroll
-                    mSv.invalidate();
-                } else if (mIsScrolling && mScroller.isScrollOutOfBounds()) {
-                    // Animate the scroll back into bounds
-                    mScroller.animateBoundScroll();
-                } else if (mActiveTaskView == null) {
-                    // This tap didn't start on a task.
-                    maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY());
+                    // If we just move linearly on the screen, then that would map to 1/arclength
+                    // of the curve, so just move the scroll proportional to that
+                    float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y);
+                    float curScrollP = mDownScrollP + deltaP;
+                    mScroller.setStackScroll(curScrollP);
                 }
 
-                mActivePointerId = INACTIVE_POINTER_ID;
-                mIsScrolling = false;
-                mTotalPMotion = 0;
-                recycleVelocityTracker();
+                mVelocityTracker.addMovement(ev);
                 break;
             }
             case MotionEvent.ACTION_POINTER_UP: {
@@ -349,11 +201,41 @@
                     // Select a new active pointer id and reset the motion state
                     final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
                     mActivePointerId = ev.getPointerId(newPointerIndex);
-                    mLastMotionX = (int) ev.getX(newPointerIndex);
-                    mLastMotionY = (int) ev.getY(newPointerIndex);
-                    mLastP = layoutAlgorithm.sCurve.xToP(mLastMotionY, layoutAlgorithm.mStackRect);
-                    mVelocityTracker.clear();
                 }
+                mVelocityTracker.addMovement(ev);
+                break;
+            }
+            case MotionEvent.ACTION_UP: {
+                mVelocityTracker.addMovement(ev);
+                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                int y = (int) ev.getY(activePointerIndex);
+                int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
+                if (mIsScrolling) {
+                    if (mScroller.isScrollOutOfBounds()) {
+                        // Animate the scroll back into bounds
+                        mScroller.animateBoundScroll();
+                    } else if (Math.abs(velocity) > mMinimumVelocity) {
+                        float deltaP = layoutAlgorithm.getDeltaPForY(mDownY, y);
+                        float curScrollP = mDownScrollP + deltaP;
+                        float downToCurY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
+                                curScrollP);
+                        float downToMinY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
+                                layoutAlgorithm.mMaxScrollP);
+                        float downToMaxY = mDownY + layoutAlgorithm.getYForDeltaP(mDownScrollP,
+                                layoutAlgorithm.mMinScrollP);
+                        mScroller.fling(mDownScrollP, mDownY, (int) downToCurY, velocity,
+                                (int) downToMinY, (int) downToMaxY, mOverscrollSize);
+                        mSv.invalidate();
+                    }
+                } else if (mActiveTaskView == null) {
+                    // This tap didn't start on a task.
+                    maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY());
+                }
+
+                mActivePointerId = INACTIVE_POINTER_ID;
+                mIsScrolling = false;
+                recycleVelocityTracker();
                 break;
             }
             case MotionEvent.ACTION_CANCEL: {
@@ -363,20 +245,19 @@
                 }
                 mActivePointerId = INACTIVE_POINTER_ID;
                 mIsScrolling = false;
-                mTotalPMotion = 0;
                 recycleVelocityTracker();
                 break;
             }
         }
-        return true;
+        return mIsScrolling;
     }
 
     /** Hides recents if the up event at (x, y) is a tap on the background area. */
     void maybeHideRecentsFromBackgroundTap(int x, int y) {
         // Ignore the up event if it's too far from its start position. The user might have been
         // trying to scroll or swipe.
-        int dx = Math.abs(mInitialMotionX - x);
-        int dy = Math.abs(mInitialMotionY - y);
+        int dx = Math.abs(mDownX - x);
+        int dy = Math.abs(mDownY - y);
         if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) {
             return;
         }
@@ -427,12 +308,16 @@
 
     @Override
     public boolean canChildBeDismissed(View v) {
+        if (v instanceof TaskView) {
+            return !((TaskView) v).getTask().isFreeformTask();
+        }
         return true;
     }
 
     @Override
     public void onBeginDrag(View v) {
         TaskView tv = (TaskView) v;
+        mSwipeHelper.setSnapBackTranslationX(tv.getTranslationX());
         // Disable clipping with the stack while we are swiping
         tv.setClipViewInStack(false);
         // Disallow touch events from this task view
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 ec50eb6..174ff33 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskViewTransform.java
@@ -26,6 +26,7 @@
 /* The transform state for a task view */
 public class TaskViewTransform {
     public int startDelay = 0;
+    public int translationX = 0;
     public int translationY = 0;
     public float translationZ = 0;
     public float scale = 1f;
@@ -43,6 +44,7 @@
 
     public TaskViewTransform(TaskViewTransform o) {
         startDelay = o.startDelay;
+        translationX = o.translationX;
         translationY = o.translationY;
         translationZ = o.translationZ;
         scale = o.scale;
@@ -52,9 +54,12 @@
         p = o.p;
     }
 
-    /** Resets the current transform */
+    /**
+     * Resets the current transform.
+     */
     public void reset() {
         startDelay = 0;
+        translationX = 0;
         translationY = 0;
         translationZ = 0;
         scale = 1f;
@@ -71,6 +76,9 @@
     public boolean hasScaleChangedFrom(float v) {
         return (Float.compare(scale, v) != 0);
     }
+    public boolean hasTranslationXChangedFrom(float v) {
+        return (Float.compare(translationX, v) != 0);
+    }
     public boolean hasTranslationYChangedFrom(float v) {
         return (Float.compare(translationY, v) != 0);
     }
@@ -87,6 +95,9 @@
             boolean requiresLayers = false;
 
             // Animate to the final state
+            if (hasTranslationXChangedFrom(v.getTranslationX())) {
+                anim.translationX(translationX);
+            }
             if (hasTranslationYChangedFrom(v.getTranslationY())) {
                 anim.translationY(translationY);
             }
@@ -117,6 +128,9 @@
                     .start();
         } else {
             // Set the changed properties
+            if (hasTranslationXChangedFrom(v.getTranslationX())) {
+                v.setTranslationX(translationX);
+            }
             if (hasTranslationYChangedFrom(v.getTranslationY())) {
                 v.setTranslationY(translationY);
             }
@@ -147,7 +161,8 @@
 
     @Override
     public String toString() {
-        return "TaskViewTransform delay: " + startDelay + " y: " + translationY + " z: " + translationZ +
+        return "TaskViewTransform delay: " + startDelay +
+                " x: " + translationX + " y: " + translationY + " z: " + translationZ +
                 " scale: " + scale + " alpha: " + alpha + " visible: " + visible + " rect: " + rect +
                 " p: " + p;
     }