Change the task layout to better match mocks, add "in" animation

This only adds an animation when transitioning into recents. "Out"
animations will come later as they need to be run in parallel with
launching the activity not to introduce any latency.

Test: Open recents on sw600dp device, check recents layout/animations
Bug: 32101881
Change-Id: I367f8e5c106cd06d2a7833c165ecb960a7821ed9
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 0b5383a..d232e00 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -297,7 +297,7 @@
                   android:screenOrientation="behind"
                   android:resizeableActivity="true"
                   android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
-                  android:theme="@style/RecentsTheme.Wallpaper">
+                  android:theme="@style/RecentsTheme.Grid">
             <intent-filter>
                 <action android:name="com.android.systemui.recents.TOGGLE_RECENTS" />
             </intent-filter>
diff --git a/packages/SystemUI/res/layout-sw600dp/recents_grid.xml b/packages/SystemUI/res/layout-sw600dp/recents_grid.xml
index cff770a..1a054a5 100644
--- a/packages/SystemUI/res/layout-sw600dp/recents_grid.xml
+++ b/packages/SystemUI/res/layout-sw600dp/recents_grid.xml
@@ -13,22 +13,11 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:paddingTop="24dp"
-    android:paddingBottom="54dp"
-    android:orientation="vertical"
-    android:id="@+id/recents_container"
+    android:id="@+id/recents_view"
+    android:gravity="center"
     android:background="#99000000">
     <include layout="@layout/recents_stack_action_button" />
-    <FrameLayout
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:id="@+id/recents_view"
-        android:layout_marginLeft="12dp"
-        android:layout_marginTop="10dp"
-        android:layout_marginRight="12dp"
-        android:gravity="center">
-    </FrameLayout>
-</LinearLayout>
\ No newline at end of file
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/recents_stack_action_button.xml b/packages/SystemUI/res/layout/recents_stack_action_button.xml
index 541000b..34b4e17 100644
--- a/packages/SystemUI/res/layout/recents_stack_action_button.xml
+++ b/packages/SystemUI/res/layout/recents_stack_action_button.xml
@@ -33,4 +33,5 @@
     android:fontFamily="sans-serif-medium"
     android:background="@drawable/recents_stack_action_background"
     android:visibility="invisible"
-    android:forceHasOverlappingRendering="false" />
+    android:forceHasOverlappingRendering="false"
+    style="?attr/clearAllStyle" />
diff --git a/packages/SystemUI/res/values/attrs.xml b/packages/SystemUI/res/values/attrs.xml
index ad65f82..a229866 100644
--- a/packages/SystemUI/res/values/attrs.xml
+++ b/packages/SystemUI/res/values/attrs.xml
@@ -30,6 +30,8 @@
     </declare-styleable>
     <declare-styleable name="RecentsPanelView">
         <attr name="recentItemLayout" format="reference" />
+        <!-- Style for the "Clear all" button. -->
+        <attr name="clearAllStyle" format="reference" />
     </declare-styleable>
     <declare-styleable name="DeadZone">
         <attr name="minSize" format="dimension" />
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 12f7881..2f7174a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -697,4 +697,19 @@
 
     <!-- The size of the PIP dismiss target. -->
     <dimen name="pip_dismiss_target_size">48dp</dimen>
+
+    <!-- Values specific to grid-based recents. -->
+    <!-- Margins around recent tasks. -->
+    <dimen name="recents_grid_margin_left">15dp</dimen>
+    <dimen name="recents_grid_margin_top">70dp</dimen>
+    <dimen name="recents_grid_margin_right">15dp</dimen>
+    <dimen name="recents_grid_margin_bottom">90dp</dimen>
+    <!-- Margins around the "Clear all" button. -->
+    <dimen name="recents_grid_clear_all_margin_left">0dp</dimen>
+    <dimen name="recents_grid_clear_all_margin_top">30dp</dimen>
+    <dimen name="recents_grid_clear_all_margin_right">15dp</dimen>
+    <dimen name="recents_grid_clear_all_margin_bottom">0dp</dimen>
+    <!-- Padding in between task views. -->
+    <dimen name="recents_grid_inter_task_padding">15dp</dimen>
+
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 6661f07..78fc9f9 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -34,6 +34,30 @@
         <item name="android:colorBackgroundCacheHint">@null</item>
         <item name="android:windowShowWallpaper">true</item>
         <item name="android:windowDisablePreview">true</item>
