Enable variable size thumbnail for large screen

- Introduced a feature flag
- Refresh TaskView size when thumbnail is refreshed or Recents resizes
- Disable edge scale down in large screens
- Added temp method to get task width/height ratio
- In fullscreen, scale down TaskThumbnailView and disable TaskView elevation
- Task boxing mechanism: http://screen/3NkePthAVUVH2Rv.png

Bug: 174464656
Test: Manually test overview and quickswitch for folded and unfolded, including with RTL.
Change-Id: I2ce24d588a246cc6a2408039a37d884021aa0800
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 7675a79..c2e5cda 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -32,6 +32,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.touch.BaseSwipeDetector;
 import com.android.launcher3.touch.PagedOrientationHandler;
 import com.android.launcher3.touch.SingleAxisSwipeDetector;
@@ -150,9 +151,11 @@
                         mTaskBeingDragged = view;
                         int upDirection = mRecentsView.getPagedOrientationHandler()
                                 .getUpDirection(mIsRtl);
-                        if (!SysUINavigationMode.getMode(mActivity).hasGestures) {
+                        if (!SysUINavigationMode.getMode(mActivity).hasGestures || (
+                                mActivity.getDeviceProfile().isTablet
+                                        && FeatureFlags.ENABLE_OVERVIEW_GRID.get())) {
                             // Don't allow swipe down to open if we don't support swipe up
-                            // to enter overview.
+                            // to enter overview, or when grid layout is enabled.
                             directionsToDetectScroll = upDirection;
                             mAllowGoingUp = true;
                             mAllowGoingDown = false;
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index 9da306e..25c0928 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -49,6 +49,7 @@
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
 import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.ItemInfo;
 import com.android.launcher3.statehandlers.DepthController;
 import com.android.launcher3.statemanager.StateManager;
@@ -174,11 +175,13 @@
 
         final RecentsView recentsView = v.getRecentsView();
         int taskIndex = recentsView.indexOfChild(v);
-        boolean parallaxCenterAndAdjacentTask = taskIndex != recentsView.getCurrentPage();
-        int startScroll = recentsView.getScrollOffset(taskIndex);
-
         Context context = v.getContext();
         DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile();
+        boolean parallaxCenterAndAdjacentTask =
+                taskIndex != recentsView.getCurrentPage() && !(dp.isTablet
+                        && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
+        int startScroll = recentsView.getScrollOffset(taskIndex);
+
         TaskViewSimulator topMostSimulator = null;
 
         if (tsv == null && targets.apps.length > 0) {
diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
index 65bcf26..6f16781 100644
--- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
+++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java
@@ -276,8 +276,9 @@
             int start = mOrientationState.getOrientationHandler()
                     .getPrimaryValue(mTaskRect.left, mTaskRect.top);
             mScrollState.screenCenter = start + mScrollState.scroll + mScrollState.halfPageSize;
-            mScrollState.updateInterpolation(start);
-            mCurveScale = TaskView.getCurveScaleForInterpolation(mScrollState.linearInterpolation);
+            mScrollState.updateInterpolation(mDp, start);
+            mCurveScale = TaskView.getCurveScaleForInterpolation(mDp,
+                    mScrollState.linearInterpolation);
         }
 
         float progress = Utilities.boundToRange(fullScreenProgress.value, 0, 1);
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index d9d0a93..38d488c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -898,9 +898,14 @@
     public void setFullscreenProgress(float fullscreenProgress) {
         mFullscreenProgress = fullscreenProgress;
         int taskCount = getTaskViewCount();
+        float accumulatedTranslationX = 0;
         for (int i = 0; i < taskCount; i++) {
-            getTaskViewAt(i).setFullscreenProgress(mFullscreenProgress);
+            TaskView taskView = getTaskViewAt(i);
+            taskView.setFullscreenProgress(mFullscreenProgress);
+            taskView.setAccumulatedTranslationX(accumulatedTranslationX);
+            accumulatedTranslationX += taskView.getFullscreenTranslationX();
         }
+
         // Fade out the actions view quickly (0.1 range)
         mActionsView.getFullscreenAlpha().setValue(
                 mapToRange(fullscreenProgress, 0, 0.1f, 1f, 0f, LINEAR));
@@ -934,6 +939,11 @@
         setPadding(mTempRect.left - mInsets.left, mTempRect.top - mInsets.top,
                 dp.widthPx - mInsets.right - mTempRect.right,
                 dp.heightPx - mInsets.bottom - mTempRect.bottom);
+        // Force TaskView to update size from thumbnail
+        final int taskCount = getTaskViewCount();
+        for (int i = 0; i < taskCount; i++) {
+            getTaskViewAt(i).updateTaskSize();
+        }
     }
 
     public void getTaskSize(Rect outRect) {
@@ -942,6 +952,11 @@
         mLastComputedTaskSize.set(outRect);
     }
 
+    /** Gets the last computed task size */
+    public Rect getLastComputedTaskSize() {
+        return mLastComputedTaskSize;
+    }
+
     /** Gets the task size for modal state. */
     public void getModalTaskSize(Rect outRect) {
         mSizeStrategy.calculateModalTaskSize(mActivity, mActivity.getDeviceProfile(), outRect);
@@ -987,7 +1002,7 @@
         final int pageCount = getPageCount();
         for (int i = 0; i < pageCount; i++) {
             View page = getPageAt(i);
-            mScrollState.updateInterpolation(
+            mScrollState.updateInterpolation(mActivity.getDeviceProfile(),
                     mOrientationHandler.getChildStartWithTranslation(page));
             ((PageCallbacks) page).onPageScroll(mScrollState);
         }
@@ -1422,13 +1437,13 @@
         /**
          * Updates linearInterpolation for the provided child position
          */
-        public void updateInterpolation(float childStart) {
+        public void updateInterpolation(DeviceProfile deviceProfile, float childStart) {
             float pageCenter = childStart + halfPageSize;
             float distanceFromScreenCenter = screenCenter - pageCenter;
             // How far the page has to move from the center to be offscreen, taking into account
             // the EDGE_SCALE_DOWN_FACTOR that will be applied at that position.
             float distanceToReachEdge = halfScreenSize
-                    + halfPageSize * (1 - TaskView.EDGE_SCALE_DOWN_FACTOR);
+                    + halfPageSize * (1 - TaskView.getEdgeScaleDownFactor(deviceProfile));
             linearInterpolation = Math.min(1,
                     Math.abs(distanceFromScreenCenter) / distanceToReachEdge);
         }
@@ -1444,12 +1459,13 @@
         }
     }
 
-    private void addDismissedTaskAnimations(View taskView, long duration, PendingAnimation anim) {
+    private void addDismissedTaskAnimations(TaskView taskView, long duration,
+            PendingAnimation anim) {
         // Use setFloat instead of setViewAlpha as we want to keep the view visible even when it's
         // alpha is set to 0 so that it can be recycled in the view pool properly
         anim.setFloat(taskView, VIEW_ALPHA, 0, ACCEL_2);
-        FloatProperty<View> secondaryViewTranslate =
-            mOrientationHandler.getSecondaryViewTranslate();
+        FloatProperty<TaskView> secondaryViewTranslate =
+                taskView.getDismissTaskTranslationProperty();
         int secondaryTaskDimension = mOrientationHandler.getSecondaryDimension(taskView);
         int verticalFactor = mOrientationHandler.getSecondaryTranslationDirectionFactor();
 
@@ -1515,7 +1531,7 @@
                 int scrollDiff = newScroll[i] - oldScroll[i] + offset;
                 if (scrollDiff != 0) {
                     FloatProperty translationProperty = child instanceof TaskView
-                            ? ((TaskView) child).getPrimaryFillDismissGapTranslationProperty()
+                            ? ((TaskView) child).getFillDismissGapTranslationProperty()
                             : mOrientationHandler.getPrimaryViewTranslate();
 
                     ResourceProvider rp = DynamicResource.provider(mActivity);
@@ -1971,7 +1987,8 @@
         // Find the task's scale based on its offscreen progress, then see how far it still needs to
         // move to be completely offscreen.
         Utilities.scaleRectFAboutCenter(taskPosition,
-                TaskView.getCurveScaleForInterpolation(centerToOffscreenProgress));
+                TaskView.getCurveScaleForInterpolation(mActivity.getDeviceProfile(),
+                        centerToOffscreenProgress));
         distanceToOffscreen = desiredLeft - taskPosition.left;
         // Finally, we need to account for RecentsView scale, because it moves tasks based on its
         // pivot. To do this, we move the task position to where it would be offscreen at scale = 1
@@ -1990,7 +2007,7 @@
         mTaskViewsSecondaryTranslation = translation;
         for (int i = 0; i < getTaskViewCount(); i++) {
             TaskView task = getTaskViewAt(i);
-            mOrientationHandler.getSecondaryViewTranslate().set(task, translation / getScaleY());
+            task.getTaskResistanceTranslationProperty().set(task, translation / getScaleY());
         }
         mLiveTileTaskViewSimulator.recentsViewSecondaryTranslation.value = translation;
     }
@@ -2359,6 +2376,52 @@
     }
 
     @Override
+    protected boolean getPageScrolls(int[] outPageScrolls, boolean layoutChildren,
+            ComputePageScrollsLogic scrollLogic) {
+        boolean pageScrollChanged = super.getPageScrolls(outPageScrolls, layoutChildren,
+                scrollLogic);
+
+        final int taskCount = getTaskViewCount();
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            if (childCount < mTaskViewStartIndex) {
+                continue;
+            }
+
+            final TaskView taskView = getTaskViewAt(
+                    Utilities.boundToRange(i, mTaskViewStartIndex, taskCount - 1));
+            float scrollDiff =
+                    taskView.getFullscreenTranslationX() + taskView.getAccumulatedTranslationX();
+            if (scrollDiff != 0) {
+                outPageScrolls[i] += scrollDiff;
+                pageScrollChanged = true;
+            }
+        }
+        return pageScrollChanged;
+    }
+
+    @Override
+    protected int getChildOffset(int index) {
+        if (index < mTaskViewStartIndex) {
+            return super.getChildOffset(index);
+        }
+
+        final TaskView taskView = getTaskViewAt(
+                Utilities.boundToRange(index, mTaskViewStartIndex, getTaskViewCount() - 1));
+        return super.getChildOffset(index) + (int) taskView.getFullscreenTranslationX()
+                + (int) taskView.getAccumulatedTranslationX();
+    }
+
+    @Override
+    protected int getChildVisibleSize(int index) {
+        final TaskView taskView = getTaskViewAtByAbsoluteIndex(index);
+        if (taskView == null) {
+            return super.getChildVisibleSize(index);
+        }
+        return super.getChildVisibleSize(index) - (int) taskView.getFullscreenTranslationX();
+    }
+
+    @Override
     protected int computeMaxScroll() {
         if (getTaskViewCount() > 0) {
             if (mDisallowScrollToClearAll) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
index 445e490..3bd883d 100644
--- a/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskThumbnailView.java
@@ -51,6 +51,7 @@
 import com.android.launcher3.DeviceProfile;
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
 import com.android.launcher3.util.MainThreadInitializedObject;
 import com.android.launcher3.util.SystemUiController;
@@ -110,6 +111,9 @@
     private boolean mOverlayEnabled;
     private OverviewScreenshotActions mOverviewScreenshotActionsPlugin;
 
+    // TODO(b/179466077): Remove when proper API is ready.
+    private Float mThumbnailRatio = null;
+
     public TaskThumbnailView(Context context) {
         this(context, null);
     }
@@ -450,6 +454,31 @@
         return mThumbnailData.isRealSnapshot;
     }
 
+    // TODO(b/179466077): Remove when proper API is ready.
+    public float getThumbnailRatio() {
+        // API is ready.
+        if (mThumbnailRatio != null) {
+            return mThumbnailRatio;
+        }
+
+        if (mThumbnailData == null || mThumbnailData.thumbnail == null) {
+            final float[] thumbnailRatios =
+                    new float[]{0.8882452f, 1.2834098f, 0.5558415f, 2.15625f};
+            // Use key's hash code to return a deterministic thumbnail ratio.
+            mThumbnailRatio = thumbnailRatios[mTask.key.hashCode() % thumbnailRatios.length];
+            return mThumbnailRatio;
+        }
+
+        float surfaceWidth = mThumbnailData.thumbnail.getWidth() / mThumbnailData.scale;
+        float surfaceHeight = mThumbnailData.thumbnail.getHeight() / mThumbnailData.scale;
+        float availableWidth = surfaceWidth
+                - (mThumbnailData.insets.left + mThumbnailData.insets.right);
+        float availableHeight = surfaceHeight
+                - (mThumbnailData.insets.top + mThumbnailData.insets.bottom);
+        mThumbnailRatio = availableWidth / availableHeight;
+        return mThumbnailRatio;
+    }
+
     /**
      * Utility class to position the thumbnail in the TaskView
      */
@@ -480,9 +509,11 @@
             float scale = thumbnailData.scale;
             final float thumbnailScale;
 
-            // Landscape vs portrait change
+            // Landscape vs portrait change.
+            // Note: Disable rotation in grid layout.
             boolean windowingModeSupportsRotation = !dp.isMultiWindowMode
-                    && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN;
+                    && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN
+                    && !(dp.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get());
             isOrientationDifferent = isOrientationChange(deltaRotate)
                     && windowingModeSupportsRotation;
             if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) {
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 59cf3b2..6709c2e 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -30,6 +30,8 @@
 import static com.android.launcher3.QuickstepAppTransitionManagerImpl.RECENTS_LAUNCH_DURATION;
 import static com.android.launcher3.Utilities.comp;
 import static com.android.launcher3.Utilities.getDescendantCoordRelativeToAncestor;
+import static com.android.launcher3.anim.Interpolators.ACCEL_DEACCEL;
+import static com.android.launcher3.anim.Interpolators.EXAGGERATED_EASE;
 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN;
 import static com.android.launcher3.anim.Interpolators.LINEAR;
 import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR;
@@ -61,6 +63,7 @@
 import android.view.Surface;
 import android.view.TouchDelegate;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.ViewOutlineProvider;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.widget.FrameLayout;
@@ -72,6 +75,7 @@
 import com.android.launcher3.Utilities;
 import com.android.launcher3.anim.AnimatorPlaybackController;
 import com.android.launcher3.anim.Interpolators;
+import com.android.launcher3.config.FeatureFlags;
 import com.android.launcher3.model.data.WorkspaceItemInfo;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.statemanager.StatefulActivity;
@@ -120,10 +124,8 @@
      */
     public static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
 
-    /**
-     * How much to scale down pages near the edge of the screen.
-     */
-    public static final float EDGE_SCALE_DOWN_FACTOR = 0.03f;
+    private static final float EDGE_SCALE_DOWN_FACTOR_CAROUSEL = 0.03f;
+    private static final float EDGE_SCALE_DOWN_FACTOR_GRID = 0.00f;
 
     public static final long SCALE_ICON_DURATION = 120;
     private static final long DIM_ANIM_DURATION = 700;
@@ -152,29 +154,29 @@
                 }
             };
 
-    private static final FloatProperty<TaskView> FILL_DISMISS_GAP_TRANSLATION_X =
-            new FloatProperty<TaskView>("fillDismissGapTranslationX") {
+    private static final FloatProperty<TaskView> DISMISS_TRANSLATION_X =
+            new FloatProperty<TaskView>("dismissTranslationX") {
                 @Override
                 public void setValue(TaskView taskView, float v) {
-                    taskView.setFillDismissGapTranslationX(v);
+                    taskView.setDismissTranslationX(v);
                 }
 
                 @Override
                 public Float get(TaskView taskView) {
-                    return taskView.mFillDismissGapTranslationX;
+                    return taskView.mDismissTranslationX;
                 }
             };
 
-    private static final FloatProperty<TaskView> FILL_DISMISS_GAP_TRANSLATION_Y =
-            new FloatProperty<TaskView>("fillDismissGapTranslationY") {
+    private static final FloatProperty<TaskView> DISMISS_TRANSLATION_Y =
+            new FloatProperty<TaskView>("dismissTranslationY") {
                 @Override
                 public void setValue(TaskView taskView, float v) {
-                    taskView.setFillDismissGapTranslationY(v);
+                    taskView.setDismissTranslationY(v);
                 }
 
                 @Override
                 public Float get(TaskView taskView) {
-                    return taskView.mFillDismissGapTranslationY;
+                    return taskView.mDismissTranslationY;
                 }
             };
 
@@ -204,6 +206,32 @@
                 }
             };
 
+    private static final FloatProperty<TaskView> TASK_RESISTANCE_TRANSLATION_X =
+            new FloatProperty<TaskView>("taskResistanceTranslationX") {
+                @Override
+                public void setValue(TaskView taskView, float v) {
+                    taskView.setTaskResistanceTranslationX(v);
+                }
+
+                @Override
+                public Float get(TaskView taskView) {
+                    return taskView.mTaskResistanceTranslationX;
+                }
+            };
+
+    private static final FloatProperty<TaskView> TASK_RESISTANCE_TRANSLATION_Y =
+            new FloatProperty<TaskView>("taskResistanceTranslationY") {
+                @Override
+                public void setValue(TaskView taskView, float v) {
+                    taskView.setTaskResistanceTranslationY(v);
+                }
+
+                @Override
+                public Float get(TaskView taskView) {
+                    return taskView.mTaskResistanceTranslationY;
+                }
+            };
+
     private final OnAttachStateChangeListener mTaskMenuStateListener =
             new OnAttachStateChangeListener() {
                 @Override
@@ -228,15 +256,22 @@
     private final DigitalWellBeingToast mDigitalWellBeingToast;
     private float mCurveScale;
     private float mFullscreenProgress;
+    private float mScaleAtFullscreen = 1;
+    private float mFullscreenScale = 1;
     private final FullscreenDrawParams mCurrentFullscreenParams;
     private final StatefulActivity mActivity;
 
     // Various causes of changing primary translation, which we aggregate to setTranslationX/Y().
-    // TODO: We should do this for secondary translation properties as well.
-    private float mFillDismissGapTranslationX;
-    private float mFillDismissGapTranslationY;
+    private float mDismissTranslationX;
+    private float mDismissTranslationY;
     private float mTaskOffsetTranslationX;
     private float mTaskOffsetTranslationY;
+    private float mTaskResistanceTranslationX;
+    private float mTaskResistanceTranslationY;
+    // The following translation variables should only be used in the same orientation as Launcher.
+    private float mFullscreenTranslationX;
+    private float mAccumulatedTranslationX;
+    private float mBoxTranslationY;
 
     private ObjectAnimator mIconAndDimAnimator;
     private float mIconScaleAnimStartProgress = 0;
@@ -425,6 +460,7 @@
         cancelPendingLoadTasks();
         mTask = task;
         mSnapshotView.bind(task);
+        updateTaskSize();
         setOrientationState(orientedState);
     }
 
@@ -668,10 +704,12 @@
 
     protected void resetViewTransforms() {
         setCurveScale(1);
-        mFillDismissGapTranslationX = mTaskOffsetTranslationX = 0f;
-        mFillDismissGapTranslationY = mTaskOffsetTranslationY = 0f;
-        setTranslationX(0f);
-        setTranslationY(0f);
+        // fullscreenTranslation and accumulatedTranslation should not be reset, as
+        // resetViewTransforms is called during Quickswitch scrolling.
+        mDismissTranslationX = mTaskOffsetTranslationX = mTaskResistanceTranslationX = 0f;
+        mDismissTranslationY = mTaskOffsetTranslationY = mTaskResistanceTranslationY = 0f;
+        applyTranslationX();
+        applyTranslationY();
         setTranslationZ(0);
         setAlpha(mStableAlpha);
         setIconScaleAndDim(1);
@@ -684,6 +722,7 @@
 
     @Override
     public void onRecycle() {
+        mFullscreenTranslationX = mAccumulatedTranslationX = mBoxTranslationY = 0f;
         resetViewTransforms();
         // Clear any references to the thumbnail (it will be re-read either from the cache or the
         // system on next bind)
@@ -702,7 +741,7 @@
         float curveInterpolation =
                 CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation);
         float curveScaleForCurveInterpolation = getCurveScaleForCurveInterpolation(
-                curveInterpolation);
+                mActivity.getDeviceProfile(), curveInterpolation);
         mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
         setCurveScale(curveScaleForCurveInterpolation);
 
@@ -787,40 +826,70 @@
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
-        setPivotX((right - left) * 0.5f);
-        setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
+        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            setPivotX(getLayoutDirection() == LAYOUT_DIRECTION_RTL ? (right - left) : 0);
+            setPivotY(0);
+        } else {
+            setPivotX((right - left) * 0.5f);
+            setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
+        }
         if (Utilities.ATLEAST_Q) {
             SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight());
             setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT);
         }
     }
 
