Merge "Move Launcher3GoIconRecents to /product partition and update local override package." into ub-launcher3-qt-dev
diff --git a/go/quickstep/res/values/dimens.xml b/go/quickstep/res/drawable/default_thumbnail.xml
similarity index 61%
rename from go/quickstep/res/values/dimens.xml
rename to go/quickstep/res/drawable/default_thumbnail.xml
index e2fa387..0a2dbf0 100644
--- a/go/quickstep/res/values/dimens.xml
+++ b/go/quickstep/res/drawable/default_thumbnail.xml
@@ -14,13 +14,9 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<resources>
-    <dimen name="task_item_half_vert_margin">8dp</dimen>
-    <dimen name="task_thumbnail_and_icon_view_size">60dp</dimen>
-    <dimen name="task_thumbnail_height">60dp</dimen>
-    <dimen name="task_thumbnail_width">36dp</dimen>
-    <dimen name="task_icon_size">36dp</dimen>
-
-    <dimen name="clear_all_button_width">106dp</dimen>
-    <dimen name="clear_all_button_height">36dp</dimen>
-</resources>
\ No newline at end of file
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@android:color/darker_gray"/>
+    <corners android:radius="2dp"/>
+</shape>
diff --git a/go/quickstep/res/layout/clear_all_button.xml b/go/quickstep/res/layout/clear_all_button.xml
new file mode 100644
index 0000000..003ee86
--- /dev/null
+++ b/go/quickstep/res/layout/clear_all_button.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2019 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.
+-->
+<com.android.quickstep.views.ClearAllItemView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/clear_all_item_view"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <Button
+        android:id="@+id/clear_all_button"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:background="@drawable/clear_all_button"
+        android:gravity="center"
+        android:text="@string/recents_clear_all"
+        android:textAllCaps="false"
+        android:textColor="@color/clear_all_button_text"
+        android:textSize="14sp"/>
+</com.android.quickstep.views.ClearAllItemView>
diff --git a/go/quickstep/res/layout/icon_recents_root_view.xml b/go/quickstep/res/layout/icon_recents_root_view.xml
index fddb1d3..6fb7e19 100644
--- a/go/quickstep/res/layout/icon_recents_root_view.xml
+++ b/go/quickstep/res/layout/icon_recents_root_view.xml
@@ -19,31 +19,11 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
-    <LinearLayout
-        android:id="@+id/recent_task_content_view"
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/recent_task_recycler_view"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:orientation="vertical"
-        android:visibility="gone">
-        <androidx.recyclerview.widget.RecyclerView
-            android:id="@+id/recent_task_recycler_view"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:scrollbars="none"/>
-        <Button
-            android:id="@+id/clear_all_button"
-            android:layout_width="@dimen/clear_all_button_width"
-            android:layout_height="@dimen/clear_all_button_height"
-            android:layout_gravity="center_horizontal"
-            android:layout_marginVertical="@dimen/task_item_half_vert_margin"
-            android:background="@drawable/clear_all_button"
-            android:gravity="center"
-            android:text="@string/recents_clear_all"
-            android:textAllCaps="false"
-            android:textColor="@color/clear_all_button_text"
-            android:textSize="14sp"/>
-    </LinearLayout>
+        android:scrollbars="none"/>
     <TextView
         android:id="@+id/recent_task_empty_view"
         android:layout_width="match_parent"
diff --git a/go/quickstep/res/layout/task_item_view.xml b/go/quickstep/res/layout/task_item_view.xml
index 048e9c5..1483d4c 100644
--- a/go/quickstep/res/layout/task_item_view.xml
+++ b/go/quickstep/res/layout/task_item_view.xml
@@ -17,26 +17,23 @@
 <com.android.quickstep.views.TaskItemView
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
     android:orientation="horizontal">
-    <FrameLayout
+    <com.android.quickstep.views.TaskThumbnailIconView
         android:id="@+id/task_icon_and_thumbnail"
-        android:layout_width="@dimen/task_thumbnail_and_icon_view_size"
-        android:layout_height="@dimen/task_thumbnail_and_icon_view_size"
-        android:layout_gravity="center_vertical"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
         android:layout_marginHorizontal="8dp"
-        android:layout_marginVertical="@dimen/task_item_half_vert_margin">
+        android:layout_marginTop="16dp">
         <ImageView
             android:id="@+id/task_thumbnail"
-            android:layout_width="@dimen/task_thumbnail_width"
-            android:layout_height="@dimen/task_thumbnail_height"
-            android:layout_gravity="top|start"/>
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
         <ImageView
             android:id="@+id/task_icon"
-            android:layout_width="@dimen/task_icon_size"
-            android:layout_height="@dimen/task_icon_size"
-            android:layout_gravity="bottom|end"/>
-    </FrameLayout>
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"/>
+    </com.android.quickstep.views.TaskThumbnailIconView>
     <TextView
         android:id="@+id/task_label"
         android:layout_width="wrap_content"
diff --git a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java b/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
index 6730e97..d20910f 100644
--- a/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
+++ b/go/quickstep/src/com/android/launcher3/uioverrides/states/OverviewState.java
@@ -18,6 +18,7 @@
 
 import static com.android.launcher3.LauncherAnimUtils.OVERVIEW_TRANSITION_MS;
 import static com.android.launcher3.anim.Interpolators.DEACCEL_2;
+import static com.android.launcher3.states.RotationHelper.REQUEST_ROTATE;
 
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.Launcher;
@@ -51,6 +52,9 @@
     public void onStateEnabled(Launcher launcher) {
         IconRecentsView recentsView = launcher.getOverviewPanel();
         recentsView.onBeginTransitionToOverview();
+        // Request orientation be set to unspecified, letting the system decide the best
+        // orientation.
+        launcher.getRotationHelper().setCurrentStateRequest(REQUEST_ROTATE);
     }
 
     @Override
diff --git a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
index d1d697c..c228bb9 100644
--- a/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
+++ b/go/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java
@@ -131,10 +131,11 @@
             return anim;
         }
 