+        <item name="clearAllStyle">@style/ClearAllButtonDefaultMargins</item>
+    </style>
+
+    <style name="ClearAllButtonDefaultMargins">
+        <item name="android:layout_marginStart">0dp</item>
+        <item name="android:layout_marginTop">0dp</item>
+        <item name="android:layout_marginEnd">0dp</item>
+        <item name="android:layout_marginBottom">0dp</item>
+    </style>
+
+    <!-- Grid-based Recents theme. -->
+    <style name="RecentsTheme.Grid">
+        <item name="android:windowBackground">@color/transparent</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowShowWallpaper">true</item>
+        <item name="android:windowDisablePreview">true</item>
+        <item name="clearAllStyle">@style/ClearAllButtonLargeMargins</item>
+    </style>
+
+    <style name="ClearAllButtonLargeMargins">
+        <item name="android:layout_marginStart">@dimen/recents_grid_clear_all_margin_left</item>
+        <item name="android:layout_marginTop">@dimen/recents_grid_clear_all_margin_top</item>
+        <item name="android:layout_marginEnd">@dimen/recents_grid_clear_all_margin_right</item>
+        <item name="android:layout_marginBottom">@dimen/recents_grid_clear_all_margin_bottom</item>
     </style>
 
     <!-- Performance optimized Recents theme (no wallpaper) -->
diff --git a/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java b/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java
index e1e654d5..71c2148 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/grid/RecentsGridActivity.java
@@ -20,16 +20,22 @@
 import android.app.Activity;
 import android.content.Intent;
 import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.Bundle;
+import android.util.DisplayMetrics;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.ScaleAnimation;
+import android.view.animation.TranslateAnimation;
 import android.widget.FrameLayout;
-import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import com.android.internal.logging.MetricsLogger;
@@ -61,17 +67,23 @@
 import com.android.systemui.recents.views.TaskView;
 
 import java.util.ArrayList;
-import java.util.List;
+import java.util.Arrays;
 
 /**
  * The main grid recents activity started by the RecentsImpl.
  */