-    public static float getCurveScaleForInterpolation(float linearInterpolation) {
+    /**
+     * How much to scale down pages near the edge of the screen according to linearInterpolation.
+     */
+    public static float getCurveScaleForInterpolation(DeviceProfile deviceProfile,
+            float linearInterpolation) {
         float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation);
-        return getCurveScaleForCurveInterpolation(curveInterpolation);
+        return getCurveScaleForCurveInterpolation(deviceProfile, curveInterpolation);
     }
 
-    private static float getCurveScaleForCurveInterpolation(float curveInterpolation) {
-        return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
+    private static float getCurveScaleForCurveInterpolation(DeviceProfile deviceProfile,
+            float curveInterpolation) {
+        return 1 - curveInterpolation * getEdgeScaleDownFactor(deviceProfile);
+    }
+
+    /**
+     * How much to scale down pages near the edge of the screen.
+     */
+    public static float getEdgeScaleDownFactor(DeviceProfile deviceProfile) {
+        if (deviceProfile.isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            return EDGE_SCALE_DOWN_FACTOR_GRID;
+        } else {
+            return EDGE_SCALE_DOWN_FACTOR_CAROUSEL;
+        }
     }
 
     private void setCurveScale(float curveScale) {
         mCurveScale = curveScale;
-        setScaleX(mCurveScale);
-        setScaleY(mCurveScale);
+        applyScale();
+    }
+
+    private void setFullscreenScale(float fullscreenScale) {
+        mFullscreenScale = fullscreenScale;
+        applyScale();
+    }
+
+    private void applyScale() {
+        setScaleX(mCurveScale * mFullscreenScale);
+        setScaleY(mCurveScale * mFullscreenScale);
     }
 
     public float getCurveScale() {
         return mCurveScale;
     }
 
-    private void setFillDismissGapTranslationX(float x) {
-        mFillDismissGapTranslationX = x;
+    private void setDismissTranslationX(float x) {
+        mDismissTranslationX = x;
         applyTranslationX();
     }
 
-    private void setFillDismissGapTranslationY(float y) {
-        mFillDismissGapTranslationY = y;
+    private void setDismissTranslationY(float y) {
+        mDismissTranslationY = y;
         applyTranslationY();
     }
 
@@ -834,17 +903,59 @@
         applyTranslationY();
     }
 
+    private void setTaskResistanceTranslationX(float x) {
+        mTaskResistanceTranslationX = x;
+        applyTranslationX();
+    }
+
+    private void setTaskResistanceTranslationY(float y) {
+        mTaskResistanceTranslationY = y;
+        applyTranslationY();
+    }
+
+    private void setFullscreenTranslationX(float fullscreenTranslationX) {
+        mFullscreenTranslationX = fullscreenTranslationX;
+        applyTranslationX();
+    }
+
+    public float getFullscreenTranslationX() {
+        return mFullscreenTranslationX;
+    }
+
+    public void setAccumulatedTranslationX(float accumulatedTranslationX) {
+        mAccumulatedTranslationX = accumulatedTranslationX;
+        applyTranslationX();
+    }
+
+    public float getAccumulatedTranslationX() {
+        return mAccumulatedTranslationX;
+    }
+
+    private void setBoxTranslationY(float boxTranslationY) {
+        mBoxTranslationY = boxTranslationY;
+        applyTranslationY();
+    }
+
     private void applyTranslationX() {
-        setTranslationX(mFillDismissGapTranslationX + mTaskOffsetTranslationX);
+        setTranslationX(
+                mDismissTranslationX + mTaskOffsetTranslationX + mTaskResistanceTranslationX
+                        + mFullscreenTranslationX + mAccumulatedTranslationX);
     }
 
     private void applyTranslationY() {
-        setTranslationY(mFillDismissGapTranslationY + mTaskOffsetTranslationY);
+        setTranslationY(
+                mDismissTranslationY + mTaskOffsetTranslationY + mTaskResistanceTranslationY
+                        + mBoxTranslationY);
     }
 
-    public FloatProperty<TaskView> getPrimaryFillDismissGapTranslationProperty() {
+    public FloatProperty<TaskView> getFillDismissGapTranslationProperty() {
         return getPagedOrientationHandler().getPrimaryValue(
-                FILL_DISMISS_GAP_TRANSLATION_X, FILL_DISMISS_GAP_TRANSLATION_Y);
+                DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y);
+    }
+
+    public FloatProperty<TaskView> getDismissTaskTranslationProperty() {
+        return getPagedOrientationHandler().getSecondaryValue(
+                DISMISS_TRANSLATION_X, DISMISS_TRANSLATION_Y);
     }
 
     public FloatProperty<TaskView> getPrimaryTaskOffsetTranslationProperty() {
@@ -852,6 +963,11 @@
                 TASK_OFFSET_TRANSLATION_X, TASK_OFFSET_TRANSLATION_Y);
     }
 
+    public FloatProperty<TaskView> getTaskResistanceTranslationProperty() {
+        return getPagedOrientationHandler().getSecondaryValue(
+                TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y);
+    }
+
     @Override
     public boolean hasOverlappingRendering() {
         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
@@ -987,6 +1103,8 @@
         setClipChildren(!isFullscreen);
         setClipToPadding(!isFullscreen);
 
+        updateTaskScaling();
+
         TaskThumbnailView thumbnail = getThumbnail();
         updateCurrentFullscreenParams(thumbnail.getPreviewPositionHelper());
 
@@ -1012,6 +1130,83 @@
                 previewPositionHelper);
     }
 