-        View thumbnailView = mRecentsView.getThumbnailViewForTask(mTargetTaskId);
+        View thumbnailView = mRecentsView.getBottomThumbnailView();
         if (thumbnailView == null) {
-            // TODO: We should either 1) guarantee the view is loaded before attempting this
-            // or 2) have a backup animation.
+            // This can be null if there were previously 0 tasks and the recycler view has not had
+            // enough time to take in the data change, bind a new view, and lay out the new view.
+            // TODO: Have a fallback to animate to
             if (Log.isLoggable(TAG, Log.WARN)) {
                 Log.w(TAG, "No thumbnail view for running task. Using stub animation.");
             }
diff --git a/go/quickstep/src/com/android/quickstep/ClearAllHolder.java b/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
new file mode 100644
index 0000000..ce87171
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ClearAllHolder.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2019 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.quickstep;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+
+/**
+ * Holder for clear all button view in task recycler view.
+ */
+final class ClearAllHolder extends ViewHolder {
+    public ClearAllHolder(@NonNull View itemView) {
+        super(itemView);
+    }
+}
diff --git a/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java b/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
new file mode 100644
index 0000000..1b6f2e3
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ContentFillItemAnimator.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2019 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.quickstep;
+
+import static android.view.View.ALPHA;
+
+import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
+import static com.android.quickstep.views.TaskItemView.CONTENT_TRANSITION_PROGRESS;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
+import androidx.recyclerview.widget.SimpleItemAnimator;
+
+import com.android.quickstep.views.TaskItemView;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * An item animator that is only set and used for the transition from the empty loading UI to
+ * the filled task content UI. The animation starts from the bottom to top, changing all valid
+ * empty item views to be filled and removing all extra empty views.
+ */
+public final class ContentFillItemAnimator extends SimpleItemAnimator {
+
+    private static final class PendingAnimation {
+        ViewHolder viewHolder;
+        int animType;
+
+        PendingAnimation(ViewHolder vh, int type) {
+            viewHolder = vh;
+            animType = type;
+        }
+    }
+
+    private static final int ANIM_TYPE_REMOVE = 0;
+    private static final int ANIM_TYPE_CHANGE = 1;
+
+    private static final int ITEM_BETWEEN_DELAY = 40;
+    private static final int ITEM_CHANGE_DURATION = 150;
+    private static final int ITEM_REMOVE_DURATION = 150;
+
+    /**
+     * Animations that have been registered to occur together at the next call of
+     * {@link #runPendingAnimations()} but have not started.
+     */
+    private final ArrayList<PendingAnimation> mPendingAnims = new ArrayList<>();
+
+    /**
+     * Animations that have started and are running.
+     */
+    private final ArrayList<ObjectAnimator> mRunningAnims = new ArrayList<>();
+
+    private Runnable mOnFinishRunnable;
+
+    /**
+     * Set runnable to run after the content fill animation is fully completed.
+     *
+     * @param runnable runnable to run on end
+     */
+    public void setOnAnimationFinishedRunnable(Runnable runnable) {
+        mOnFinishRunnable = runnable;
+    }
+
+    @Override
+    public void setChangeDuration(long changeDuration) {
+        throw new UnsupportedOperationException("Cascading item animator cannot have animation "
+                + "duration changed.");
+    }
+
+    @Override
+    public void setRemoveDuration(long removeDuration) {
+        throw new UnsupportedOperationException("Cascading item animator cannot have animation "
+                + "duration changed.");
+    }
+
+    @Override
+    public boolean animateRemove(ViewHolder holder) {
+        PendingAnimation pendAnim = new PendingAnimation(holder, ANIM_TYPE_REMOVE);
+        mPendingAnims.add(pendAnim);
+        return true;
+    }
+
+    private void animateRemoveImpl(ViewHolder holder, long startDelay) {
+        final View view = holder.itemView;
+        if (holder.itemView.getAlpha() == 0) {
+            // View is already visually removed. We can just get rid of it now.
+            view.setAlpha(1.0f);
+            dispatchRemoveFinished(holder);
+            dispatchFinishedWhenDone();
+            return;
+        }
+        final ObjectAnimator anim = ObjectAnimator.ofFloat(
+                holder.itemView, ALPHA, holder.itemView.getAlpha(), 0.0f);
+        anim.setDuration(ITEM_REMOVE_DURATION).setStartDelay(startDelay);
+        anim.addListener(
+                new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationStart(Animator animation) {
+                        dispatchRemoveStarting(holder);
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        view.setAlpha(1);
+                        dispatchRemoveFinished(holder);
+                        mRunningAnims.remove(anim);
+                        dispatchFinishedWhenDone();
+                    }
+                }
+        );
+        anim.start();
+        mRunningAnims.add(anim);
+    }
+
+    @Override
+    public boolean animateAdd(ViewHolder holder) {
+        dispatchAddFinished(holder);
+        return false;
+    }
+
+    @Override
+    public boolean animateMove(ViewHolder holder, int fromX, int fromY, int toX,
+            int toY) {
+        dispatchMoveFinished(holder);
+        return false;
+    }
+
+    @Override
+    public boolean animateChange(ViewHolder oldHolder,
+            ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
+        // Only support changes where the holders are the same
+        if (oldHolder == newHolder) {
+            PendingAnimation pendAnim = new PendingAnimation(oldHolder, ANIM_TYPE_CHANGE);
+            mPendingAnims.add(pendAnim);
+            return true;
+        }
+        dispatchChangeFinished(oldHolder, true /* oldItem */);
+        dispatchChangeFinished(newHolder, false /* oldItem */);
+        return false;
+    }
+
+    private void animateChangeImpl(ViewHolder viewHolder, long startDelay) {
+        TaskItemView itemView = (TaskItemView) viewHolder.itemView;
+        final ObjectAnimator anim =
+                ObjectAnimator.ofFloat(itemView, CONTENT_TRANSITION_PROGRESS, 0.0f, 1.0f);
+        anim.setDuration(ITEM_CHANGE_DURATION).setStartDelay(startDelay);
+        anim.addListener(
+                new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationStart(Animator animation) {
+                        dispatchChangeStarting(viewHolder, true /* oldItem */);
+                    }
+
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        dispatchChangeFinished(viewHolder, true /* oldItem */);
+                        mRunningAnims.remove(anim);
+                        dispatchFinishedWhenDone();
+                    }
+                }
+        );
+        anim.start();
+        mRunningAnims.add(anim);
+    }
+
+    @Override
+    public void runPendingAnimations() {
+        // Run animations bottom to top.
+        mPendingAnims.sort(Comparator.comparingInt(o -> -o.viewHolder.itemView.getBottom()));
+        int delay = 0;
+        while (!mPendingAnims.isEmpty()) {
+            PendingAnimation curAnim = mPendingAnims.remove(0);
+            ViewHolder vh = curAnim.viewHolder;
+            switch (curAnim.animType) {
+                case ANIM_TYPE_REMOVE:
+                    animateRemoveImpl(vh, delay);
+                    break;
+                case ANIM_TYPE_CHANGE:
+                    animateChangeImpl(vh, delay);
+                    break;
+                default:
+                    break;
+            }
+            delay += ITEM_BETWEEN_DELAY;
+        }
+    }
+
+    @Override
+    public void endAnimation(@NonNull ViewHolder item) {
+        for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
+            PendingAnimation pendAnim = mPendingAnims.get(i);
+            if (pendAnim.viewHolder == item) {
+                mPendingAnims.remove(i);
+                switch (pendAnim.animType) {
+                    case ANIM_TYPE_REMOVE:
+                        dispatchRemoveFinished(item);
+                        break;
+                    case ANIM_TYPE_CHANGE:
+                        dispatchChangeFinished(item, true /* oldItem */);
+                        break;
+                    default:
+                        break;
+                }
+            }
+        }
+        dispatchFinishedWhenDone();
+    }
+
+    @Override
+    public void endAnimations() {
+        for (int i = mPendingAnims.size() - 1; i >= 0; i--) {
+            PendingAnimation pendAnim = mPendingAnims.get(i);
+            ViewHolder item = pendAnim.viewHolder;
+            switch (pendAnim.animType) {
+                case ANIM_TYPE_REMOVE:
+                    dispatchRemoveFinished(item);
+                    break;
+                case ANIM_TYPE_CHANGE:
+                    dispatchChangeFinished(item, true /* oldItem */);
+                    break;
+                default:
+                    break;
+            }
+            mPendingAnims.remove(i);
+        }
+        for (int i = 0; i < mRunningAnims.size(); i++) {
+            ObjectAnimator anim = mRunningAnims.get(i);
+            anim.end();
+        }
+        dispatchAnimationsFinished();
+    }
+
+    @Override
+    public boolean isRunning() {
+        return !mPendingAnims.isEmpty() || !mRunningAnims.isEmpty();
+    }
+
+    @Override
+    public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+            @NonNull List<Object> payloads) {
+        if (!payloads.isEmpty()
+                && (int) payloads.get(0) == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
+            return true;
+        }
+        return super.canReuseUpdatedViewHolder(viewHolder, payloads);
+    }
+
+    private void dispatchFinishedWhenDone() {
+        if (!isRunning()) {
+            dispatchAnimationsFinished();
+            if (mOnFinishRunnable != null) {
+                mOnFinishRunnable.run();
+            }
+        }
+    }
+}
diff --git a/go/quickstep/src/com/android/quickstep/TaskActionController.java b/go/quickstep/src/com/android/quickstep/TaskActionController.java
index 71bee91..09e2367 100644
--- a/go/quickstep/src/com/android/quickstep/TaskActionController.java
+++ b/go/quickstep/src/com/android/quickstep/TaskActionController.java
@@ -15,6 +15,8 @@
  */
 package com.android.quickstep;
 