-public class RecentsGridActivity extends Activity implements ViewTreeObserver.OnPreDrawListener {
+public class RecentsGridActivity extends Activity {
+    public final static int MAX_VISIBLE_TASKS = 9;
+
     private final static String TAG = "RecentsGridActivity";
+    private final static int TITLE_BAR_HEIGHT_DP = 64;
+
+    private ArrayList<Integer> mMargins = new ArrayList<>();
 
     private TaskStack mTaskStack;
-    private List<Task> mTasks = new ArrayList<>();
-    private List<TaskView> mTaskViews = new ArrayList<>();
+    private ArrayList<Task> mTasks = new ArrayList<>();
+    private ArrayList<TaskView> mTaskViews = new ArrayList<>();
+    private ArrayList<Rect> mTaskViewRects;
     private FrameLayout mRecentsView;
     private TextView mEmptyView;
     private View mClearAllButton;
@@ -80,6 +92,10 @@
     private Rect mDisplayRect = new Rect();
     private LayoutInflater mInflater;
     private boolean mTouchExplorationEnabled;
+    private Point mScreenSize;
+    private int mTitleBarHeightPx;
+    private int mStatusBarHeightPx;
+    private int mNavigationBarHeightPx;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -87,12 +103,28 @@
         setContentView(R.layout.recents_grid);
         SystemServicesProxy ssp = Recents.getSystemServices();
 
+        Resources res = getResources();
+        Integer[] margins = {
+                res.getDimensionPixelSize(R.dimen.recents_grid_margin_left),
+                res.getDimensionPixelSize(R.dimen.recents_grid_margin_top),
+                res.getDimensionPixelSize(R.dimen.recents_grid_margin_right),
+                res.getDimensionPixelSize(R.dimen.recents_grid_margin_bottom),
+        };
+        mMargins.addAll(Arrays.asList(margins));
+
         mInflater = LayoutInflater.from(this);
         Configuration appConfiguration = Utilities.getAppConfiguration(this);
         mDisplayRect = ssp.getDisplayRect();
         mLastDisplayOrientation = appConfiguration.orientation;
         mLastDisplayDensity = appConfiguration.densityDpi;
         mTouchExplorationEnabled = ssp.isTouchExplorationEnabled();
+        mScreenSize = new Point();
+        getWindowManager().getDefaultDisplay().getRealSize(mScreenSize);
+        DisplayMetrics metrics = res.getDisplayMetrics();
+        mTitleBarHeightPx = (int) (TITLE_BAR_HEIGHT_DP *
+                ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
+        mStatusBarHeightPx = res.getDimensionPixelSize(R.dimen.status_bar_height);
+        mNavigationBarHeightPx = res.getDimensionPixelSize(R.dimen.navigation_bar_height);
 
         mRecentsView = (FrameLayout) findViewById(R.id.recents_view);
         mRecentsView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
@@ -100,8 +132,7 @@
                 View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
         getWindow().getAttributes().privateFlags |=
                 WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
-        LinearLayout recentsContainer = (LinearLayout) findViewById(R.id.recents_container);
-        mEmptyView = (TextView) mInflater.inflate(R.layout.recents_empty, recentsContainer, false);
+        mEmptyView = (TextView) mInflater.inflate(R.layout.recents_empty, mRecentsView, false);
         mClearAllButton = findViewById(R.id.button);
 
         FrameLayout.LayoutParams emptyViewLayoutParams = new FrameLayout.LayoutParams(
@@ -111,8 +142,8 @@
         mRecentsView.addView(mEmptyView);
 
         mClearAllButton.setVisibility(View.VISIBLE);
-        LinearLayout.LayoutParams lp =
-                (LinearLayout.LayoutParams) mClearAllButton.getLayoutParams();
+        FrameLayout.LayoutParams lp =
+                (FrameLayout.LayoutParams) mClearAllButton.getLayoutParams();
         lp.gravity = Gravity.END;
 
         mClearAllButton.setOnClickListener(v -> {
@@ -154,6 +185,57 @@
         return null;
     }
 
+    /**
+     * Starts animations for each task view to either enlarge it to the size of the screen (when
+     * launching a task), or (if {@code reverse} is true, to reduce it from the size of the screen
+     * back to its place in the recents layout (when opening recents).
+     * @param animationListener An animation listener for executing code before or after the
+     *         animations run.
+     * @param reverse Whether the blow-up animations should be run in reverse.
+     */
+    private void startBlowUpAnimations(Animation.AnimationListener animationListener,
+            boolean reverse) {
+        if (mTaskViews.size() == 0) {
+            return;
+        }
+        int screenWidth = mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
+                ? mScreenSize.x : mScreenSize.y;
+        int screenHeight = mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
+                ? mScreenSize.y : mScreenSize.x;
+        screenHeight -= mStatusBarHeightPx + mNavigationBarHeightPx;
+        for (int i = 0; i < mTaskViews.size(); i++) {
+            View tv = mTaskViews.get(i);
+            AnimationSet animations = new AnimationSet(true /* shareInterpolator */);
+            animations.setInterpolator(new DecelerateInterpolator());
+            if (i == 0 && animationListener != null) {
+                animations.setAnimationListener(animationListener);
+            }
+            animations.setFillBefore(reverse);
+            animations.setFillAfter(!reverse);
+            Rect initialRect = mTaskViewRects.get(mTaskViewRects.size() - 1 - i);
+            int xDelta = - initialRect.left;
+            int yDelta = - initialRect.top - mTitleBarHeightPx + mStatusBarHeightPx;
+            TranslateAnimation translate = new TranslateAnimation(
+                    reverse ? xDelta : 0, reverse ? 0 : xDelta,
+                    reverse ? yDelta : 0, reverse ? 0 : yDelta);
+            translate.setDuration(250);
+            animations.addAnimation(translate);
+
+
+            float xScale = (float) screenWidth / (float) initialRect.width();
+            float yScale = (float) screenHeight /
+                    ((float) initialRect.height() - mTitleBarHeightPx);
+            ScaleAnimation scale = new ScaleAnimation(
+                    reverse ? xScale : 1, reverse ? 1 : xScale,
+                    reverse ? yScale : 1, reverse ? 1 : yScale,
+                    Animation.ABSOLUTE, 0, Animation.ABSOLUTE, mStatusBarHeightPx);
+            scale.setDuration(300);
+            animations.addAnimation(scale);
+
+            tv.startAnimation(animations);
+        }
+    }
+
     private void updateControlVisibility() {
         boolean empty = (mTasks.size() == 0);
         mClearAllButton.setVisibility(empty ? View.INVISIBLE : View.VISIBLE);
@@ -163,7 +245,7 @@
         }
     }
 
-    private void updateRecentsTasks() {
+    private void updateModel() {
         RecentsTaskLoader loader = Recents.getTaskLoader();
         RecentsTaskLoadPlan plan = RecentsImpl.consumeInstanceLoadPlan();
         if (plan == null) {
@@ -174,29 +256,66 @@
         if (!plan.hasTasks()) {
             loader.preloadTasks(plan, -1, !launchState.launchedFromHome);
         }
-        int numVisibleTasks = 9;
         mTaskStack = plan.getTaskStack();
         RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options();
         loadOpts.runningTaskId = launchState.launchedToTaskId;
-        loadOpts.numVisibleTasks = numVisibleTasks;
-        loadOpts.numVisibleTaskThumbnails = numVisibleTasks;
+        loadOpts.numVisibleTasks = MAX_VISIBLE_TASKS;
+        loadOpts.numVisibleTaskThumbnails = MAX_VISIBLE_TASKS;
         loader.loadTasks(this, plan, loadOpts);
 
         mTasks = mTaskStack.getStackTasks();
+    }
 
-        updateControlVisibility();
-
-        clearTaskViews();
+    private void updateViews() {
+        int screenWidth = mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
+                ? mScreenSize.x : mScreenSize.y;
+        int screenHeight = mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
+                ? mScreenSize.y : mScreenSize.x;
+        int paddingPixels = getResources().getDimensionPixelSize(
+                R.dimen.recents_grid_inter_task_padding);
+        mTaskViewRects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
+                mTasks.size(), screenWidth, screenHeight, getAppRectRatio(), paddingPixels,
+                mMargins, mTitleBarHeightPx);
+        boolean recycleViews = (mTaskViews.size() == mTasks.size());
+        if (!recycleViews) {
+            clearTaskViews();
+        }
         for (int i = 0; i < mTasks.size(); i++) {
             Task task = mTasks.get(i);
-            TaskView taskView = createView();
+            // We keep the same ordering in the model as other Recents flavors (older tasks are
+            // first in the stack) so that the logic can be similar, but we reverse the order
+            // when placing views on the screen so that most recent tasks are displayed first.
+            Rect rect = mTaskViewRects.get(mTaskViewRects.size() - 1 - i);
+            TaskView taskView;
+            if (recycleViews) {
+                taskView = mTaskViews.get(i);
+            } else {
+                taskView = createView();
+            }
             taskView.onTaskBound(task, mTouchExplorationEnabled, mLastDisplayOrientation,
                     mDisplayRect);
             Recents.getTaskLoader().loadTaskData(task);
             taskView.setTouchEnabled(true);
             // Show dismiss button right away.
             taskView.startNoUserInteractionAnimation();
-            mTaskViews.add(taskView);
+            taskView.setLayoutParams(new FrameLayout.LayoutParams(rect.width(), rect.height()));
+            taskView.setTranslationX(rect.left);
+            taskView.setTranslationY(rect.top);
+            if (!recycleViews) {
+                mRecentsView.addView(taskView);
+                mTaskViews.add(taskView);
+            }
+        }
+        updateControlVisibility();
+    }
+
+    private float getAppRectRatio() {
+        if (mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+            return (float) mScreenSize.x /
+                    (float) (mScreenSize.y - mStatusBarHeightPx - mNavigationBarHeightPx);
+        } else {
+            return (float) mScreenSize.y /
+                    (float) (mScreenSize.x - mStatusBarHeightPx - mNavigationBarHeightPx);
         }
     }
 
@@ -204,13 +323,23 @@
     protected void onStart() {
         super.onStart();
         EventBus.getDefault().send(new RecentsVisibilityChangedEvent(this, true));
-        mRecentsView.getViewTreeObserver().addOnPreDrawListener(this);
-    }
+        updateModel();
+        updateViews();
+        if (mTaskViews.size() > 0) {
+            mTaskViews.get(mTaskViews.size() - 1).bringToFront();
+        }
+        startBlowUpAnimations(new Animation.AnimationListener() {
+            @Override
+            public void onAnimationStart(Animation animation) { }
 
-    @Override
-    public void onResume() {
-        super.onResume();
-        updateRecentsTasks();
+            @Override
+            public void onAnimationEnd(Animation animation) {
+                updateViews();
+            }
+
+            @Override
+            public void onAnimationRepeat(Animation animation) { }
+        }, true /* reverse */);
     }
 
     @Override
@@ -237,37 +366,13 @@
         // Notify of the config change.
         Configuration newDeviceConfiguration = Utilities.getAppConfiguration(this);
         mDisplayRect = Recents.getSystemServices().getDisplayRect();
-        mRecentsView.getViewTreeObserver().addOnPreDrawListener(this);
-        mRecentsView.requestLayout();
         int numStackTasks = mTaskStack.getStackTaskCount();
         EventBus.getDefault().send(new ConfigurationChangedEvent(false /* fromMultiWindow */,
                 mLastDisplayOrientation != newDeviceConfiguration.orientation,
                 mLastDisplayDensity != newDeviceConfiguration.densityDpi, numStackTasks > 0));
         mLastDisplayOrientation = newDeviceConfiguration.orientation;
         mLastDisplayDensity = newDeviceConfiguration.densityDpi;
-    }
-
-    @Override
-    public boolean onPreDraw() {
-        mRecentsView.getViewTreeObserver().removeOnPreDrawListener(this);
-        int width = mRecentsView.getWidth();
-        int height = mRecentsView.getHeight();
-
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-            mTasks.size(), width, height, false /* allowLineOfThree */, 30 /* padding */);
-        removeTaskViews();
-        for (int i = 0; i < rects.size(); i++) {
-            Rect rect = rects.get(i);
-            // We keep the same ordering in the model as other Recents flavors (older tasks are
-            // first in the stack) so that the logic can be similar, but we reverse the order
-            // when placing views on the screen so that most recent tasks are displayed first.
-            View taskView = mTaskViews.get(rects.size() - 1 - i);
-            taskView.setLayoutParams(new FrameLayout.LayoutParams(rect.width(), rect.height()));
-            taskView.setTranslationX(rect.left);
-            taskView.setTranslationY(rect.top);
-            mRecentsView.addView(taskView);
-        }
-        return true;
+        updateViews();
     }
 
     void dismissRecentsToHome() {
@@ -360,7 +465,8 @@
             EventBus.getDefault().send(new DeleteTaskDataEvent(tasks.get(i)));
         }
         mTasks = new ArrayList<>();
-        updateRecentsTasks();
+        updateModel();
+        updateViews();
 
         MetricsLogger.action(this, MetricsEvent.OVERVIEW_DISMISS_ALL);
     }
@@ -390,6 +496,9 @@
     }
 
     public final void onBusEvent(LaunchTaskEvent event) {
+        event.taskView.bringToFront();
         startActivity(event.task.key.baseIntent);
+        // Eventually we should start blow-up animations here, but we need to make sure it's done
+        // in parallel with starting the activity so that we don't introduce unneeded latency.
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithm.java b/packages/SystemUI/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithm.java
index 057aa54..648f2f0 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithm.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithm.java
@@ -19,66 +19,111 @@
 import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 class TaskGridLayoutAlgorithm {
 
-    private static final String TAG = "TaskGridLayoutAlgorithm";
-
-    static List<Rect> getRectsForTaskCount(int count, int containerWidth, int containerHeight,
-            boolean allowLineOfThree, int padding) {
-        return getRectsForTaskCount(count, containerWidth, containerHeight, allowLineOfThree,
-                padding, null);
+    public enum VerticalGravity {
+        START, END, CENTER
     }
 
-    static List<Rect> getRectsForTaskCount(int count, int containerWidth, int containerHeight,
-            boolean allowLineOfThree, int padding, Rect preCalculatedTile) {
-        int singleLineMaxCount = allowLineOfThree ? 3 : 2;
-        List<Rect> rects = new ArrayList<>(count);
+    public static final List<Integer> ZERO_MARGIN = new ArrayList<>();
+    static {
+        Integer[] zero = {0, 0, 0, 0};
+        ZERO_MARGIN.addAll(Arrays.asList(zero));
+    }
+    private static final String TAG = "TaskGridLayoutAlgorithm";
+
+    /**
+     * Calculates the adequate rectangles for the specified number of tasks to be layed out on
+     * the screen.
+     * @param count The number of task views to layout.
+     * @param containerWidth The width of the whole area containing those tasks.
+     * @param containerHeight The height of the whole area containing those tasks.
+     * @param screenRatio The ratio of the device's screen, so that tasks have the same aspect
+     *         ratio (ignoring the title bar).
+     * @param padding The amount of padding, in pixels, in between task views.
+     * @param margins The amount of space to be left blank around the area on the left, top, right
+     *         and bottom.
+     * @param titleBarHeight The height, in pixels, of the task views title bar.
+     * @return A list of rectangles to be used for layout.
+     */
+    static ArrayList<Rect> getRectsForTaskCount(int count, int containerWidth, int containerHeight,
+            float screenRatio, int padding, List<Integer> margins, int titleBarHeight) {
+        return getRectsForTaskCount(count, containerWidth, containerHeight,  screenRatio, padding,
+                margins, titleBarHeight, null, VerticalGravity.CENTER);
+    }
+
+    private static ArrayList<Rect> getRectsForTaskCount(int count, int containerWidth,
+            int containerHeight, float screenRatio, int padding, List<Integer> margins,
+            int titleBarHeight, Rect preCalculatedTile, VerticalGravity gravity) {
+        ArrayList<Rect> rects = new ArrayList<>(count);
         boolean landscape = (containerWidth > containerHeight);
+        containerWidth -= margins.get(0) + margins.get(2);
+        containerHeight -= margins.get(1) + margins.get(3);
 
         // We support at most 9 tasks in this layout.
-        count = Math.min(count, 9);
+        count = Math.min(count, RecentsGridActivity.MAX_VISIBLE_TASKS);
 
         if (count == 0) {
             return rects;
         }
-        if (count <= singleLineMaxCount) {
-            if (landscape) {
-                // Single line.
-                int taskWidth = 0;
-                int emptySpace = 0;
-                if (preCalculatedTile != null) {
-                    taskWidth = preCalculatedTile.width();
-                    emptySpace = containerWidth - (count * taskWidth) - (count - 1) * padding;
-                } else {
-                    // Divide available space in equal parts.
-                    taskWidth = (containerWidth - (count - 1) * padding) / count;
-                }
-                for (int i = 0; i < count; i++) {
-                    int left = emptySpace / 2 + i * taskWidth + i * padding;
-                    rects.add(new Rect(left, 0, left + taskWidth, containerHeight));
-                }
+        if (count <= 3) {
+            // Base case: single line.
+            int taskWidth, taskHeight;
+            if (preCalculatedTile != null) {
+                taskWidth = preCalculatedTile.width();
+                taskHeight = preCalculatedTile.height();
             } else {
-                // Single column. Divide available space in equal parts.
-                int taskHeight = (containerHeight - (count - 1) * padding) / count;
-                for (int i = 0; i < count; i++) {
-                    int top = i * taskHeight + i * padding;
-                    rects.add(new Rect(0, top, containerWidth, top + taskHeight));
+                // Divide available width in equal parts.
+                int maxTaskWidth = (containerWidth - (count - 1) * padding) / count;
+                int maxTaskHeight = containerHeight;
+                if (maxTaskHeight >= maxTaskWidth / screenRatio + titleBarHeight) {
+                    // Width bound.
+                    taskWidth = maxTaskWidth;
+                    taskHeight = (int) (maxTaskWidth / screenRatio + titleBarHeight);
+                } else {
+                    // Height bound.
+                    taskHeight = maxTaskHeight;
+                    taskWidth = (int) ((taskHeight - titleBarHeight) * screenRatio);
                 }
             }
+            int emptySpaceX = containerWidth - (count * taskWidth) - (count - 1) * padding;
+            int emptySpaceY = containerHeight - taskHeight;
+            for (int i = 0; i < count; i++) {
+                int left = emptySpaceX / 2 + i * taskWidth + i * padding;
+                int top;
+                switch (gravity) {
+                    case CENTER:
+                        top = emptySpaceY / 2;
+                        break;
+                    case END:
+                        top = emptySpaceY;
+                        break;
+                    case START:
+                    default:
+                        top = 0;
+                        break;
+                }
+                Rect rect = new Rect(left, top, left + taskWidth, top + taskHeight);
+                rect.offset(margins.get(0), margins.get(1));
+                rects.add(rect);
+            }
         } else if (count < 7) {
             // Two lines.
             int lineHeight = (containerHeight - padding) / 2;
             int lineTaskCount = (int) Math.ceil((double) count / 2);
-            List<Rect> rectsA = getRectsForTaskCount(
-                    lineTaskCount, containerWidth, lineHeight, true /* allowLineOfThree */, padding,
-                            null);
-            List<Rect> rectsB = getRectsForTaskCount(
-                    count - lineTaskCount, containerWidth, lineHeight, true /* allowLineOfThree */,
-                            padding, rectsA.get(0));
-            for (Rect rect : rectsB) {
-                rect.offset(0, lineHeight + padding);
+            List<Rect> rectsA = getRectsForTaskCount(lineTaskCount, containerWidth, lineHeight,
+                    screenRatio, padding, ZERO_MARGIN, titleBarHeight, null, VerticalGravity.END);
+            List<Rect> rectsB = getRectsForTaskCount(count - lineTaskCount, containerWidth,
+                    lineHeight, screenRatio, padding, ZERO_MARGIN, titleBarHeight, rectsA.get(0),
+                    VerticalGravity.START);
+            for (int i = 0; i < rectsA.size(); i++) {
+                rectsA.get(i).offset(margins.get(0), margins.get(1));
+            }
+            for (int i = 0; i < rectsB.size(); i++) {
+                rectsB.get(i).offset(margins.get(0), margins.get(1) + lineHeight + padding);
             }
             rects.addAll(rectsA);
             rects.addAll(rectsB);
@@ -86,19 +131,22 @@
             // Three lines.
             int lineHeight = (containerHeight - 2 * padding) / 3;
             int lineTaskCount = (int) Math.ceil((double) count / 3);
-            List<Rect> rectsA = getRectsForTaskCount(
-                lineTaskCount, containerWidth, lineHeight, true /* allowLineOfThree */, padding, null);
-            List<Rect> rectsB = getRectsForTaskCount(
-                lineTaskCount, containerWidth, lineHeight, true /* allowLineOfThree */, padding,
-                        rectsA.get(0));
-            List<Rect> rectsC = getRectsForTaskCount(
-                count - (2 * lineTaskCount), containerWidth, lineHeight,
-                         true /* allowLineOfThree */, padding, rectsA.get(0));
-            for (Rect rect : rectsB) {
-                rect.offset(0, lineHeight + padding);
+            List<Rect> rectsA = getRectsForTaskCount(lineTaskCount, containerWidth, lineHeight,
+                    screenRatio, padding, ZERO_MARGIN, titleBarHeight, null, VerticalGravity.END);
+            List<Rect> rectsB = getRectsForTaskCount(lineTaskCount, containerWidth, lineHeight,
+                    screenRatio,  padding, ZERO_MARGIN, titleBarHeight, rectsA.get(0),
+                    VerticalGravity.END);
+            List<Rect> rectsC = getRectsForTaskCount(count - (2 * lineTaskCount), containerWidth,
+                    lineHeight, screenRatio, padding, ZERO_MARGIN, titleBarHeight, rectsA.get(0),
+                    VerticalGravity.START);
+            for (int i = 0; i < rectsA.size(); i++) {
+                rectsA.get(i).offset(margins.get(0), margins.get(1));
             }
-            for (Rect rect : rectsC) {
-                rect.offset(0, 2 * (lineHeight + padding));
+            for (int i = 0; i < rectsB.size(); i++) {
+                rectsB.get(i).offset(margins.get(0), margins.get(1) + lineHeight + padding);
+            }
+            for (int i = 0; i < rectsC.size(); i++) {
+                rectsC.get(i).offset(margins.get(0), margins.get(1) + 2 * (lineHeight + padding));
             }
             rects.addAll(rectsA);
             rects.addAll(rectsB);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithmTest.java
index 74b6127..a9f811b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithmTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/grid/TaskGridLayoutAlgorithmTest.java
@@ -19,39 +19,44 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import com.android.systemui.SysuiTestCase;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertTrue;
 
+import org.junit.Test;
+
 @SmallTest
 public class TaskGridLayoutAlgorithmTest extends SysuiTestCase {
 
-    public void testMethodName_ExpectedBehavior() {
-        assertTrue(true);
-    }
+    private static final List<Integer> ZERO_MARGIN = TaskGridLayoutAlgorithm.ZERO_MARGIN;
 
+    @Test
     public void testOneTile() {
         List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                1, 1000, 500, false /* allowLineOfThree */, 0 /* padding */);
+                1, 1000, 1000, 1 /* screenRatio */, 0 /* padding */, ZERO_MARGIN, 0);
         assertEquals(1, rects.size());
         Rect singleRect = rects.get(0);
         assertEquals(1000, singleRect.width());
     }
 
+    @Test
     public void testTwoTilesLandscape() {
         List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                2, 1200, 500, false /* allowLineOfThree */, 0 /* padding */);
+                2, 1200, 500, 1.2f /* screenRatio */, 0 /* padding */, ZERO_MARGIN, 0);
         assertEquals(2, rects.size());
         for (Rect rect : rects) {
             assertEquals(600, rect.width());
-            assertEquals(500, rect.height());
+            assertEquals(499, rect.height());
         }
     }
 
+    @Test
     public void testTwoTilesLandscapeWithPadding() {
         List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                2, 1200, 500, false /* allowLineOfThree */, 10 /* padding */);
+                2, 1200, 500, 1.19f /* screenRatio */, 10 /* padding */, ZERO_MARGIN, 0);
         assertEquals(2, rects.size());
         Rect rectA = rects.get(0);
         Rect rectB = rects.get(1);
@@ -60,46 +65,46 @@
         assertEquals(605, rectB.left);
     }
 
+    @Test
     public void testTwoTilesPortrait() {
         List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                2, 500, 1200, false /* allowLineOfThree */, 0 /* padding */);
+                2, 500, 1200, 1 /* screenRatio */, 0 /* padding */, ZERO_MARGIN, 0);
         assertEquals(2, rects.size());
         for (Rect rect : rects) {
-            assertEquals(500, rect.width());
-            assertEquals(600, rect.height());
-        }
-    }
-
-    public void testThreeTiles() {
-        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                3, 1200, 500, false /* allowLineOfThree */, 0 /* padding */);
-        assertEquals(3, rects.size());
-        for (Rect rect : rects) {
-            assertEquals(600, rect.width());
+            assertEquals(250, rect.width());
             assertEquals(250, rect.height());
         }
-        // The third tile should be on the second line, in the middle.
-        Rect rectC = rects.get(2);
-        assertEquals(300, rectC.left);
-        assertEquals(250, rectC.top);
     }
 
+    @Test
+    public void testThreeTiles() {
+        List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
+                3, 1200, 500, 2 /* screenRatio */, 0 /* padding */, ZERO_MARGIN, 0);
+        assertEquals(3, rects.size());
+        for (Rect rect : rects) {
+            assertEquals(400, rect.width());
+            assertEquals(200, rect.height());
+        }
+    }
+
+    @Test
     public void testFourTiles() {
         List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                4, 1200, 500, false /* allowLineOfThree */, 0 /* padding */);
+                4, 1200, 500, 2.4f /* screenRatio */, 0 /* padding */, ZERO_MARGIN, 0);
         assertEquals(4, rects.size());
         for (Rect rect : rects) {
             assertEquals(600, rect.width());
-            assertEquals(250, rect.height());
+            assertEquals(249, rect.height());
         }
         Rect rectD = rects.get(3);
         assertEquals(600, rectD.left);
         assertEquals(250, rectD.top);
     }
 
+    @Test
     public void testNineTiles() {
         List<Rect> rects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
-                9, 1200, 600, false /* allowLineOfThree */, 0 /* padding */);
+                9, 1200, 600, 2 /* screenRatio */, 0 /* padding */, ZERO_MARGIN, 0);
         assertEquals(9, rects.size());
         for (Rect rect : rects) {
             assertEquals(400, rect.width());