+    void updateTaskSize() {
+        ViewGroup.LayoutParams params = getLayoutParams();
+        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            final int thumbnailPadding = (int) getResources().getDimension(
+                    R.dimen.task_thumbnail_top_margin);
+
+            Rect lastComputedTaskSize = getRecentsView().getLastComputedTaskSize();
+            int taskWidth = lastComputedTaskSize.width();
+            int taskHeight = lastComputedTaskSize.height();
+            int boxLength = Math.max(taskWidth, taskHeight);
+            float thumbnailRatio = mSnapshotView.getThumbnailRatio();
+
+            int expectedWidth;
+            int expectedHeight;
+            if (thumbnailRatio > 1) {
+                expectedWidth = boxLength;
+                expectedHeight = (int) (boxLength / thumbnailRatio) + thumbnailPadding;
+            } else {
+                expectedWidth = (int) (boxLength * thumbnailRatio);
+                expectedHeight = boxLength + thumbnailPadding;
+            }
+
+            float heightDiff = (expectedHeight - thumbnailPadding - taskHeight) / 2.0f;
+            setBoxTranslationY(heightDiff);
+
+            if (expectedWidth > taskWidth) {
+                // In full screen, expectedWidth should not be larger than taskWidth.
+                mScaleAtFullscreen = taskWidth / (float) expectedWidth;
+            } else if (expectedHeight - thumbnailPadding > taskHeight) {
+                // In full screen, expectedHeight should not be larger than taskHeight.
+                mScaleAtFullscreen = taskHeight / (float) (expectedHeight - thumbnailPadding);
+            } else {
+                mScaleAtFullscreen = 1f;
+            }
+
+            if (params.width != expectedWidth || params.height != expectedHeight) {
+                params.width = expectedWidth;
+                params.height = expectedHeight;
+                setLayoutParams(params);
+            }
+        } else {
+            setBoxTranslationY(0);
+            if (params.width != ViewGroup.LayoutParams.MATCH_PARENT) {
+                params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+                params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+                setLayoutParams(params);
+            }
+        }
+        updateTaskScaling();
+    }
+
+    private void updateTaskScaling() {
+        if (mActivity.getDeviceProfile().isTablet && FeatureFlags.ENABLE_OVERVIEW_GRID.get()) {
+            ViewGroup.LayoutParams params = getLayoutParams();
+            if (params.width == ViewGroup.LayoutParams.MATCH_PARENT
+                    || params.height == ViewGroup.LayoutParams.MATCH_PARENT) {
+                // Snapshot is not loaded yet, skip.
+                return;
+            }
+
+            float progress = EXAGGERATED_EASE.getInterpolation(mFullscreenProgress);
+            setFullscreenScale(Utilities.mapRange(progress, 1f, mScaleAtFullscreen));
+
+            float widthDiff = params.width * (1 - mFullscreenScale);
+            setFullscreenTranslationX(getFullscreenTrans(
+                    getLayoutDirection() == LAYOUT_DIRECTION_RTL ? -widthDiff : widthDiff));
+        } else {
+            setFullscreenScale(1);
+            setFullscreenTranslationX(0);
+        }
+    }
+
+    private float getFullscreenTrans(float endTranslation) {
+        float progress = ACCEL_DEACCEL.getInterpolation(mFullscreenProgress);
+        return Utilities.mapRange(progress, 0, endTranslation);
+    }
+
     public boolean isRunningTask() {
         if (getRecentsView() == null) {
             return false;
diff --git a/src/com/android/launcher3/PagedView.java b/src/com/android/launcher3/PagedView.java
index 4303dee..ada297f 100644
--- a/src/com/android/launcher3/PagedView.java
+++ b/src/com/android/launcher3/PagedView.java
@@ -745,6 +745,11 @@
         return mOrientationHandler.getChildStart(pageAtIndex);
     }
 
+    protected int getChildVisibleSize(int index) {
+        View layout = getPageAt(index);
+        return mOrientationHandler.getMeasuredSize(layout);
+    }
+
     @Override
     public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
         int page = indexToPage(indexOfChild(child));
@@ -1457,8 +1462,7 @@
     }
 
     private int getDisplacementFromScreenCenter(int childIndex, int screenCenter) {
-        View layout = getPageAt(childIndex);
-        int childSize = mOrientationHandler.getMeasuredSize(layout);
+        int childSize = getChildVisibleSize(childIndex);
         int halfChildSize = (childSize / 2);
         int childCenter = getChildOffset(childIndex) + halfChildSize;
         return childCenter - screenCenter;
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index ac0083c..2b373a8 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -214,6 +214,10 @@
     public static final BooleanFlag ENABLE_TASKBAR = new DeviceFlag(
             "ENABLE_TASKBAR", false, "Allows a system Taskbar to be shown on larger devices.");
 
+    public static final BooleanFlag ENABLE_OVERVIEW_GRID = new DeviceFlag(
+            "ENABLE_OVERVIEW_GRID", false, "Uses grid overview layout. "
+            + "Only applicable on large screen devices.");
+
     public static void initialize(Context context) {
         synchronized (sDebugFlags) {
             for (DebugFlag flag : sDebugFlags) {