+import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
+
 import android.app.ActivityOptions;
 import android.view.View;
 
@@ -42,7 +44,7 @@
      * @param viewHolder the task view holder to launch
      */
     public void launchTask(TaskHolder viewHolder) {
-        if (viewHolder.getTask() == null) {
+        if (!viewHolder.getTask().isPresent()) {
             return;
         }
         TaskItemView itemView = (TaskItemView) (viewHolder.itemView);
@@ -53,8 +55,9 @@
         int height = v.getMeasuredHeight();
 
         ActivityOptions opts = ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
-        ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(viewHolder.getTask().key,
-                opts, null /* resultCallback */, null /* resultCallbackHandler */);
+        ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(
+                viewHolder.getTask().get().key, opts, null /* resultCallback */,
+                null /* resultCallbackHandler */);
     }
 
     /**
@@ -63,11 +66,11 @@
      * @param viewHolder the task view holder to remove
      */
     public void removeTask(TaskHolder viewHolder) {
-        if (viewHolder.getTask() == null) {
+        if (!viewHolder.getTask().isPresent()) {
             return;
         }
         int position = viewHolder.getAdapterPosition();
-        Task task = viewHolder.getTask();
+        Task task = viewHolder.getTask().get();
         ActivityManagerWrapper.getInstance().removeTask(task.key.id);
         mLoader.removeTask(task);
         mAdapter.notifyItemRemoved(position);
@@ -80,6 +83,6 @@
         int count = mAdapter.getItemCount();
         ActivityManagerWrapper.getInstance().removeAllRecentTasks();
         mLoader.clearAllTasks();
-        mAdapter.notifyItemRangeRemoved(0 /* positionStart */, count);
+        mAdapter.notifyItemRangeRemoved(TASKS_START_POSITION /* positionStart */, count);
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/TaskAdapter.java b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
index 674fcae..6f75629 100644
--- a/go/quickstep/src/com/android/quickstep/TaskAdapter.java
+++ b/go/quickstep/src/com/android/quickstep/TaskAdapter.java
@@ -15,13 +15,15 @@
  */
 package com.android.quickstep;
 
-import android.util.ArrayMap;
 import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
+import android.widget.Button;
 
 import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
 import androidx.recyclerview.widget.RecyclerView.Adapter;
+import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
 import com.android.launcher3.R;
 import com.android.quickstep.views.TaskItemView;
@@ -29,18 +31,25 @@
 
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 
 /**
  * Recycler view adapter that dynamically inflates and binds {@link TaskHolder} instances with the
  * appropriate {@link Task} from the recents task list.
  */
-public final class TaskAdapter extends Adapter<TaskHolder> {
+public final class TaskAdapter extends Adapter<ViewHolder> {
 
-    private static final int MAX_TASKS_TO_DISPLAY = 6;
+    public static final int CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT = 0;
+    public static final int MAX_TASKS_TO_DISPLAY = 6;
+    public static final int TASKS_START_POSITION = 1;
+
+    public static final int ITEM_TYPE_TASK = 0;
+    public static final int ITEM_TYPE_CLEAR_ALL = 1;
+
     private static final String TAG = "TaskAdapter";
     private final TaskListLoader mLoader;
-    private final ArrayMap<Integer, TaskItemView> mTaskIdToViewMap = new ArrayMap<>();
     private TaskActionController mTaskActionController;
+    private OnClickListener mClearAllListener;
     private boolean mIsShowingLoadingUi;
 
     public TaskAdapter(@NonNull TaskListLoader loader) {
@@ -51,6 +60,10 @@
         mTaskActionController = taskActionController;
     }
 
+    public void setOnClearAllClickListener(OnClickListener listener) {
+        mClearAllListener = listener;
+    }
+
     /**
      * Sets all positions in the task adapter to loading views, binding new views if necessary.
      * This changes the task adapter's view of the data, so the appropriate notify events should be
@@ -63,75 +76,103 @@
         mIsShowingLoadingUi = isShowingLoadingUi;
     }
 
-    /**
-     * Get task item view for a given task id if it's attached to the view.
-     *
-     * @param taskId task id to search for
-     * @return corresponding task item view if it's attached, null otherwise
-     */
-    public @Nullable TaskItemView getTaskItemView(int taskId) {
-        return mTaskIdToViewMap.get(taskId);
+    @Override
+    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case ITEM_TYPE_TASK:
+                TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.task_item_view, parent, false);
+                TaskHolder taskHolder = new TaskHolder(itemView);
+                itemView.setOnClickListener(view -> mTaskActionController.launchTask(taskHolder));
+                return taskHolder;
+            case ITEM_TYPE_CLEAR_ALL:
+                View clearView = LayoutInflater.from(parent.getContext())
+                        .inflate(R.layout.clear_all_button, parent, false);
+                ClearAllHolder clearAllHolder = new ClearAllHolder(clearView);
+                Button clearViewButton = clearView.findViewById(R.id.clear_all_button);
+                clearViewButton.setOnClickListener(mClearAllListener);
+                return clearAllHolder;
+            default:
+                throw new IllegalArgumentException("No known holder for item type: " + viewType);
+        }
     }
 
     @Override
-    public TaskHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        TaskItemView itemView = (TaskItemView) LayoutInflater.from(parent.getContext())
-                .inflate(R.layout.task_item_view, parent, false);
-        TaskHolder holder = new TaskHolder(itemView);
-        itemView.setOnClickListener(view -> mTaskActionController.launchTask(holder));
-        return holder;
+    public void onBindViewHolder(ViewHolder holder, int position) {
+        onBindViewHolderInternal(holder, position, false /* willAnimate */);
     }
 
     @Override
-    public void onBindViewHolder(TaskHolder holder, int position) {
-        if (mIsShowingLoadingUi) {
-            holder.bindEmptyUi();
+    public void onBindViewHolder(@NonNull ViewHolder holder, int position,
+            @NonNull List<Object> payloads) {
+        if (payloads.isEmpty()) {
+            super.onBindViewHolder(holder, position, payloads);
             return;
         }
-        List<Task> tasks = mLoader.getCurrentTaskList();
-        if (position >= tasks.size()) {
-            // Task list has updated.
-            return;
+        int changeType = (int) payloads.get(0);
+        if (changeType == CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT) {
+            // Bind in preparation for animation
+            onBindViewHolderInternal(holder, position, true /* willAnimate */);
+        } else {
+            throw new IllegalArgumentException("Payload content is not a valid change event type: "
+                    + changeType);
         }
-        Task task = tasks.get(position);
-        holder.bindTask(task);
-        mLoader.loadTaskIconAndLabel(task, () -> {
-            // Ensure holder still has the same task.
-            if (Objects.equals(task, holder.getTask())) {
-                holder.getTaskItemView().setIcon(task.icon);
-                holder.getTaskItemView().setLabel(task.titleDescription);
-            }
-        });
-        mLoader.loadTaskThumbnail(task, () -> {
-            if (Objects.equals(task, holder.getTask())) {
-                holder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
-            }
-        });
+    }
+
+    private void onBindViewHolderInternal(@NonNull ViewHolder holder, int position,
+            boolean willAnimate) {
+        int itemType = getItemViewType(position);
+        switch (itemType) {
+            case ITEM_TYPE_TASK:
+                TaskHolder taskHolder = (TaskHolder) holder;
+                if (mIsShowingLoadingUi) {
+                    taskHolder.bindEmptyUi();
+                    return;
+                }
+                List<Task> tasks = mLoader.getCurrentTaskList();
+                int taskPos = position - TASKS_START_POSITION;
+                if (taskPos >= tasks.size()) {
+                    // Task list has updated.
+                    return;
+                }
+                Task task = tasks.get(taskPos);
+                taskHolder.bindTask(task, willAnimate /* willAnimate */);
+                mLoader.loadTaskIconAndLabel(task, () -> {
+                    // Ensure holder still has the same task.
+                    if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
+                        taskHolder.getTaskItemView().setIcon(task.icon);
+                        taskHolder.getTaskItemView().setLabel(task.titleDescription);
+                    }
+                });
+                mLoader.loadTaskThumbnail(task, () -> {
+                    if (Objects.equals(Optional.of(task), taskHolder.getTask())) {
+                        taskHolder.getTaskItemView().setThumbnail(task.thumbnail.thumbnail);
+                    }
+                });
+                break;
+            case ITEM_TYPE_CLEAR_ALL:
+                // Nothing to bind.
+                break;
+            default:
+                throw new IllegalArgumentException("No known holder for item type: " + itemType);
+        }
     }
 
     @Override
-    public void onViewAttachedToWindow(@NonNull TaskHolder holder) {
-        if (holder.getTask() == null) {
-            return;
-        }
-        mTaskIdToViewMap.put(holder.getTask().key.id, (TaskItemView) holder.itemView);
-    }
-
-    @Override
-    public void onViewDetachedFromWindow(@NonNull TaskHolder holder) {
-        if (holder.getTask() == null) {
-            return;
-        }
-        mTaskIdToViewMap.remove(holder.getTask().key.id);
+    public int getItemViewType(int position) {
+        // Bottom is always clear all button.
+        return (position == 0) ? ITEM_TYPE_CLEAR_ALL : ITEM_TYPE_TASK;
     }
 
     @Override
     public int getItemCount() {
+        int itemCount = TASKS_START_POSITION;
         if (mIsShowingLoadingUi) {
             // Show loading version of all items.
-            return MAX_TASKS_TO_DISPLAY;
+            itemCount += MAX_TASKS_TO_DISPLAY;
         } else {
-            return Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
+            itemCount += Math.min(mLoader.getCurrentTaskList().size(), MAX_TASKS_TO_DISPLAY);
         }
+        return itemCount;
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/TaskHolder.java b/go/quickstep/src/com/android/quickstep/TaskHolder.java
index 98dc989..5755df4 100644
--- a/go/quickstep/src/com/android/quickstep/TaskHolder.java
+++ b/go/quickstep/src/com/android/quickstep/TaskHolder.java
@@ -15,12 +15,16 @@
  */
 package com.android.quickstep;
 
-import androidx.annotation.Nullable;
+import android.graphics.Bitmap;
+
+import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
 
 import com.android.quickstep.views.TaskItemView;
 import com.android.systemui.shared.recents.model.Task;
 
+import java.util.Optional;
+
 /**
  * A recycler view holder that holds the task view and binds {@link Task} content (app title, icon,
  * etc.) to the view.
@@ -40,13 +44,28 @@
     }
 
     /**
-     * Bind a task to the holder, resetting the view and preparing it for content to load in.
+     * Bind the task model to the holder. This will take the current task content in the task
+     * object (i.e. icon, thumbnail, label) and either apply the content immediately or simply bind
+     * the content to animate to at a later time. If the task does not have all its content loaded,
+     * the view will prepare appropriate default placeholders and it is the callers responsibility
+     * to change them at a later time.
+     *
+     * Regardless of whether it is animating, input handlers will be bound immediately (see
+     * {@link TaskActionController}).
      *
      * @param task the task to bind to the view
+     * @param willAnimate true if UI should animate in later, false if it should apply immediately
      */
-    public void bindTask(Task task) {
+    public void bindTask(@NonNull Task task, boolean willAnimate) {
         mTask = task;
-        mTaskItemView.resetTaskItemView();
+        Bitmap thumbnail = (task.thumbnail != null) ? task.thumbnail.thumbnail : null;
+        if (willAnimate) {
+            mTaskItemView.startContentAnimation(task.icon, thumbnail, task.titleDescription);
+        } else {
+            mTaskItemView.setIcon(task.icon);
+            mTaskItemView.setThumbnail(thumbnail);
+            mTaskItemView.setLabel(task.titleDescription);
+        }
     }
 
     /**
@@ -55,10 +74,7 @@
      */
     public void bindEmptyUi() {
         mTask = null;
-        // TODO: Set the task view to a loading, empty UI.
-        // Temporarily using the one below for visual confirmation but should be swapped out to new
-        // UI later.
-        mTaskItemView.resetTaskItemView();
+        mTaskItemView.resetToEmptyUi();
     }
 
     /**
@@ -66,7 +82,7 @@
      *
      * @return the current task
      */
-    public @Nullable Task getTask() {
-        return mTask;
+    public Optional<Task> getTask() {
+        return Optional.ofNullable(mTask);
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/TaskListLoader.java b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
index 51b73f1..850c7e6 100644
--- a/go/quickstep/src/com/android/quickstep/TaskListLoader.java
+++ b/go/quickstep/src/com/android/quickstep/TaskListLoader.java
@@ -38,23 +38,9 @@
 
     private ArrayList<Task> mTaskList = new ArrayList<>();
     private int mTaskListChangeId;
-    private RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
-        Task foundTask = null;
-        for (Task task : mTaskList) {
-            if (task.key.id == taskId) {
-                foundTask = task;
-                break;
-            }
-        }
-        if (foundTask != null) {
-            foundTask.thumbnail = thumbnailData;
-        }
-        return foundTask;
-    };
 
     public TaskListLoader(Context context) {
         mRecentsModel = RecentsModel.INSTANCE.get(context);
-        mRecentsModel.addThumbnailChangeListener(listener);
     }
 
     /**
diff --git a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java b/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
index 98407d8..19951bb 100644
--- a/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
+++ b/go/quickstep/src/com/android/quickstep/TaskSwipeCallback.java
@@ -17,6 +17,9 @@
 
 import static androidx.recyclerview.widget.ItemTouchHelper.RIGHT;
 
+import static com.android.quickstep.TaskAdapter.ITEM_TYPE_CLEAR_ALL;
+
+import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.ItemTouchHelper;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
@@ -45,4 +48,14 @@
             mTaskActionController.removeTask((TaskHolder) viewHolder);
         }
     }
+
+    @Override
+    public int getSwipeDirs(@NonNull RecyclerView recyclerView,
+            @NonNull ViewHolder viewHolder) {
+        if (viewHolder.getItemViewType() == ITEM_TYPE_CLEAR_ALL) {
+            // Clear all button should not be swipable.
+            return 0;
+        }
+        return super.getSwipeDirs(recyclerView, viewHolder);
+    }
 }
diff --git a/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java b/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
new file mode 100644
index 0000000..6ef9039
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/ThumbnailDrawable.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2019 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.quickstep;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.NonNull;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+/**
+ * Bitmap backed drawable that supports rotating the thumbnail bitmap depending on if the
+ * orientation the thumbnail was taken in matches the desired orientation. In addition, the
+ * thumbnail always fills into the containing bounds.
+ */
+public final class ThumbnailDrawable extends Drawable {
+
+    private final Paint mPaint = new Paint();
+    private final Matrix mMatrix = new Matrix();
+    private final ThumbnailData mThumbnailData;
+    private int mRequestedOrientation;
+
+    public ThumbnailDrawable(@NonNull ThumbnailData thumbnailData, int requestedOrientation) {
+        mThumbnailData = thumbnailData;
+        mRequestedOrientation = requestedOrientation;
+        updateMatrix();
+    }
+
+    /**
+     * Set the requested orientation.
+     *
+     * @param orientation the orientation we want the thumbnail to be in
+     */
+    public void setRequestedOrientation(int orientation) {
+        if (mRequestedOrientation != orientation) {
+            mRequestedOrientation = orientation;
+            updateMatrix();
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if (mThumbnailData.thumbnail == null) {
+            return;
+        }
+        canvas.drawBitmap(mThumbnailData.thumbnail, mMatrix, mPaint);
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+        updateMatrix();
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        final int oldAlpha = mPaint.getAlpha();
+        if (alpha != oldAlpha) {
+            mPaint.setAlpha(alpha);
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public int getAlpha() {
+        return mPaint.getAlpha();
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        mPaint.setColorFilter(colorFilter);
+        invalidateSelf();
+    }
+
+    @Override
+    public ColorFilter getColorFilter() {
+        return mPaint.getColorFilter();
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    private void updateMatrix() {
+        if (mThumbnailData.thumbnail == null) {
+            return;
+        }
+        mMatrix.reset();
+        float scaleX;
+        float scaleY;
+        Rect bounds = getBounds();
+        Bitmap thumbnail = mThumbnailData.thumbnail;
+        if (mRequestedOrientation != mThumbnailData.orientation) {
+            // Rotate and translate so that top left is the same.
+            mMatrix.postRotate(90, 0, 0);
+            mMatrix.postTranslate(thumbnail.getHeight(), 0);
+
+            scaleX = (float) bounds.width() / thumbnail.getHeight();
+            scaleY = (float) bounds.height() / thumbnail.getWidth();
+        } else {
+            scaleX = (float) bounds.width() / thumbnail.getWidth();
+            scaleY = (float) bounds.height() / thumbnail.getHeight();
+        }
+        // Scale to fill.
+        mMatrix.postScale(scaleX, scaleY);
+    }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java b/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java
new file mode 100644
index 0000000..378dbf4
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/ClearAllItemView.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.quickstep.views;
+
+import static com.android.quickstep.views.TaskLayoutUtils.getClearAllItemHeight;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * Recycler view item that lays out the clear all button and measures the space it takes based on
+ * the device height.
+ */
+public final class ClearAllItemView extends FrameLayout {
+
+    public ClearAllItemView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int buttonHeight = getClearAllItemHeight(getContext());
+        int newHeightSpec = MeasureSpec.makeMeasureSpec(buttonHeight, MeasureSpec.EXACTLY);
+        super.onMeasure(widthMeasureSpec, newHeightSpec);
+    }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
index c06b6ec..b7ed5b5 100644
--- a/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
+++ b/go/quickstep/src/com/android/quickstep/views/IconRecentsView.java
@@ -15,8 +15,13 @@
  */
 package com.android.quickstep.views;
 
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+
 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
 
+import static com.android.quickstep.TaskAdapter.CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT;
+import static com.android.quickstep.TaskAdapter.TASKS_START_POSITION;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
@@ -24,30 +29,37 @@
 import android.animation.PropertyValuesHolder;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.util.ArraySet;
 import android.util.AttributeSet;
 import android.util.FloatProperty;
 import android.view.View;
 import android.view.ViewDebug;
-import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
-import android.view.animation.AnimationSet;
-import android.view.animation.LayoutAnimationController;
+import android.view.ViewTreeObserver;
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.DefaultItemAnimator;
 import androidx.recyclerview.widget.ItemTouchHelper;
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver;
+import androidx.recyclerview.widget.RecyclerView.OnChildAttachStateChangeListener;
 
+import com.android.launcher3.BaseActivity;
 import com.android.launcher3.R;
+import com.android.quickstep.ContentFillItemAnimator;
+import com.android.quickstep.RecentsModel;
 import com.android.quickstep.RecentsToActivityHelper;
 import com.android.quickstep.TaskActionController;
 import com.android.quickstep.TaskAdapter;
 import com.android.quickstep.TaskHolder;
 import com.android.quickstep.TaskListLoader;
 import com.android.quickstep.TaskSwipeCallback;
+import com.android.systemui.shared.recents.model.Task;
+
+import java.util.ArrayList;
+import java.util.Optional;
 
 /**
  * Root view for the icon recents view. Acts as the main interface to the rest of the Launcher code
@@ -89,24 +101,48 @@
     private final Context mContext;
     private final TaskListLoader mTaskLoader;
     private final TaskAdapter mTaskAdapter;
+    private final LinearLayoutManager mTaskLayoutManager;
     private final TaskActionController mTaskActionController;
-    private final LayoutAnimationController mLayoutAnimation;
+    private final DefaultItemAnimator mDefaultItemAnimator = new DefaultItemAnimator();
+    private final ContentFillItemAnimator mLoadingContentItemAnimator =
+            new ContentFillItemAnimator();
 
     private RecentsToActivityHelper mActivityHelper;
     private RecyclerView mTaskRecyclerView;
+    private View mShowingContentView;
     private View mEmptyView;
     private View mContentView;
-    private View mClearAllView;
     private boolean mTransitionedFromApp;
+    private AnimatorSet mLayoutAnimation;
+    private final ArraySet<View> mLayingOutViews = new ArraySet<>();
+    private final RecentsModel.TaskThumbnailChangeListener listener = (taskId, thumbnailData) -> {
+        ArrayList<TaskItemView> itemViews = getTaskViews();
+        for (int i = 0, size = itemViews.size(); i < size; i++) {
+            TaskItemView taskView = itemViews.get(i);
+            TaskHolder taskHolder = (TaskHolder) mTaskRecyclerView.getChildViewHolder(taskView);
+            Optional<Task> optTask = taskHolder.getTask();
+            if (optTask.filter(task -> task.key.id == taskId).isPresent()) {
+                Task task = optTask.get();
+                // Update thumbnail on the task.
+                task.thumbnail = thumbnailData;
+                taskView.setThumbnail(thumbnailData.thumbnail);
+                return task;
+            }
+        }
+        return null;
+    };
 
     public IconRecentsView(Context context, AttributeSet attrs) {
         super(context, attrs);
+        BaseActivity activity = BaseActivity.fromContext(context);
         mContext = context;
         mTaskLoader = new TaskListLoader(mContext);
         mTaskAdapter = new TaskAdapter(mTaskLoader);
+        mTaskAdapter.setOnClearAllClickListener(view -> animateClearAllTasks());
         mTaskActionController = new TaskActionController(mTaskLoader, mTaskAdapter);
         mTaskAdapter.setActionController(mTaskActionController);
-        mLayoutAnimation = createLayoutAnimation();
+        mTaskLayoutManager = new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */);
+        RecentsModel.INSTANCE.get(context).addThumbnailChangeListener(listener);
     }
 
     @Override
@@ -115,15 +151,30 @@
         if (mTaskRecyclerView == null) {
             mTaskRecyclerView = findViewById(R.id.recent_task_recycler_view);
             mTaskRecyclerView.setAdapter(mTaskAdapter);
-            mTaskRecyclerView.setLayoutManager(
-                    new LinearLayoutManager(mContext, VERTICAL, true /* reverseLayout */));
+            mTaskRecyclerView.setLayoutManager(mTaskLayoutManager);
             ItemTouchHelper helper = new ItemTouchHelper(
                     new TaskSwipeCallback(mTaskActionController));
             helper.attachToRecyclerView(mTaskRecyclerView);
-            mTaskRecyclerView.setLayoutAnimation(mLayoutAnimation);
+            mTaskRecyclerView.addOnChildAttachStateChangeListener(
+                    new OnChildAttachStateChangeListener() {
+                        @Override
+                        public void onChildViewAttachedToWindow(@NonNull View view) {
+                            if (mLayoutAnimation != null && !mLayingOutViews.contains(view)) {
+                                // Child view was added that is not part of current layout animation
+                                // so restart the animation.
+                                animateFadeInLayoutAnimation();
+                            }
+                        }
+
+                        @Override
+                        public void onChildViewDetachedFromWindow(@NonNull View view) { }
+                    });
+            mTaskRecyclerView.setItemAnimator(mDefaultItemAnimator);
+            mLoadingContentItemAnimator.setOnAnimationFinishedRunnable(
+                    () -> mTaskRecyclerView.setItemAnimator(new DefaultItemAnimator()));
 
             mEmptyView = findViewById(R.id.recent_task_empty_view);
-            mContentView = findViewById(R.id.recent_task_content_view);
+            mContentView = mTaskRecyclerView;
             mTaskAdapter.registerAdapterDataObserver(new AdapterDataObserver() {
                 @Override
                 public void onChanged() {
@@ -135,19 +186,17 @@
                     updateContentViewVisibility();
                 }
             });
-            mClearAllView = findViewById(R.id.clear_all_button);
-            mClearAllView.setOnClickListener(v -> animateClearAllTasks());
+            // TODO: Move layout param logic into onMeasure
         }
     }
 
     @Override
     public void setEnabled(boolean enabled) {
         super.setEnabled(enabled);
-        TaskItemView[] itemViews = getTaskViews();
-        for (TaskItemView itemView : itemViews) {
-            itemView.setEnabled(enabled);
+        int childCount = mTaskRecyclerView.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            mTaskRecyclerView.getChildAt(i).setEnabled(enabled);
         }
-        mClearAllView.setEnabled(enabled);
     }
 
     /**
@@ -165,8 +214,13 @@
      * becomes visible.
      */
     public void onBeginTransitionToOverview() {
-        mTaskRecyclerView.scheduleLayoutAnimation();
-
+        if (mContext.getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE) {
+            // Scroll to bottom of task in landscape mode. This is a non-issue in portrait mode as
+            // all tasks should be visible to fill up the screen in portrait mode and the view will
+            // not be scrollable.
+            mTaskLayoutManager.scrollToPositionWithOffset(TASKS_START_POSITION, 0 /* offset */);
+        }
+        scheduleFadeInLayoutAnimation();
         // Load any task changes
         if (!mTaskLoader.needsToLoad()) {
             return;
@@ -174,9 +228,24 @@
         mTaskAdapter.setIsShowingLoadingUi(true);
         mTaskAdapter.notifyDataSetChanged();
         mTaskLoader.loadTaskList(tasks -> {
+            int numEmptyItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
             mTaskAdapter.setIsShowingLoadingUi(false);
-            // TODO: Animate the loading UI out and the loaded data in.
-            mTaskAdapter.notifyDataSetChanged();
+            int numActualItems = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
+            if (numEmptyItems < numActualItems) {
+                throw new IllegalStateException("There are less empty item views than the number "
+                        + "of items to animate to.");
+            }
+            // Possible that task list loads faster than adapter changes propagate to layout so
+            // only start content fill animation if there aren't any pending adapter changes.
+            if (!mTaskRecyclerView.hasPendingAdapterUpdates()) {
+                // Set item animator for content filling animation. The item animator will switch
+                // back to the default on completion
+                mTaskRecyclerView.setItemAnimator(mLoadingContentItemAnimator);
+            }
+            mTaskAdapter.notifyItemRangeRemoved(TASKS_START_POSITION + numActualItems,
+                    numEmptyItems - numActualItems);
+            mTaskAdapter.notifyItemRangeChanged(TASKS_START_POSITION, numActualItems,
+                    CHANGE_EVENT_TYPE_EMPTY_TO_CONTENT);
         });
     }
 
@@ -194,35 +263,37 @@
      * the app. In that case, we launch the next most recent.
      */
     public void handleOverviewCommand() {
-        int childCount = mTaskRecyclerView.getChildCount();
-        if (childCount == 0) {
+        // TODO(130735711): Need to address case where most recent task is off screen/unattached.
+        ArrayList<TaskItemView> taskViews = getTaskViews();
+        int taskViewsSize = taskViews.size();
+        if (taskViewsSize <= 1) {
             // Do nothing
             return;
         }
         TaskHolder taskToLaunch;
-        if (mTransitionedFromApp && childCount > 1) {
+        if (mTransitionedFromApp && taskViewsSize > 1) {
             // Launch the next most recent app
-            TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(1);
+            TaskItemView itemView = taskViews.get(1);
             taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
         } else {
             // Launch the most recent app
-            TaskItemView itemView = (TaskItemView) mTaskRecyclerView.getChildAt(0);
+            TaskItemView itemView = taskViews.get(0);
             taskToLaunch = (TaskHolder) mTaskRecyclerView.getChildViewHolder(itemView);
         }
         mTaskActionController.launchTask(taskToLaunch);
     }
 
     /**
-     * Get the thumbnail view associated with a task for the purposes of animation.
+     * Get the bottom most thumbnail view to animate to.
      *
-     * @param taskId task id of thumbnail view to get
-     * @return the thumbnail view for the task if attached, null otherwise
+     * @return the thumbnail view if laid out
      */
-    public @Nullable View getThumbnailViewForTask(int taskId) {
-        TaskItemView view = mTaskAdapter.getTaskItemView(taskId);
-        if (view == null) {
+    public @Nullable View getBottomThumbnailView() {
+        ArrayList<TaskItemView> taskViews = getTaskViews();
+        if (taskViews.isEmpty()) {
             return null;
         }
+        TaskItemView view = taskViews.get(0);
         return view.getThumbnailView();
     }
 
@@ -231,13 +302,14 @@
      */
     private void animateClearAllTasks() {
         setEnabled(false);
-        TaskItemView[] itemViews = getTaskViews();
+        ArrayList<TaskItemView> itemViews = getTaskViews();
 
         AnimatorSet clearAnim = new AnimatorSet();
         long currentDelay = 0;
 
         // Animate each item view to the right and fade out.
-        for (TaskItemView itemView : itemViews) {
+        for (int i = 0, size = itemViews.size(); i < size; i++) {
+            TaskItemView itemView = itemViews.get(i);
             PropertyValuesHolder transXproperty = PropertyValuesHolder.ofFloat(TRANSLATION_X,
                     0, itemView.getWidth() * ITEM_ANIMATE_OUT_TRANSLATION_X_RATIO);
             PropertyValuesHolder alphaProperty = PropertyValuesHolder.ofFloat(ALPHA, 1.0f, 0f);
@@ -272,7 +344,8 @@
         clearAnim.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
-                for (TaskItemView itemView : itemViews) {
+                for (int i = 0, size = itemViews.size(); i < size; i++) {
+                    TaskItemView itemView = itemViews.get(i);
                     itemView.setTranslationX(0);
                     itemView.setAlpha(1.0f);
                 }
@@ -287,13 +360,16 @@
     /**
      * Get attached task item views ordered by most recent.
      *
-     * @return array of attached task item views
+     * @return array list of attached task item views
      */
-    private TaskItemView[] getTaskViews() {
+    private ArrayList<TaskItemView> getTaskViews() {
         int taskCount = mTaskRecyclerView.getChildCount();
-        TaskItemView[] itemViews = new TaskItemView[taskCount];
+        ArrayList<TaskItemView> itemViews = new ArrayList<>();
         for (int i = 0; i < taskCount; i ++) {
-            itemViews[i] = (TaskItemView) mTaskRecyclerView.getChildAt(i);
+            View child = mTaskRecyclerView.getChildAt(i);
+            if (child instanceof TaskItemView) {
+                itemViews.add((TaskItemView) child);
+            }
         }
         return itemViews;
     }
@@ -303,12 +379,14 @@
      * of tasks.
      */
     private void updateContentViewVisibility() {
-        int taskListSize = mTaskLoader.getCurrentTaskList().size();
-        if (mEmptyView.getVisibility() != VISIBLE && taskListSize == 0) {
+        int taskListSize = mTaskAdapter.getItemCount() - TASKS_START_POSITION;
+        if (mShowingContentView != mEmptyView && taskListSize == 0) {
+            mShowingContentView = mEmptyView;
             crossfadeViews(mEmptyView, mContentView);
             mActivityHelper.leaveRecents();
         }
-        if (mContentView.getVisibility() != VISIBLE && taskListSize > 0) {
+        if (mShowingContentView != mContentView && taskListSize > 0) {
+            mShowingContentView = mContentView;
             crossfadeViews(mContentView, mEmptyView);
         }
     }
@@ -320,6 +398,7 @@
      * @param fadeOutView view that should fade out
      */
     private void crossfadeViews(View fadeInView, View fadeOutView) {
+        fadeInView.animate().cancel();
         fadeInView.setVisibility(VISIBLE);
         fadeInView.setAlpha(0f);
         fadeInView.animate()
@@ -327,6 +406,7 @@
                 .setDuration(CROSSFADE_DURATION)
                 .setListener(null);
 
+        fadeOutView.animate().cancel();
         fadeOutView.animate()
                 .alpha(0f)
                 .setDuration(CROSSFADE_DURATION)
@@ -338,17 +418,56 @@
                 });
     }
 
-    private static LayoutAnimationController createLayoutAnimation() {
-        AnimationSet anim = new AnimationSet(false /* shareInterpolator */);
+    /**
+     * Schedule a one-shot layout animation on the next layout. Separate from
+     * {@link #scheduleLayoutAnimation()} as the animation is {@link Animator} based and acts on the
+     * view properties themselves, allowing more controllable behavior and making it easier to
+     * manage when the animation conflicts with another animation.
+     */
+    private void scheduleFadeInLayoutAnimation() {
+        mTaskRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(
+                new ViewTreeObserver.OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        animateFadeInLayoutAnimation();
+                        mTaskRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                    }
+                });
+    }
 
-        Animation alphaAnim = new AlphaAnimation(0, 1);
-        alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION);
-        anim.addAnimation(alphaAnim);
-
-        LayoutAnimationController layoutAnim = new LayoutAnimationController(anim);
-        layoutAnim.setDelay(
-                (float) LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN / LAYOUT_ITEM_ANIMATE_IN_DURATION);
-
-        return layoutAnim;
+    /**
+     * Start animating the layout animation where items fade in.
+     */
+    private void animateFadeInLayoutAnimation() {
+        if (mLayoutAnimation != null) {
+            // If layout animation still in progress, cancel and restart.
+            mLayoutAnimation.cancel();
+        }
+        ArrayList<TaskItemView> views = getTaskViews();
+        int delay = 0;
+        mLayoutAnimation = new AnimatorSet();
+        for (int i = 0, size = views.size(); i < size; i++) {
+            TaskItemView view = views.get(i);
+            view.setAlpha(0.0f);
+            Animator alphaAnim = ObjectAnimator.ofFloat(view, ALPHA, 0.0f, 1.0f);
+            alphaAnim.setDuration(LAYOUT_ITEM_ANIMATE_IN_DURATION).setStartDelay(delay);
+            alphaAnim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    view.setAlpha(1.0f);
+                    mLayingOutViews.remove(view);
+                }
+            });
+            delay += LAYOUT_ITEM_ANIMATE_IN_DELAY_BETWEEN;
+            mLayoutAnimation.play(alphaAnim);
+            mLayingOutViews.add(view);
+        }
+        mLayoutAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mLayoutAnimation = null;
+            }
+        });
+        mLayoutAnimation.start();
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
index d831b20..7d9916e 100644
--- a/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
+++ b/go/quickstep/src/com/android/quickstep/views/TaskItemView.java
@@ -15,16 +15,21 @@
  */
 package com.android.quickstep.views;
 
+import static com.android.quickstep.views.TaskLayoutUtils.getTaskHeight;
+
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Bitmap;
-import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
+import android.util.FloatProperty;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.launcher3.R;
@@ -34,16 +39,41 @@
  */
 public final class TaskItemView extends LinearLayout {
 
+    private static final String EMPTY_LABEL = "";
     private static final String DEFAULT_LABEL = "...";
     private final Drawable mDefaultIcon;
+    private final Drawable mDefaultThumbnail;
+    private final TaskLayerDrawable mIconDrawable;
+    private final TaskLayerDrawable mThumbnailDrawable;
     private TextView mLabelView;
     private ImageView mIconView;
     private ImageView mThumbnailView;
+    private float mContentTransitionProgress;
+
+    /**
+     * Property representing the content transition progress of the view. 1.0f represents that the
+     * currently bound icon, thumbnail, and label are fully animated in and visible.
+     */
+    public static FloatProperty CONTENT_TRANSITION_PROGRESS =
+            new FloatProperty<TaskItemView>("taskContentTransitionProgress") {
+                @Override
+                public void setValue(TaskItemView view, float progress) {
+                    view.setContentTransitionProgress(progress);
+                }
+
+                @Override
+                public Float get(TaskItemView view) {
+                    return view.mContentTransitionProgress;
+                }
+            };
 
     public TaskItemView(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mDefaultIcon = context.getResources().getDrawable(
-                android.R.drawable.sym_def_app_icon, context.getTheme());
+        Resources res = context.getResources();
+        mDefaultIcon = res.getDrawable(android.R.drawable.sym_def_app_icon, context.getTheme());
+        mDefaultThumbnail = res.getDrawable(R.drawable.default_thumbnail, context.getTheme());
+        mIconDrawable = new TaskLayerDrawable(context);
+        mThumbnailDrawable = new TaskLayerDrawable(context);
     }
 
     @Override
@@ -52,15 +82,28 @@
         mLabelView = findViewById(R.id.task_label);
         mThumbnailView = findViewById(R.id.task_thumbnail);
         mIconView = findViewById(R.id.task_icon);
+
+        mThumbnailView.setImageDrawable(mThumbnailDrawable);
+        mIconView.setImageDrawable(mIconDrawable);
+
+        resetToEmptyUi();
+        CONTENT_TRANSITION_PROGRESS.setValue(this, 1.0f);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int taskHeight = getTaskHeight(getContext());
+        int newHeightSpec = MeasureSpec.makeMeasureSpec(taskHeight,MeasureSpec.EXACTLY);
+        super.onMeasure(widthMeasureSpec, newHeightSpec);
     }
 
     /**
-     * Resets task item view to default values.
+     * Resets task item view to empty, loading UI.
      */
-    public void resetTaskItemView() {
-        setLabel(DEFAULT_LABEL);
-        setIcon(null);
-        setThumbnail(null);
+    public void resetToEmptyUi() {
+        mIconDrawable.resetDrawable();
+        mThumbnailDrawable.resetDrawable();
+        setLabel(EMPTY_LABEL);
     }
 
     /**
@@ -69,11 +112,8 @@
      * @param label task label
      */
     public void setLabel(@Nullable String label) {
-        if (label == null) {
-            mLabelView.setText(DEFAULT_LABEL);
-            return;
-        }
-        mLabelView.setText(label);
+        mLabelView.setText(getSafeLabel(label));
+        // TODO: Animation for label
     }
 
     /**
@@ -86,11 +126,7 @@
         // The icon proper is actually smaller than the drawable and has "padding" on the side for
         // the purpose of drawing the shadow, allowing the icon to pop up, so we need to scale the
         // view if we want the icon to be flush with the bottom of the thumbnail.
-        if (icon == null) {
-            mIconView.setImageDrawable(mDefaultIcon);
-            return;
-        }
-        mIconView.setImageDrawable(icon);
+        mIconDrawable.setCurrentDrawable(getSafeIcon(icon));
     }
 
     /**
@@ -99,16 +135,48 @@
      * @param thumbnail task thumbnail for the task
      */
     public void setThumbnail(@Nullable Bitmap thumbnail) {
-        if (thumbnail == null) {
-            mThumbnailView.setImageBitmap(null);
-            mThumbnailView.setBackgroundColor(Color.GRAY);
-            return;
-        }
-        mThumbnailView.setBackgroundColor(Color.TRANSPARENT);
-        mThumbnailView.setImageBitmap(thumbnail);
+        mThumbnailDrawable.setCurrentDrawable(getSafeThumbnail(thumbnail));
     }
 
     public View getThumbnailView() {
         return mThumbnailView;
     }
+
+    /**
+     * Start a new animation from the current task content to the specified new content. The caller
+     * is responsible for the actual animation control via the property
+     * {@link #CONTENT_TRANSITION_PROGRESS}.
+     *
+     * @param endIcon the icon to animate to
+     * @param endThumbnail the thumbnail to animate to
+     * @param endLabel the label to animate to
+     */
+    public void startContentAnimation(@Nullable Drawable endIcon, @Nullable Bitmap endThumbnail,
+            @Nullable String endLabel) {
+        mIconDrawable.startNewTransition(getSafeIcon(endIcon));
+        mThumbnailDrawable.startNewTransition(getSafeThumbnail(endThumbnail));
+        // TODO: Animation for label
+
+        setContentTransitionProgress(0.0f);
+    }
+
+    private void setContentTransitionProgress(float progress) {
+        mContentTransitionProgress = progress;
+        mIconDrawable.setTransitionProgress(progress);
+        mThumbnailDrawable.setTransitionProgress(progress);
+        // TODO: Animation for label
+    }
+
+    private @NonNull Drawable getSafeIcon(@Nullable Drawable icon) {
+        return (icon != null) ? icon : mDefaultIcon;
+    }
+
+    private @NonNull Drawable getSafeThumbnail(@Nullable Bitmap thumbnail) {
+        return (thumbnail != null) ? new BitmapDrawable(getResources(), thumbnail)
+                                   : mDefaultThumbnail;
+    }
+
+    private @NonNull String getSafeLabel(@Nullable String label) {
+        return (label != null) ? label : DEFAULT_LABEL;
+    }
 }
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
index 3a23048..98b66b9 100644
--- a/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
+++ b/go/quickstep/src/com/android/quickstep/views/TaskLayerDrawable.java
@@ -31,6 +31,7 @@
  */
 public final class TaskLayerDrawable extends LayerDrawable {
     private final Drawable mEmptyDrawable;
+    private float mProgress;
 
     public TaskLayerDrawable(Context context) {
         super(new Drawable[0]);
@@ -50,6 +51,7 @@
      */
     public void setCurrentDrawable(@NonNull Drawable drawable) {
         setDrawable(0, drawable);
+        applyTransitionProgress(mProgress);
     }
 
     /**
@@ -82,9 +84,18 @@
         if (progress > 1 || progress < 0) {
             throw new IllegalArgumentException("Transition progress should be between 0 and 1");
         }
+        mProgress = progress;
+        applyTransitionProgress(progress);
+    }
+
+    private void applyTransitionProgress(float progress) {
         int drawableAlpha = (int) (progress * 255);
         getDrawable(0).setAlpha(drawableAlpha);
-        getDrawable(1).setAlpha(255 - drawableAlpha);
+        if (getDrawable(0) != getDrawable(1)) {
+            // Only do this if it's a different drawable so that it fades out.
+            // Otherwise, we'd just be overwriting the front drawable's alpha.
+            getDrawable(1).setAlpha(255 - drawableAlpha);
+        }
         invalidateSelf();
     }
 }
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java b/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java
new file mode 100644
index 0000000..e28a9e0
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskLayoutUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2019 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.quickstep.views;
+
+import static com.android.quickstep.TaskAdapter.MAX_TASKS_TO_DISPLAY;
+
+import android.content.Context;
+
+import com.android.launcher3.InvariantDeviceProfile;
+
+/**
+ * Utils to determine dynamically task and view sizes based off the device height and width.
+ */
+public final class TaskLayoutUtils {
+
+    private static final float CLEAR_ALL_ITEM_TO_HEIGHT_RATIO = 7.0f / 64;
+
+    private TaskLayoutUtils() {}
+
+    /**
+     * Calculate task height based off the available height in portrait mode such that when the
+     * recents list is full, the total height fills in the available device height perfectly. In
+     * landscape mode, we keep the same task height so that tasks scroll off the top.
+     *
+     * @param context current context
+     * @return task height
+     */
+    public static int getTaskHeight(Context context) {
+        final int availableHeight =
+                InvariantDeviceProfile.INSTANCE.get(context).portraitProfile.availableHeightPx;
+        final int availableTaskSpace = availableHeight - getClearAllItemHeight(context);
+        return (int) (availableTaskSpace * 1.0f / MAX_TASKS_TO_DISPLAY);
+    }
+
+    /**
+     * Calculate clear all item height scaled to available height in portrait mode.
+     *
+     * @param context current context
+     * @return clear all item height
+     */
+    public static int getClearAllItemHeight(Context context) {
+        final int availableHeight =
+                InvariantDeviceProfile.INSTANCE.get(context).portraitProfile.availableHeightPx;
+        return (int) (CLEAR_ALL_ITEM_TO_HEIGHT_RATIO * availableHeight);
+    }
+}
diff --git a/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java b/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
new file mode 100644
index 0000000..b1c60dd
--- /dev/null
+++ b/go/quickstep/src/com/android/quickstep/views/TaskThumbnailIconView.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2019 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.quickstep.views;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.launcher3.R;
+
+/**
+ * Square view that holds thumbnail and icon and shrinks them appropriately so that both fit nicely
+ * within the view. Side length is determined by height.
+ */
+final class TaskThumbnailIconView extends ViewGroup {
+    private final Rect mTmpFrameRect = new Rect();
+    private final Rect mTmpChildRect = new Rect();
+    private View mThumbnailView;
+    private View mIconView;
+    private static final float SUBITEM_FRAME_RATIO = .6f;
+
+    public TaskThumbnailIconView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mThumbnailView = findViewById(R.id.task_thumbnail);
+        mIconView = findViewById(R.id.task_icon);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int height = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
+        int width = height;
+        setMeasuredDimension(width, height);
+
+        int subItemSize = (int) (SUBITEM_FRAME_RATIO * height);
+        if (mThumbnailView.getVisibility() != GONE) {
+            int thumbnailHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+            int thumbnailWidthSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+            measureChild(mThumbnailView, thumbnailWidthSpec, thumbnailHeightSpec);
+        }
+        if (mIconView.getVisibility() != GONE) {
+            int iconHeightSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+            int iconWidthSpec = MeasureSpec.makeMeasureSpec(subItemSize, MeasureSpec.EXACTLY);
+            measureChild(mIconView, iconWidthSpec, iconHeightSpec);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        mTmpFrameRect.left = getPaddingLeft();
+        mTmpFrameRect.right = right - left - getPaddingRight();
+        mTmpFrameRect.top = getPaddingTop();
+        mTmpFrameRect.bottom = bottom - top - getPaddingBottom();
+
+        // Layout the thumbnail to the top-start corner of the view
+        if (mThumbnailView.getVisibility() != GONE) {
+            final int width = mThumbnailView.getMeasuredWidth();
+            final int height = mThumbnailView.getMeasuredHeight();
+
+            final int thumbnailGravity = Gravity.TOP | Gravity.START;
+            Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
+
+            mThumbnailView.layout(mTmpChildRect.left, mTmpChildRect.top,
+                    mTmpChildRect.right, mTmpChildRect.bottom);
+        }
+
+        // Layout the icon to the bottom-end corner of the view
+        if (mIconView.getVisibility() != GONE) {
+            final int width = mIconView.getMeasuredWidth();
+            final int height = mIconView.getMeasuredHeight();
+
+            int thumbnailGravity = Gravity.BOTTOM | Gravity.END;
+            Gravity.apply(thumbnailGravity, width, height, mTmpFrameRect, mTmpChildRect);
+
+            mIconView.layout(mTmpChildRect.left, mTmpChildRect.top,
+                    mTmpChildRect.right, mTmpChildRect.bottom);
+        }
+    }
+}