Initial changes to stack layout to support paging and nonsquare thumbs.

- Moving to a couple piecewise curves to define the various overview
  layout states.  Added a new state for focus (to be used in follow up
  CL) to control paging of overview from the nav bar button.  This 
  allows us to control the visible range of items on the curve, and 
  to better fit other UI controls around the stack.
- Removed the scaling of the tasks in the stack
- Also refactoring parametric curve to just use the system Path

Change-Id: I4108da77986d86896576e36fa8f31189d6fcb6f3
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index 368f9f7..5af3b67 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -12,12 +12,23 @@
 
 -keep class com.android.systemui.statusbar.phone.PhoneStatusBar
 -keep class com.android.systemui.statusbar.tv.TvStatusBar
--keep class com.android.systemui.recents.*
 
 -keepclassmembers class ** {
     public void onBusEvent(**);
     public void onInterprocessBusEvent(**);
 }
 -keepclassmembers class ** extends **.EventBus$InterprocessEvent {
-	public <init>(android.os.Bundle);
-}
\ No newline at end of file
+    public <init>(android.os.Bundle);
+}
+
+-keep class com.android.systemui.recents.views.StackLayoutAlgorithm {
+    public float getFocusState();
+    public void setFocusState(float);
+}
+
+-keep class com.android.systemui.recents.views.TaskView {
+    public int getDim();
+    public void setDim(int);
+    public float getTaskProgress();
+    public void setTaskProgress(float);
+}
diff --git a/packages/SystemUI/res/values-land/config.xml b/packages/SystemUI/res/values-land/config.xml
index f7e2344..d7940cc 100644
--- a/packages/SystemUI/res/values-land/config.xml
+++ b/packages/SystemUI/res/values-land/config.xml
@@ -28,4 +28,14 @@
 
     <!-- We have only space for one notification on phone landscape layouts. -->
     <integer name="keyguard_max_notification_count">1</integer>
+
+    <!-- Recents: The relative range of visible tasks from the current scroll position
+         while the stack is focused. -->
+    <item name="recents_layout_focused_range_min" format="float" type="integer">-2</item>
+    <item name="recents_layout_focused_range_max" format="float" type="integer">2</item>
+
+    <!-- Recents: The relative range of visible tasks from the current scroll position
+         while the stack is not focused. -->
+    <item name="recents_layout_unfocused_range_min" format="float" type="integer">-2</item>
+    <item name="recents_layout_unfocused_range_max" format="float" type="integer">1.5</item>
 </resources>
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index 4f6d209..6795da4 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -32,4 +32,14 @@
 
     <!-- Set to true to enable the user switcher on the keyguard. -->
     <bool name="config_keyguardUserSwitcher">true</bool>
+
+    <!-- Recents: The relative range of visible tasks from the current scroll position
+         while the stack is focused. -->
+    <item name="recents_layout_focused_range_min" format="float" type="integer">-4</item>
+    <item name="recents_layout_focused_range_max" format="float" type="integer">3</item>
+
+    <!-- Recents: The relative range of visible tasks from the current scroll position
+         while the stack is not focused. -->
+    <item name="recents_layout_unfocused_range_min" format="float" type="integer">-2</item>
+    <item name="recents_layout_unfocused_range_max" format="float" type="integer">2.5</item>
 </resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 0c638a2..5677cc3 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -192,6 +192,16 @@
     <!-- Svelte specific logic, see RecentsConfiguration.SVELTE_* constants. -->
     <integer name="recents_svelte_level">0</integer>
 
+    <!-- Recents: The relative range of visible tasks from the current scroll position
+         while the stack is focused. -->
+    <item name="recents_layout_focused_range_min" format="float" type="integer">-4</item>
+    <item name="recents_layout_focused_range_max" format="float" type="integer">3</item>
+
+    <!-- Recents: The relative range of visible tasks from the current scroll position
+         while the stack is not focused. -->
+    <item name="recents_layout_unfocused_range_min" format="float" type="integer">-2</item>
+    <item name="recents_layout_unfocused_range_max" format="float" type="integer">2.5</item>
+
     <!-- Whether to enable KeyguardService or not -->
     <bool name="config_enableKeyguardService">true</bool>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 73f63a9..c85ada8 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -247,6 +247,12 @@
     <!-- The amount to allow the stack to overscroll. -->
     <dimen name="recents_stack_overscroll">24dp</dimen>
 
+    <!-- The size of the peek area at the top of the stack. -->
+    <dimen name="recents_layout_focused_peek_size">@dimen/recents_history_button_height</dimen>
+
+    <!-- The height of the history button. -->
+    <dimen name="recents_history_button_height">48dp</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/misc/FreePathInterpolator.java b/packages/SystemUI/src/com/android/systemui/recents/misc/FreePathInterpolator.java
new file mode 100644
index 0000000..720c952
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/FreePathInterpolator.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.recents.misc;
+
+import android.graphics.Path;
+import android.view.animation.BaseInterpolator;
+import android.view.animation.Interpolator;
+
+/**
+ * An interpolator that can traverse a Path. The x coordinate along the <code>Path</code>
+ * is the input value and the output is the y coordinate of the line at that point.
+ * This means that the Path must conform to a function <code>y = f(x)</code>.
+ *
+ * <p>The <code>Path</code> must not have gaps in the x direction and must not
+ * loop back on itself such that there can be two points sharing the same x coordinate.
+ * It is alright to have a disjoint line in the vertical direction:</p>
+ * <p><blockquote><pre>
+ *     Path path = new Path();
+ *     path.lineTo(0.25f, 0.25f);
+ *     path.moveTo(0.25f, 0.5f);
+ *     path.lineTo(1f, 1f);
+ * </pre></blockquote></p>
+ */
+public class FreePathInterpolator extends BaseInterpolator {
+
+    // This governs how accurate the approximation of the Path is.
+    private static final float PRECISION = 0.002f;
+
+    private float[] mX;
+    private float[] mY;
+    private float mArcLength;
+
+    /**
+     * Create an interpolator for an arbitrary <code>Path</code>.
+     *
+     * @param path The <code>Path</code> to use to make the line representing the interpolator.
+     */
+    public FreePathInterpolator(Path path) {
+        initPath(path);
+    }
+
+    private void initPath(Path path) {
+        float[] pointComponents = path.approximate(PRECISION);
+
+        int numPoints = pointComponents.length / 3;
+
+        mX = new float[numPoints];
+        mY = new float[numPoints];
+        mArcLength = 0;
+        float prevX = 0;
+        float prevY = 0;
+        float prevFraction = 0;
+        int componentIndex = 0;
+        for (int i = 0; i < numPoints; i++) {
+            float fraction = pointComponents[componentIndex++];
+            float x = pointComponents[componentIndex++];
+            float y = pointComponents[componentIndex++];
+            if (fraction == prevFraction && x != prevX) {
+                throw new IllegalArgumentException(
+                        "The Path cannot have discontinuity in the X axis.");
+            }
+            if (x < prevX) {
+                throw new IllegalArgumentException("The Path cannot loop back on itself.");
+            }
+            mX[i] = x;
+            mY[i] = y;
+            mArcLength += Math.hypot(x - prevX, y - prevY);
+            prevX = x;
+            prevY = y;
+            prevFraction = fraction;
+        }
+    }
+
+    /**
+     * Using the line in the Path in this interpolator that can be described as
+     * <code>y = f(x)</code>, finds the y coordinate of the line given <code>t</code>
+     * as the x coordinate.
+     *
+     * @param t Treated as the x coordinate along the line.
+     * @return The y coordinate of the Path along the line where x = <code>t</code>.
+     * @see Interpolator#getInterpolation(float)
+     */
+    @Override
+    public float getInterpolation(float t) {
+        int startIndex = 0;
+        int endIndex = mX.length - 1;
+
+        // Return early if out of bounds
+        if (t <= 0) {
+            return mY[startIndex];
+        } else if (t >= 1) {
+            return mY[endIndex];
+        }
+
+        // Do a binary search for the correct x to interpolate between.
+        while (endIndex - startIndex > 1) {
+            int midIndex = (startIndex + endIndex) / 2;
+            if (t < mX[midIndex]) {
+                endIndex = midIndex;
+            } else {
+                startIndex = midIndex;
+            }
+        }
+
+        float xRange = mX[endIndex] - mX[startIndex];
+        if (xRange == 0) {
+            return mY[startIndex];
+        }
+
+        float tInRange = t - mX[startIndex];
+        float fraction = tInRange / xRange;
+
+        float startY = mY[startIndex];
+        float endY = mY[endIndex];
+        return startY + (fraction * (endY - startY));
+    }
+
+    /**
+     * Finds the x that provides the given <code>y = f(x)</code>.
+     *
+     * @param y a value from (0,1) that is in this path.
+     */
+    public float getX(float y) {
+        int startIndex = 0;
+        int endIndex = mY.length - 1;
+
+        // Return early if out of bounds
+        if (y <= 0) {
+            return mX[endIndex];
+        } else if (y >= 1) {
+            return mX[startIndex];
+        }
+
+        // Do a binary search for index that bounds the y
+        while (endIndex - startIndex > 1) {
+            int midIndex = (startIndex + endIndex) / 2;
+            if (y < mY[midIndex]) {
+                startIndex = midIndex;
+            } else {
+                endIndex = midIndex;
+            }
+        }
+
+        float yRange = mY[endIndex] - mY[startIndex];
+        if (yRange == 0) {
+            return mX[startIndex];
+        }
+
+        float tInRange = y - mY[startIndex];
+        float fraction = tInRange / yRange;
+
+        float startX = mX[startIndex];
+        float endX = mX[endIndex];
+        return startX + (fraction * (endX - startX));
+    }
+
+    /**
+     * Returns the arclength of the path we are interpolating.
+     */
+    public float getArcLength() {
+        return mArcLength;
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java b/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java
deleted file mode 100644
index 515c3bd..0000000
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/ParametricCurve.java
+++ /dev/null
@@ -1,274 +0,0 @@
-/*
- * Copyright (C) 2014 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.recents.misc;
-
-import android.graphics.Rect;
-import android.os.SystemClock;
-import android.util.Log;
-
-/**
- * Represents a 2d curve that is parameterized along the arc length of the curve (p), and allows the
- * conversions of x->p and p->x.
- */
-public class ParametricCurve {
-
-    private static final boolean DEBUG = false;
-    private static final String TAG = "ParametricCurve";
-
-    private static final int PrecisionSteps = 250;
-
-    /**
-     * A 2d function, representing the curve.
-     */
-    public interface CurveFunction {
-        float f(float x);
-        float invF(float y);
-    }
-
-    /**
-     * A function that returns a value given a parametric value.
-     */
-    public interface ParametricCurveFunction {
-        float f(float p);
-    }
-
-    float[] xp;
-    float[] px;
-    float mLength;
-
-    CurveFunction mFn;
-    ParametricCurveFunction mScaleFn;
-
-    public ParametricCurve(CurveFunction fn, ParametricCurveFunction scaleFn) {
-        long t1;
-        if (DEBUG) {
-            t1 = SystemClock.currentThreadTimeMicro();
-            Log.d(TAG, "initializeCurve");
-        }
-        mFn = fn;
-        mScaleFn = scaleFn;
-        xp = new float[PrecisionSteps + 1];
-        px = new float[PrecisionSteps + 1];
-
-        // Approximate f(x)
-        float[] fx = new float[PrecisionSteps + 1];
-        float step = 1f / PrecisionSteps;
-        float x = 0;
-        for (int xStep = 0; xStep <= PrecisionSteps; xStep++) {
-            fx[xStep] = fn.f(x);
-            x += step;
-        }
-        // Calculate the arc length for x:1->0
-        float pLength = 0;
-        float[] dx = new float[PrecisionSteps + 1];
-        dx[0] = 0;
-        for (int xStep = 1; xStep < PrecisionSteps; xStep++) {
-            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;
-        px[PrecisionSteps] = 1f;
-        if (DEBUG) {
-            Log.d(TAG, "p[0]=0");
-            Log.d(TAG, "p[" + PrecisionSteps + "]=1");
-        }
-        for (int xStep = 1; xStep < PrecisionSteps; xStep++) {
-            p += Math.abs(dx[xStep] / pLength);
-            px[xStep] = p;
-            if (DEBUG) {
-                Log.d(TAG, "p[" + xStep + "]=" + p);
-            }
-        }
-        // Given p(x), calculate the inverse function x(p). This assumes that x(p) is also a valid
-        // function.
-        int xStep = 0;
-        p = 0;
-        xp[0] = 0f;
-        xp[PrecisionSteps] = 1f;
-        if (DEBUG) {
-            Log.d(TAG, "x[0]=0");
-            Log.d(TAG, "x[" + PrecisionSteps + "]=1");
-        }
-        for (int pStep = 0; pStep < PrecisionSteps; pStep++) {
-            // Walk forward in px and find the x where px <= p && p < px+1
-            while (xStep < PrecisionSteps) {
-                if (px[xStep] > p) break;
-                xStep++;
-            }
-            // Now, px[xStep-1] <= p < px[xStep]
-            if (xStep == 0) {
-                xp[pStep] = 0;
-            } else {
-                // Find x such that proportionally, x is correct
-                float fraction = (p - px[xStep - 1]) / (px[xStep] - px[xStep - 1]);
-                x = (xStep - 1 + fraction) * step;
-                xp[pStep] = x;
-            }
-            if (DEBUG) {
-                Log.d(TAG, "x[" + pStep + "]=" + xp[pStep]);
-            }
-            p += step;
-        }
-        if (DEBUG) {
-            Log.d(TAG, "\t1t: " + (SystemClock.currentThreadTimeMicro() - t1) + "microsecs");
-        }
-    }
-
-    /**
-     * Converts from the progress along the arc-length of the curve to a coordinate within the
-     * bounds.  Note, p=0 represents the top of the bounds, and p=1 represents the bottom.
-     */
-    public int pToX(float p, Rect bounds) {
-        int top = bounds.top;
-        int height = bounds.height();
-
-        if (p <= 0f) return top;
-        if (p >= 1f) return top + (int) (p * height);
-
-        float pIndex = p * PrecisionSteps;
-        int pFloorIndex = (int) Math.floor(pIndex);
-        int pCeilIndex = (int) Math.ceil(pIndex);
-        float x = xp[pFloorIndex];
-        if (pFloorIndex < PrecisionSteps && (pCeilIndex != pFloorIndex)) {
-            // Interpolate between the two precalculated positions
-            x += (xp[pCeilIndex] - xp[pFloorIndex]) * (pIndex - pFloorIndex);
-        }
-        return top + (int) (x * height);
-    }
-
-    /**
-     * Converts from the progress along the arc-length of the curve to a scale.
-     */
-    public float pToScale(float p) {
-        return mScaleFn.f(p);
-    }
-
-    /**
-     * Converts from a bounds coordinate to the progress along the arc-length of the curve.
-     * Note, p=0 represents the top of the bounds, and p=1 represents the bottom.
-     */
-    public float xToP(int x, Rect bounds) {
-        int top = bounds.top;
-
-        float xf = (float) (x - top) / bounds.height();
-        if (xf <= 0f) return 0f;
-        if (xf >= 1f) return xf;
-
-        float xIndex = xf * PrecisionSteps;
-        int xFloorIndex = (int) Math.floor(xIndex);
-        int xCeilIndex = (int) Math.ceil(xIndex);
-        float p = px[xFloorIndex];
-        if (xFloorIndex < PrecisionSteps && (xCeilIndex != xFloorIndex)) {
-            // Interpolate between the two precalculated positions
-            p += (px[xCeilIndex] - px[xFloorIndex]) * (xIndex - xFloorIndex);
-        }
-        return p;
-    }
-
-    /**
-     * Computes the progress offset from the bottom of the curve (p=1) such that the given height
-     * is visible when scaled at the that progress.
-     */
-    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;
-        }
-
-        // Find the next p(x) such that (bottom-x) == scale(p(x))*height
-        int minX = top;
-        int maxX = bottom;
-        long t1;
-        if (DEBUG) {
-            t1 = SystemClock.currentThreadTimeMicro();
-            Log.d(TAG, "computePOffsetForScaledHeight: " + height);
-        }
-        while (minX <= maxX) {
-            int midX = minX + ((maxX - minX) / 2);
-            float pMidX = xToP(midX, bounds);
-            float scaleMidX = mScaleFn.f(pMidX);
-            int scaledHeight = (int) (scaleMidX * height);
-            if ((bottom - midX) < scaledHeight) {
-                maxX = midX - 1;
-            } else if ((bottom - midX) > scaledHeight) {
-                minX = midX + 1;
-            } else {
-                if (DEBUG) {
-                    Log.d(TAG, "\t1t: " + (SystemClock.currentThreadTimeMicro() - t1) + "microsecs");
-                }
-                return 1f - pMidX;
-            }
-        }
-        if (DEBUG) {
-            Log.d(TAG, "\t2t: " + (SystemClock.currentThreadTimeMicro() - t1) + "microsecs");
-        }
-        return 1f - xToP(maxX, bounds);
-    }
-
-    /**
-     * Computes the progress offset from the bottom of the curve (p=1) that allows the given height,
-     * unscaled at the progress, will be visible.
-     */
-    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;
-        }
-
-        // Find the next p(x) such that (bottom-x) == height
-        int minX = top;
-        int maxX = bottom;
-        long t1;
-        if (DEBUG) {
-            t1 = SystemClock.currentThreadTimeMicro();
-            Log.d(TAG, "computePOffsetForHeight: " + height);
-        }
-        while (minX <= maxX) {
-            int midX = minX + ((maxX - minX) / 2);
-            if ((bottom - midX) < height) {
-                maxX = midX - 1;
-            } else if ((bottom - midX) > height) {
-                minX = midX + 1;
-            } else {
-                if (DEBUG) {
-                    Log.d(TAG, "\t1t: " + (SystemClock.currentThreadTimeMicro() - t1) + "microsecs");
-                }
-                return 1f - xToP(midX, bounds);
-            }
-        }
-        if (DEBUG) {
-            Log.d(TAG, "\t2t: " + (SystemClock.currentThreadTimeMicro() - t1) + "microsecs");
-        }
-        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/views/TaskStackLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
index b3bd6ed..bc74bdb 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackLayoutAlgorithm.java
@@ -18,12 +18,14 @@
 
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.Path;
 import android.graphics.Rect;
 import android.util.Log;
 import com.android.systemui.R;
 import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.RecentsActivityLaunchState;
 import com.android.systemui.recents.RecentsConfiguration;
-import com.android.systemui.recents.misc.ParametricCurve;
+import com.android.systemui.recents.misc.FreePathInterpolator;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.recents.misc.Utilities;
 import com.android.systemui.recents.model.Task;
@@ -32,22 +34,72 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 
+/**
+ * Used to describe a visible range that can be normalized to [0, 1].
+ */
+class Range {
+    final float relativeMin;
+    final float relativeMax;
+    float origin;
+    float min;
+    float max;
+
+    public Range(float relMin, float relMax) {
+        min = relativeMin = relMin;
+        max = relativeMax = relMax;
+    }
+
+    /**
+     * Offsets this range to a given absolute position.
+     */
+    public void offset(float x) {
+        this.origin = x;
+        min = x + relativeMin;
+        max = x + relativeMax;
+    }
+
+    /**
+     * Returns x normalized to the range 0 to 1 such that 0 = min, 0.5 = origin and 1 = max
+     *
+     * @param x is an absolute value in the same domain as origin
+     */
+    public float getNormalizedX(float x) {
+        if (x < origin) {
+            return 0.5f + 0.5f * (x - origin) / -relativeMin;
+        } else {
+            return 0.5f + 0.5f * (x - origin) / relativeMax;
+        }
+    }
+
+    /**
+     * Given a normalized {@param x} value in this range, projected onto the full range to get an
+     * absolute value about the given {@param origin}.
+     */
+    public float getAbsoluteX(float normX) {
+        if (normX < 0.5f) {
+            return (normX - 0.5f) / 0.5f * -relativeMin;
+        } else {
+            return (normX - 0.5f) / 0.5f * relativeMax;
+        }
+    }
+
+    /**
+     * Returns whether a value at an absolute x would be within range.
+     */
+    public boolean isInRange(float absX) {
+        return (absX >= Math.floor(min)) && (absX <= Math.ceil(max));
+    }
+}
 
 /**
- * The layout logic for a TaskStackView.
+ * The layout logic for a TaskStackView.  This layout can have two states focused and unfocused,
+ * and in the focused state, there is a task that is displayed more prominently in the stack.
  */
 public class TaskStackLayoutAlgorithm {
 
     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.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
-    private static final float VISIBLE_TASK_HEIGHT_BETWEEN_TASKS = 0.5f;
-
     // A report of the visibility state of the stack
     public class VisibilityReport {
         public int numVisibleTasks;
@@ -74,10 +126,30 @@
     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;
+    public Rect mCurrentStackRect = new Rect();
     // This is the current system insets
     public Rect mSystemInsets = new Rect();
 
+    // The visible ranges when the stack is focused and unfocused
+    private Range mUnfocusedRange;
+    private Range mFocusedRange;
+
+    // The offset from the top when scrolled to the top of the stack
+    private int mFocusedPeekHeight;
+
+    // The offset from the bottom of the stack to the bottom of the bounds
+    private int mStackBottomOffset;
+
+    // The paths defining the motion of the tasks when the stack is focused and unfocused
+    private Path mUnfocusedCurve;
+    private Path mFocusedCurve;
+    private FreePathInterpolator mUnfocusedCurveInterpolator;
+    private FreePathInterpolator mFocusedCurveInterpolator;
+
+    // The state of the stack focus (0..1), which controls the transition of the stack from the
+    // focused to non-focused state
+    private float mFocusState;
+
     // The smallest scroll progress, at this value, the back most task will be visible
     float mMinScrollP;
     // The largest scroll progress, at this value, the front most task will be visible above the
@@ -88,78 +160,33 @@
     // 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;
-    // The relative progress to ensure that the height between non-affiliated tasks is
-    // respected
-    float mBetweenAffiliationPOffset;
-    // The relative progress to ensure that the task height is respected
-    float mTaskHeightPOffset;
-    // The relative progress to ensure that the half task height is respected
-    float mTaskHalfHeightPOffset;
-    // The front-most task bottom offset
-    int mStackBottomOffset;
-    // 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 last computed task counts
     int mNumStackTasks;
     int mNumFreeformTasks;
+
     // The min/max z translations
     int mMinTranslationZ;
     int mMaxTranslationZ;
 
-    // Optimization, allows for quick lookup of task -> progress
-    HashMap<Task.TaskKey, Float> mTaskProgressMap = new HashMap<>();
+    // Optimization, allows for quick lookup of task -> index
+    private HashMap<Task.TaskKey, Integer> mTaskIndexMap = new HashMap<>();
 
     // The freeform workspace layout
     FreeformWorkspaceLayoutAlgorithm mFreeformLayoutAlgorithm;
 
-    // Log function
-    static ParametricCurve sCurve;
-
     public TaskStackLayoutAlgorithm(Context context) {
         Resources res = context.getResources();
+
+        mFocusedRange = new Range(res.getFloat(R.integer.recents_layout_focused_range_min),
+                res.getFloat(R.integer.recents_layout_focused_range_max));
+        mUnfocusedRange = new Range(res.getFloat(R.integer.recents_layout_unfocused_range_min),
+                res.getFloat(R.integer.recents_layout_unfocused_range_max));
+        mFocusedPeekHeight = res.getDimensionPixelSize(R.dimen.recents_layout_focused_peek_size);
+
         mMinTranslationZ = res.getDimensionPixelSize(R.dimen.recents_task_view_z_min);
         mMaxTranslationZ = res.getDimensionPixelSize(R.dimen.recents_task_view_z_max);
         mContext = context;
         mFreeformLayoutAlgorithm = new FreeformWorkspaceLayoutAlgorithm();
-        if (sCurve == null) {
-            sCurve = new ParametricCurve(new ParametricCurve.CurveFunction() {
-                // The large the XScale, the longer the flat area of the curve
-                private static final float XScale = 1.75f;
-                private static final float LogBase = 3000;
-
-                float reverse(float x) {
-                    return (-x * XScale) + 1;
-                }
-
-                @Override
-                public float f(float x) {
-                    return 1f - (float) (Math.pow(LogBase, reverse(x))) / (LogBase);
-                }
-
-                @Override
-                public float invF(float y) {
-                    return (float) (Math.log(1f - reverse(y)) / (-Math.log(LogBase) * XScale));
-                }
-            }, new ParametricCurve.ParametricCurveFunction() {
-                @Override
-                public float f(float p) {
-                    SystemServicesProxy ssp = Recents.getSystemServices();
-                    if (ssp.hasFreeformWorkspaceSupport()) {
-                        return 1f;
-                    }
-
-                    if (p < 0) return STACK_PEEK_MIN_SCALE;
-                    if (p > 1) return 1f;
-                    float scaleRange = (1f - STACK_PEEK_MIN_SCALE);
-                    float scale = STACK_PEEK_MIN_SCALE + (p * scaleRange);
-                    return scale;
-                }
-            });
-        }
     }
 
     /**
@@ -173,16 +200,30 @@
     }
 
     /**
+     * Sets the focused state.
+     */
+    public void setFocusState(float focusState) {
+        mFocusState = focusState;
+    }
+
+    /**
+     * Gets the focused state.
+     */
+    public float getFocusState() {
+        return mFocusState;
+    }
+
+    /**
      * 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);
+        Rect lastStackRect = new Rect(mCurrentStackRect);
 
         // The freeform height is the visible height (not including system insets) - padding above
         // freeform and below stack - gap between the freeform and stack
@@ -206,37 +247,24 @@
                 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,
-                mCurrentStackRect);
-        mBetweenAffiliationPOffset = sCurve.computePOffsetForScaledHeight(betweenAffiliationOffset,
-                mCurrentStackRect);
-        mTaskHeightPOffset = sCurve.computePOffsetForScaledHeight(mTaskRect.height(),
-                mCurrentStackRect);
-        mTaskHalfHeightPOffset = sCurve.computePOffsetForScaledHeight(mTaskRect.height() / 2,
-                mCurrentStackRect);
-        mStackBottomPOffset = sCurve.computePOffsetForHeight(mStackBottomOffset, mCurrentStackRect);
+        // Short circuit here if the stack rects haven't changed so we don't do all the work below
+        if (lastStackRect.equals(mCurrentStackRect)) {
+            return;
+        }
+
+        // Reinitialize the focused and unfocused curves
+        mUnfocusedCurve = constructUnfocusedCurve();
+        mUnfocusedCurveInterpolator = new FreePathInterpolator(mUnfocusedCurve);
+        mFocusedCurve = constructFocusedCurve();
+        mFocusedCurveInterpolator = new FreePathInterpolator(mFocusedCurve);
 
         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);
-
-            Log.d(TAG, "\tpWithinAffiliateOffset: " + mWithinAffiliationPOffset);
-            Log.d(TAG, "\tpBetweenAffiliateOffset: " + mBetweenAffiliationPOffset);
-            Log.d(TAG, "\tmTaskHeightPOffset: " + mTaskHeightPOffset);
-            Log.d(TAG, "\tmTaskHalfHeightPOffset: " + mTaskHalfHeightPOffset);
-            Log.d(TAG, "\tmStackBottomPOffset: " + mStackBottomPOffset);
-
-            Log.d(TAG, "\ty at p=0: " + sCurve.pToX(0f, mCurrentStackRect));
-            Log.d(TAG, "\ty at p=1: " + sCurve.pToX(1f, mCurrentStackRect));
         }
     }
 
@@ -248,7 +276,7 @@
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Clear the progress map
-        mTaskProgressMap.clear();
+        mTaskIndexMap.clear();
 
         // Return early if we have no tasks
         ArrayList<Task> tasks = stack.getTasks();
@@ -273,41 +301,28 @@
         mNumStackTasks = stackTasks.size();
         mNumFreeformTasks = freeformTasks.size();
 
-        float pAtBackMostTaskTop = 0;
-        float pAtFrontMostTaskTop = pAtBackMostTaskTop;
-        if (!stackTasks.isEmpty()) {
-            // Update the for each task from back to front.
-            int taskCount = stackTasks.size();
-            for (int i = 0; i < taskCount; i++) {
-                Task task = stackTasks.get(i);
-                mTaskProgressMap.put(task.key, pAtFrontMostTaskTop);
+        // 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 = stackTasks.size();
+        for (int i = 0; i < taskCount; i++) {
+            Task task = stackTasks.get(i);
+            mTaskIndexMap.put(task.key, i);
+        }
 
-                if (DEBUG) {
-                    Log.d(TAG, "Update: " + task.activityLabel + " p: " + pAtFrontMostTaskTop);
-                }
-
-                if (i < (taskCount - 1)) {
-                    // Increment the peek height
-                    float pPeek = task.group == null || task.group.isFrontMostTask(task) ?
-                            mBetweenAffiliationPOffset : mWithinAffiliationPOffset;
-                    pAtFrontMostTaskTop += pPeek;
-                }
-            }
-
-            mFrontMostTaskP = pAtFrontMostTaskTop;
-            if (mNumStackTasks > 1) {
-                // 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 + (ssp.hasFreeformWorkspaceSupport() ?
-                                mTaskHalfHeightPOffset : mTaskHeightPOffset));
-                // Basically align the back-most task such that the last two tasks would be visible
-                mMinScrollP = alignToStackBottom(pAtBackMostTaskTop,
-                        mStackBottomPOffset + (ssp.hasFreeformWorkspaceSupport() ?
-                                mTaskHalfHeightPOffset : mTaskHeightPOffset));
-            } else {
-                // When there is a single item, then just make all the stack progresses the same
+        // Calculate the min/max scroll
+        if (getDefaultFocusScroll() > 0f) {
+            mMinScrollP = 0;
+            mMaxScrollP = Math.max(mMinScrollP, mNumStackTasks - 1);
+        } else {
+            if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
                 mMinScrollP = mMaxScrollP = 0;
+            } else {
+                float bottomOffsetPct = (float) (mStackBottomOffset + mTaskRect.height()) /
+                        mCurrentStackRect.height();
+                float normX = mUnfocusedCurveInterpolator.getX(bottomOffsetPct);
+                mMinScrollP = 0;
+                mMaxScrollP = Math.max(mMinScrollP,
+                        (mNumStackTasks - 1) - mUnfocusedRange.getAbsoluteX(normX));
             }
         }
 
@@ -315,7 +330,20 @@
             mFreeformLayoutAlgorithm.update(freeformTasks, this);
             mInitialScrollP = mMaxScrollP;
         } else {
-            mInitialScrollP = Math.max(mMinScrollP, mMaxScrollP - mTaskHalfHeightPOffset);
+            if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
+                mInitialScrollP = mMinScrollP;
+            } else if (getDefaultFocusScroll() > 0f) {
+                RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
+                if (launchState.launchedFromHome) {
+                    mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 1);
+                } else {
+                    mInitialScrollP = Math.max(mMinScrollP, mNumStackTasks - 2);
+                }
+            } else {
+                float offsetPct = (float) (mTaskRect.height() / 2) / mCurrentStackRect.height();
+                float normX = mUnfocusedCurveInterpolator.getX(offsetPct);
+                mInitialScrollP = (mNumStackTasks - 1) - mUnfocusedRange.getAbsoluteX(normX);
+            }
         }
 
         if (DEBUG) {
@@ -327,8 +355,8 @@
     }
 
     /**
-     * Computes the maximum number of visible tasks and thumbnails.  Requires that
-     * update() is called first.
+     * Computes the maximum number of visible tasks and thumbnails when the scroll is at the initial
+     * stack scroll.  Requires that update() is called first.
      */
     public VisibilityReport computeStackVisibilityReport(ArrayList<Task> tasks) {
         // Ensure minimum visibility count
@@ -344,29 +372,32 @@
 
         // Otherwise, walk backwards in the stack and count the number of tasks and visible
         // thumbnails and add that to the total freeform task count
-        int taskHeight = mTaskRect.height();
+        TaskViewTransform tmpTransform = new TaskViewTransform();
+        Range currentRange = getDefaultFocusScroll() > 0f ? mFocusedRange : mUnfocusedRange;
+        currentRange.offset(mInitialScrollP);
         int taskBarHeight = mContext.getResources().getDimensionPixelSize(
                 R.dimen.recents_task_bar_height);
         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--) {
+        float prevScreenY = Integer.MAX_VALUE;
+        for (int i = tasks.size() - 1; i >= 0; i--) {
             Task task = tasks.get(i);
+
+            // Skip freeform
             if (task.isFreeformTask()) {
                 continue;
             }
 
-            progress = mTaskProgressMap.get(task.key) - mInitialScrollP;
-            if (progress < 0) {
-                break;
+            // Skip invisible
+            float taskProgress = getStackScrollForTask(task);
+            if (!currentRange.isInRange(taskProgress)) {
+                continue;
             }
+
             boolean isFrontMostTaskInGroup = task.group == null || task.group.isFrontMostTask(task);
             if (isFrontMostTaskInGroup) {
-                float scaleAtP = sCurve.pToScale(progress);
-                int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2);
-                int screenY = sCurve.pToX(progress, mCurrentStackRect) + scaleYOffsetAtP;
+                getStackTransform(taskProgress, mInitialScrollP, tmpTransform, null);
+                float screenY = tmpTransform.rect.top;
                 boolean hasVisibleThumbnail = (prevScreenY - screenY) > taskBarHeight;
                 if (hasVisibleThumbnail) {
                     numVisibleThumbnails++;
@@ -374,12 +405,12 @@
                     prevScreenY = screenY;
                 } else {
                     // Once we hit the next front most task that does not have a visible thumbnail,
-                    // w  alk through remaining visible set
+                    // walk through remaining visible set
                     for (int j = i; j >= 0; j--) {
                         numVisibleTasks++;
-                        progress = mTaskProgressMap.get(tasks.get(j).key) - mInitialScrollP;
-                        if (progress < 0) {
-                            break;
+                        taskProgress = getStackScrollForTask(tasks.get(j));
+                        if (!currentRange.isInRange(taskProgress)) {
+                            continue;
                         }
                     }
                     break;
@@ -397,86 +428,76 @@
      * is what the view is measured and laid out with.
      */
     public TaskViewTransform getStackTransform(Task task, float stackScroll,
-            TaskViewTransform transformOut, TaskViewTransform prevTransform) {
+            TaskViewTransform transformOut, TaskViewTransform frontTransform) {
         if (mFreeformLayoutAlgorithm.isTransformAvailable(task, this)) {
             mFreeformLayoutAlgorithm.getTransform(task, transformOut, this);
             return transformOut;
         } else {
             // Return early if we have an invalid index
-            if (task == null || !mTaskProgressMap.containsKey(task.key)) {
+            if (task == null || !mTaskIndexMap.containsKey(task.key)) {
                 transformOut.reset();
                 return transformOut;
             }
-            return getStackTransform(mTaskProgressMap.get(task.key), stackScroll, transformOut,
-                    prevTransform);
+            return getStackTransform(mTaskIndexMap.get(task.key), stackScroll, transformOut,
+                    frontTransform);
         }
     }
 
     /** Update/get the transform */
     public TaskViewTransform getStackTransform(float taskProgress, float stackScroll,
-            TaskViewTransform transformOut, TaskViewTransform prevTransform) {
+            TaskViewTransform transformOut, TaskViewTransform frontTransform) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
-        if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
-            // Center the task in the stack, changing the scale will not follow the curve, but just
-            // modulate some values directly
-            float pTaskRelative = mMinScrollP - stackScroll;
-            float scale = ssp.hasFreeformWorkspaceSupport() ? 1f : SINGLE_TASK_SCALE;
-            int topOffset = (mCurrentStackRect.top - mTaskRect.top) +
-                    (mCurrentStackRect.height() - mTaskRect.height()) / 2;
-            transformOut.scale = scale;
-            transformOut.translationX = (mStackRect.width() - mTaskRect.width()) / 2;
-            transformOut.translationY = (int) (topOffset + (pTaskRelative * mCurrentStackRect.height()));
-            transformOut.translationZ = mMaxTranslationZ;
-            transformOut.rect.set(mTaskRect);
-            transformOut.rect.offset(transformOut.translationX, transformOut.translationY);
-            Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
-            transformOut.visible = true;
-            transformOut.p = pTaskRelative;
-            return transformOut;
+        // Compute the focused and unfocused offset
+        mUnfocusedRange.offset(stackScroll);
+        float p = mUnfocusedRange.getNormalizedX(taskProgress);
+        float yp = mUnfocusedCurveInterpolator.getInterpolation(p);
+        float unfocusedP = p;
+        int unFocusedY = (int) (Math.max(0f, (1f - yp)) * mCurrentStackRect.height());
+        int focusedY = 0;
+        if (mFocusState > 0f) {
+            mFocusedRange.offset(stackScroll);
+            p = mFocusedRange.getNormalizedX(taskProgress);
+            yp = mFocusedCurveInterpolator.getInterpolation(p);
+            focusedY = (int) (Math.max(0f, (1f - yp)) * mCurrentStackRect.height());
+        }
 
-        } else {
-            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) {
+        // Skip calculating this if it is identical to the task in front of it
+        if (frontTransform != null) {
+            if (Float.compare(p, frontTransform.p) == 0) {
                 transformOut.reset();
-                transformOut.rect.set(mTaskRect);
                 return transformOut;
             }
-            // The check for the top is trickier, since we want to show the next task if it is at
-            // all visible, even if p < 0.
-            if (pTaskRelative < 0f) {
-                if (prevTransform != null && Float.compare(prevTransform.p, 0f) <= 0) {
-                    transformOut.reset();
-                    transformOut.rect.set(mTaskRect);
-                    return transformOut;
-                }
-            }
-            float scale = sCurve.pToScale(pBounded);
-            int scaleYOffset = (int) (((1f - scale) * mTaskRect.height()) / 2);
-            transformOut.scale = scale;
-            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(transformOut.translationX, transformOut.translationY);
-            Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
-            transformOut.visible = true;
-            transformOut.p = pTaskRelative;
-            if (DEBUG) {
-                Log.d(TAG, "\t" + transformOut);
-            }
-
-            return transformOut;
         }
+
+        if (!ssp.hasFreeformWorkspaceSupport() && mNumStackTasks == 1) {
+            // When there is exactly one task, then decouple the task from the stack and just move
+            // in screen space
+            p = (mMinScrollP - stackScroll) / mNumStackTasks;
+            int centerYOffset = (mCurrentStackRect.top - mTaskRect.top) +
+                    (mCurrentStackRect.height() - mTaskRect.height()) / 2;
+            transformOut.translationY = (int) (centerYOffset +
+                    (p * mCurrentStackRect.height()));
+            transformOut.translationZ = mMaxTranslationZ;
+            transformOut.rect.set(mTaskRect);
+            transformOut.p = p;
+
+        } else {
+            // Otherwise, update the task to the stack layout
+            int y = unFocusedY + (int) (mFocusState * (focusedY - unFocusedY));
+            transformOut.translationY = (mCurrentStackRect.top - mTaskRect.top) + y;
+            transformOut.translationZ = Math.max(mMinTranslationZ, Math.min(mMaxTranslationZ,
+                    mMinTranslationZ + (p * (mMaxTranslationZ - mMinTranslationZ))));
+            transformOut.rect.set(mTaskRect);
+            transformOut.p = unfocusedP;
+        }
+
+        transformOut.scale = 1f;
+        transformOut.translationX = (mCurrentStackRect.width() - mTaskRect.width()) / 2;
+        transformOut.rect.offset(transformOut.translationX, transformOut.translationY);
+        Utilities.scaleRectAboutCenter(transformOut.rect, transformOut.scale);
+        transformOut.visible = true;
+        return transformOut;
     }
 
     /**
@@ -491,8 +512,8 @@
      * stack.
      */
     float getStackScrollForTask(Task t) {
-        if (!mTaskProgressMap.containsKey(t.key)) return 0f;
-        return mTaskProgressMap.get(t.key);
+        if (!mTaskIndexMap.containsKey(t.key)) return 0f;
+        return mTaskIndexMap.get(t.key);
     }
 
     /**
@@ -501,7 +522,8 @@
      * screen along the arc-length proportionally (1/arclength).
      */
     public float getDeltaPForY(int downY, int y) {
-        float deltaP = (float) (y - downY) / mCurrentStackRect.height() * (1f / sCurve.getArcLength());
+        float deltaP = (float) (y - downY) / mCurrentStackRect.height() *
+                mUnfocusedCurveInterpolator.getArcLength();
         return -deltaP;
     }
 
@@ -510,18 +532,66 @@
      * of the curve, map back to the screen y.
      */
     public int getYForDeltaP(float downScrollP, float p) {
-        int y = (int) ((p - downScrollP) * mCurrentStackRect.height() * sCurve.getArcLength());
+        int y = (int) ((p - downScrollP) * mCurrentStackRect.height() *
+                (1f / mUnfocusedCurveInterpolator.getArcLength()));
         return -y;
     }
 
-    private float alignToStackTop(float p) {
-        // At scroll progress == p, then p is at the top of the stack
+    /**
+     * Returns the default focus state.
+     */
+    private float getDefaultFocusScroll() {
+        return 0f;
+    }
+
+    /**
+     * Creates a new path for the focused curve.
+     */
+    private Path constructFocusedCurve() {
+        SystemServicesProxy ssp = Recents.getSystemServices();
+
+        // Initialize the focused curve. This curve is a piecewise curve composed of several
+        // quadradic beziers that goes from (0,1) through (0.5, peek height offset),
+        // (0.667, next task offset), (0.833, bottom task offset), and (1,0).
+        float peekHeightPct = 0f;
+        if (!ssp.hasFreeformWorkspaceSupport()) {
+            peekHeightPct = (float) mFocusedPeekHeight / mCurrentStackRect.height();
+        }
+        Path p = new Path();
+        p.moveTo(0f, 1f);
+        p.lineTo(0.5f, 1f - peekHeightPct);
+        p.lineTo(0.66666667f, 0.25f);
+        p.lineTo(0.83333333f, 0.1f);
+        p.lineTo(1f, 0f);
         return p;
     }
 
-    private float alignToStackBottom(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);
+    /**
+     * Creates a new path for the unfocused curve.
+     */
+    private Path constructUnfocusedCurve() {
+        SystemServicesProxy ssp = Recents.getSystemServices();
+
+        // Initialize the unfocused curve. This curve is a piecewise curve composed of two quadradic
+        // beziers that goes from (0,1) through (0.5, peek height offset) and ends at (1,0).  This
+        // ensures that we match the range, at which 0.5 represents the stack scroll at the current
+        // task progress.  Because the height offset can change depending on a resource, we compute
+        // the control point of the second bezier such that between it and a first known point,
+        // there is a tangent at (0.5, peek height offset).
+        float cpoint1X = 0.4f;
+        float cpoint1Y = 1f;
+        float peekHeightPct = 0f;
+        if (!ssp.hasFreeformWorkspaceSupport()) {
+            peekHeightPct = (float) mFocusedPeekHeight / mCurrentStackRect.height();
+        }
+        float slope = ((1f - peekHeightPct) - cpoint1Y) / (0.5f - cpoint1X);
+        float b = 1f - slope * cpoint1X;
+        float cpoint2X = 0.75f;
+        float cpoint2Y = slope * cpoint2X + b;
+        Path p = new Path();
+        p.moveTo(0f, 1f);
+        p.cubicTo(0f, 1f, cpoint1X, 1f, 0.5f, 1f - peekHeightPct);
+        p.cubicTo(0.5f, 1f - peekHeightPct, cpoint2X, cpoint2Y, 1f, 0f);
+        return p;
     }
 }
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 7250d6a..219e9fd 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/views/TaskStackView.java
@@ -353,7 +353,7 @@
         }
 
         // Update the stack transforms
-        TaskViewTransform prevTransform = null;
+        TaskViewTransform frontTransform = null;
         for (int i = taskCount - 1; i >= 0; i--) {
             Task task = tasks.get(i);
             if (task.isFreeformTask()) {
@@ -361,7 +361,7 @@
             }
 
             TaskViewTransform transform = mLayoutAlgorithm.getStackTransform(task, stackScroll,
-                    taskTransforms.get(i), prevTransform);
+                    taskTransforms.get(i), frontTransform);
             if (DEBUG) {
                 Log.d(TAG, "updateStackTransform: " + i + ", " + transform.visible);
             }
@@ -386,7 +386,7 @@
                 transform.translationY = Math.min(transform.translationY,
                         mLayoutAlgorithm.mCurrentStackRect.bottom);
             }
-            prevTransform = transform;
+            frontTransform = transform;
         }
         if (visibleRangeOut != null) {
             visibleRangeOut[0] = frontMostVisibleIndex;