More grid changes, closer to final specs.

* Added isScalable, minCellWidth, minCellHeight, and borderSpacing attrs

When isScalable is true, we use set workspace cell values to
minCellWidth/minCellHeight and then scale the cell values to
fit the available space. This allows us to have consistent
aspect ratios when two devices match the display/grid options.

This is different from the dynamic grid, which calculates
cell values based on iconSize/textSize/etc and then allows
the cell width to be as wide as space allows.

I adjusted some variables so that they will auto adjust
based on another value, indepenent from whether the
grid isScalable or not. An example of this is the folder
label text, where it's always set to be 1.14x of whatever
the workspace icon text size is. This is so we don't need
to add a bunch of more variables to DisplayOption/GridOption.

Bug: 175329686
Test: - switching between device profiles,
      - testing folder open/close
      - testing in multiwindow
      - testing in landscape
Change-Id: Ia469ae0d65b518469ef264b726db46f4a3210056
diff --git a/res/layout/folder_application.xml b/res/layout/folder_application.xml
index 32a5419..1cdee08 100644
--- a/res/layout/folder_application.xml
+++ b/res/layout/folder_application.xml
@@ -21,4 +21,5 @@
     android:textColor="?attr/folderTextColor"
     android:includeFontPadding="false"
     android:hapticFeedbackEnabled="false"
-    launcher:iconDisplay="folder" />
+    launcher:iconDisplay="folder"
+    launcher:centerVertically="true" />
diff --git a/res/layout/user_folder_icon_normalized.xml b/res/layout/user_folder_icon_normalized.xml
index c230dad..d38a77a 100644
--- a/res/layout/user_folder_icon_normalized.xml
+++ b/res/layout/user_folder_icon_normalized.xml
@@ -27,15 +27,12 @@
         android:clipToPadding="false"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        android:paddingLeft="16dp"
-        android:paddingRight="16dp"
-        android:paddingTop="16dp"
         launcher:pageIndicator="@+id/folder_page_indicator" />
 
     <LinearLayout
         android:id="@+id/folder_footer"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="48dp"
         android:clipChildren="false"
         android:orientation="horizontal"
         android:paddingLeft="12dp"
@@ -53,13 +50,10 @@
             android:gravity="center_horizontal"
             android:hint="@string/folder_hint_text"
             android:imeOptions="flagNoExtractUi"
-            android:paddingBottom="@dimen/folder_label_padding_bottom"
-            android:paddingTop="@dimen/folder_label_padding_top"
             android:singleLine="true"
             android:textColor="?attr/folderTextColor"
             android:textColorHighlight="?android:attr/colorControlHighlight"
-            android:textColorHint="?attr/folderHintColor"
-            android:textSize="@dimen/folder_label_text_size" />
+            android:textColorHint="?attr/folderHintColor"/>
 
         <com.android.launcher3.pageindicators.PageIndicatorDots
             android:id="@+id/folder_page_indicator"
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 587df6d..4078ef4 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -133,6 +133,8 @@
         <attr name="numAllAppsColumns" format="integer" />
         <attr name="defaultLayoutId" format="reference" />
         <attr name="demoModeLayoutId" format="reference" />
+        <attr name="isScalable" format="boolean" />
+
     </declare-styleable>
 
     <declare-styleable name="DevicePadding">
@@ -150,6 +152,12 @@
         <attr name="minWidthDps" format="float" />
         <attr name="minHeightDps" format="float" />
 
+        <!-- These min cell values are only used if GridDisplayOption#isScalable is true-->
+        <attr name="minCellHeightDps" format="float" />
+        <attr name="minCellWidthDps" format="float" />
+
+        <attr name="borderSpacingDps" format="float" />
+
         <attr name="iconImageSize" format="float" />
         <!-- landscapeIconSize defaults to iconSize, if not specified -->
         <attr name="landscapeIconSize" format="float" />
diff --git a/res/values/config.xml b/res/values/config.xml
index 41d1a12..89415b8 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -24,7 +24,7 @@
 
     <!-- AllApps & Launcher transitions -->
     <!-- Out of 100, the percent to shrink the workspace during spring loaded mode. -->
-    <integer name="config_workspaceSpringLoadShrinkPercentage">90</integer>
+    <integer name="config_workspaceSpringLoadShrinkPercentage">85</integer>
 
     <!-- The duration of the animation from search hint to text entry -->
     <integer name="config_searchHintAnimationDuration">50</integer>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index acc6466..e532305 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -20,6 +20,7 @@
 
     <!-- Dynamic Grid -->
     <dimen name="dynamic_grid_edge_margin">8dp</dimen>
+    <dimen name="dynamic_grid_left_right_margin">8dp</dimen>
     <dimen name="dynamic_grid_icon_drawable_padding">7dp</dimen>
     <!-- Minimum space between workspace and hotseat in spring loaded mode -->
     <dimen name="dynamic_grid_min_spring_loaded_space">8dp</dimen>
@@ -27,7 +28,6 @@
     <dimen name="dynamic_grid_cell_border_spacing">16dp</dimen>
     <dimen name="dynamic_grid_cell_layout_padding">5.5dp</dimen>
     <dimen name="dynamic_grid_cell_padding_x">8dp</dimen>
-    <dimen name="dynamic_grid_cell_padding_y">7dp</dimen>
 
     <!-- Hotseat -->
     <dimen name="dynamic_grid_hotseat_top_padding">8dp</dimen>
@@ -37,6 +37,9 @@
     <dimen name="dynamic_grid_hotseat_extra_vertical_size">34dp</dimen>
     <dimen name="dynamic_grid_hotseat_side_padding">0dp</dimen>
 
+    <!-- Scalable Grid -->
+    <dimen name="scalable_grid_left_right_margin">22dp</dimen>
+
     <!-- Workspace page indicator -->
     <dimen name="workspace_page_indicator_height">24dp</dimen>
     <dimen name="workspace_page_indicator_line_height">1dp</dimen>
@@ -90,7 +93,6 @@
     <!-- The size of corner radius of the arrow in the arrow toast. -->
     <dimen name="arrow_toast_corner_radius">2dp</dimen>
 
-
 <!-- Search bar in All Apps -->
     <dimen name="all_apps_header_max_elevation">3dp</dimen>
     <dimen name="all_apps_header_scroll_to_elevation">16dp</dimen>
@@ -148,10 +150,12 @@
 
     <dimen name="folder_cell_x_padding">9dp</dimen>
     <dimen name="folder_cell_y_padding">6dp</dimen>
-    <dimen name="folder_child_text_size">13sp</dimen>
-    <dimen name="folder_label_padding_top">12dp</dimen>
-    <dimen name="folder_label_padding_bottom">12dp</dimen>
-    <dimen name="folder_label_text_size">16sp</dimen>
+    <!-- label text size = workspace text size multiplied by this scale -->
+    <dimen name="folder_label_text_scale">1.14</dimen>
+    <dimen name="folder_label_height">48dp</dimen>
+
+    <dimen name="folder_content_padding_left_right">8dp</dimen>
+    <dimen name="folder_content_padding_top">16dp</dimen>
 
 <!-- Sizes for managed profile badges -->
     <dimen name="profile_badge_size">24dp</dimen>
diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java
index be92c5d..4f008e2 100644
--- a/src/com/android/launcher3/AppWidgetResizeFrame.java
+++ b/src/com/android/launcher3/AppWidgetResizeFrame.java
@@ -3,7 +3,6 @@
 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_HEIGHT;
 import static com.android.launcher3.LauncherAnimUtils.LAYOUT_WIDTH;
 import static com.android.launcher3.Utilities.ATLEAST_S;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_X;
 import static com.android.launcher3.views.BaseDragLayer.LAYOUT_Y;
 
@@ -50,6 +49,14 @@
                         inv.portraitProfile.getCellSize()};
             });
 
+    // Represents the border spacing size on the grid in the two orientations.
+    public static final MainThreadInitializedObject<int[]> BORDER_SPACING_SIZE =
+            new MainThreadInitializedObject<>(c -> {
+                InvariantDeviceProfile inv = LauncherAppState.getIDP(c);
+                return new int[] {inv.landscapeProfile.cellLayoutBorderSpacingPx,
+                        inv.portraitProfile.cellLayoutBorderSpacingPx};
+            });
+
     private static final int HANDLE_COUNT = 4;
     private static final int INDEX_LEFT = 0;
     private static final int INDEX_TOP = 1;
@@ -370,16 +377,12 @@
         }
     }
 
-    private static PointF getWidgetSize(Context context, Point cellSize, int spanX, int spanY) {
+    private static PointF getWidgetSize(Context context, Point cellSize, int spanX, int spanY,
+            int borderSpacing) {
         final float density = context.getResources().getDisplayMetrics().density;
-        float hBorderSpacing = 0;
-        float vBorderSpacing = 0;
-        if (ENABLE_FOUR_COLUMNS.get()) {
-            final int borderSpacing = context.getResources()
-                    .getDimensionPixelSize(R.dimen.dynamic_grid_cell_border_spacing);
-            hBorderSpacing = (spanX - 1) * borderSpacing;
-            vBorderSpacing = (spanY - 1) * borderSpacing;
-        }
+        final float hBorderSpacing = (spanX - 1) * borderSpacing;
+        final float vBorderSpacing = (spanY - 1) * borderSpacing;
+
         PointF widgetSize = new PointF();
         widgetSize.x = ((spanX * cellSize.x) + hBorderSpacing) / density;
         widgetSize.y = ((spanY * cellSize.y) + vBorderSpacing) / density;
@@ -389,10 +392,11 @@
     /** Returns the actual widget size given its span. */
     public static PointF getWidgetSize(Context context, int spanX, int spanY) {
         final Point[] cellSize = CELL_SIZE.get(context);
+        final int[] borderSpacing = BORDER_SPACING_SIZE.get(context);
         if (isLandscape(context)) {
-            return getWidgetSize(context, cellSize[0], spanX, spanY);
+            return getWidgetSize(context, cellSize[0], spanX, spanY, borderSpacing[0]);
         }
-        return getWidgetSize(context, cellSize[1], spanX, spanY);
+        return getWidgetSize(context, cellSize[1], spanX, spanY, borderSpacing[1]);
     }
 
     /** Returns true if the screen is in landscape mode. */
@@ -404,9 +408,10 @@
     /** Returns the list of sizes for a widget of given span, in dp. */
     public static ArrayList<PointF> getWidgetSizes(Context context, int spanX, int spanY) {
         final Point[] cellSize = CELL_SIZE.get(context);
+        final int[] borderSpacing = BORDER_SPACING_SIZE.get(context);
 
-        PointF landSize = getWidgetSize(context, cellSize[0], spanX, spanY);
-        PointF portSize = getWidgetSize(context, cellSize[1], spanX, spanY);
+        PointF landSize = getWidgetSize(context, cellSize[0], spanX, spanY, borderSpacing[0]);
+        PointF portSize = getWidgetSize(context, cellSize[1], spanX, spanY,  borderSpacing[1]);
 
         ArrayList<PointF> sizes = new ArrayList<>(2);
         sizes.add(landSize);
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index ee4d7ec..e0be6de 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -17,7 +17,6 @@
 package com.android.launcher3;
 
 import static com.android.launcher3.FastBitmapDrawable.newIcon;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
 import static com.android.launcher3.graphics.IconShape.getShape;
 import static com.android.launcher3.graphics.PreloadIconDrawable.newPendingIcon;
 import static com.android.launcher3.icons.GraphicsUtils.setColorAlphaBound;
@@ -194,7 +193,7 @@
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.iconTextSizePx);
             setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
             defaultIconSize = grid.iconSizePx;
-            setCenterVertically(ENABLE_FOUR_COLUMNS.get());
+            setCenterVertically(grid.isScalableGrid);
         } else if (mDisplay == DISPLAY_ALL_APPS) {
             setTextSize(TypedValue.COMPLEX_UNIT_PX, grid.allAppsIconTextSizePx);
             setCompoundDrawablePadding(grid.allAppsIconDrawablePaddingPx);
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 634093c..7a9f852 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -16,7 +16,7 @@
 
 package com.android.launcher3;
 
-import static com.android.launcher3.config.FeatureFlags.ENABLE_FOUR_COLUMNS;
+import static com.android.launcher3.ResourceUtils.pxFromDp;
 
 import android.content.Context;
 import android.content.res.Configuration;
@@ -66,6 +66,8 @@
 
     public final float aspectRatio;
 
+    public final boolean isScalableGrid;
+
     /**
      * The maximum amount of left/right workspace padding as a percentage of the screen width.
      * To be clear, this means that up to 7% of the screen width can be used as left padding, and
@@ -79,8 +81,10 @@
     private static final int PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER = 4;
 
     // Workspace
-    public final int desiredWorkspaceLeftRightMarginPx;
-    public final int cellLayoutBorderSpacingPx;
+    public final int desiredWorkspaceLeftRightOriginalPx;
+    public int desiredWorkspaceLeftRightMarginPx;
+    public final int cellLayoutBorderSpacingOriginalPx;
+    public int cellLayoutBorderSpacingPx;
     public final int cellLayoutPaddingLeftRightPx;
     public final int cellLayoutBottomPaddingPx;
     public final int edgeMarginPx;
@@ -102,13 +106,21 @@
 
     public int cellWidthPx;
     public int cellHeightPx;
-    public int cellYPaddingPx;
     public int workspaceCellPaddingXPx;
 
+    public int cellYPaddingPx;
+    public int cellYPaddingOriginalPx;
+
     // Folder
+    public float folderLabelTextScale;
+    public int folderLabelTextSizePx;
     public int folderIconSizePx;
     public int folderIconOffsetYPx;
 
+    // Folder content
+    public int folderContentPaddingLeftRight;
+    public int folderContentPaddingTop;
+
     // Folder cell
     public int folderCellWidthPx;
     public int folderCellHeightPx;
@@ -164,9 +176,12 @@
         this.inv = inv;
         this.isLandscape = isLandscape;
         this.isMultiWindowMode = isMultiWindowMode;
+        this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
         windowX = windowPosition.x;
         windowY = windowPosition.y;
 
+        isScalableGrid = inv.isScalable && !isVerticalBarLayout() && !isMultiWindowMode;
+
         // Determine sizes.
         widthPx = width;
         heightPx = height;
@@ -193,8 +208,6 @@
         boolean isTallDevice = Float.compare(aspectRatio, TALL_DEVICE_ASPECT_RATIO_THRESHOLD) >= 0;
 
         // Some more constants
-        this.transposeLayoutWithOrientation = transposeLayoutWithOrientation;
-
         context = getContext(context, info, isVerticalBarLayout()
                 ? Configuration.ORIENTATION_LANDSCAPE
                 : Configuration.ORIENTATION_PORTRAIT);
@@ -217,16 +230,25 @@
         availableHeightPx = nonFinalAvailableHeightPx;
 
         edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
-        desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : edgeMarginPx;
 
-        cellYPaddingPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_padding_y);
-        cellLayoutBorderSpacingPx = isVerticalBarLayout()
-                || isMultiWindowMode
-                || !ENABLE_FOUR_COLUMNS.get()
-                ? 0 : res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_border_spacing);
+        desiredWorkspaceLeftRightMarginPx = isVerticalBarLayout() ? 0 : isScalableGrid
+                ? res.getDimensionPixelSize(R.dimen.scalable_grid_left_right_margin)
+                : res.getDimensionPixelSize(R.dimen.dynamic_grid_left_right_margin);
+        desiredWorkspaceLeftRightOriginalPx = desiredWorkspaceLeftRightMarginPx;
+
+        folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);
+        folderContentPaddingLeftRight =
+                res.getDimensionPixelSize(R.dimen.folder_content_padding_left_right);
+        folderContentPaddingTop = res.getDimensionPixelSize(R.dimen.folder_content_padding_top);
+
+        setCellLayoutBorderSpacing(pxFromDp(inv.borderSpacing, mInfo.metrics, 1f));
+        cellLayoutBorderSpacingOriginalPx = cellLayoutBorderSpacingPx;
+
         int cellLayoutPaddingLeftRightMultiplier = !isVerticalBarLayout() && isTablet
                 ? PORTRAIT_TABLET_LEFT_RIGHT_PADDING_MULTIPLIER : 1;
-        int cellLayoutPadding = res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
+        int cellLayoutPadding = isScalableGrid
+                ? 0
+                : res.getDimensionPixelSize(R.dimen.dynamic_grid_cell_layout_padding);
         if (isLandscape) {
             cellLayoutPaddingLeftRightPx = 0;
             cellLayoutBottomPaddingPx = cellLayoutPadding;
@@ -259,16 +281,16 @@
         hotseatBarSidePaddingStartPx = isVerticalBarLayout() ? workspacePageIndicatorHeight : 0;
         int hotseatExtraVerticalSize =
                 res.getDimensionPixelSize(R.dimen.dynamic_grid_hotseat_extra_vertical_size);
-        hotseatBarSizePx = ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics)
+        hotseatBarSizePx = pxFromDp(inv.iconSize, mInfo.metrics, 1f)
                 + (isVerticalBarLayout()
                 ? (hotseatBarSidePaddingStartPx + hotseatBarSidePaddingEndPx)
                 : (hotseatBarTopPaddingPx + hotseatBarBottomPaddingPx
-                        + (ENABLE_FOUR_COLUMNS.get() ? 0 : hotseatExtraVerticalSize)));
+                        + (isScalableGrid ? 0 : hotseatExtraVerticalSize)));
 
         // Calculate all of the remaining variables.
         int extraSpace = updateAvailableDimensions(res);
         // Now that we have all of the variables calculated, we can tune certain sizes.
-        if (ENABLE_FOUR_COLUMNS.get()) {
+        if (isScalableGrid) {
             DevicePadding padding = inv.devicePaddings.getDevicePadding(extraSpace);
             workspaceTopPadding = padding.getWorkspaceTopPadding(extraSpace);
             workspaceBottomPadding = padding.getWorkspaceBottomPadding(extraSpace);
@@ -299,6 +321,17 @@
                         IconShape.DEFAULT_PATH_SIZE);
     }
 
+    private void setCellLayoutBorderSpacing(int borderSpacing) {
+        if (isScalableGrid) {
+            cellLayoutBorderSpacingPx = borderSpacing;
+            folderContentPaddingLeftRight = borderSpacing;
+            folderContentPaddingTop = borderSpacing;
+        } else {
+            cellLayoutBorderSpacingPx = 0;
+        }
+    }
+
+
     public Builder toBuilder(Context context) {
         Point size = new Point(availableWidthPx, availableHeightPx);
         return new Builder(context, inv, mInfo)
@@ -380,20 +413,42 @@
     private int updateAvailableDimensions(Resources res) {
         updateIconSize(1f, res);
 
-        // Check to see if the icons fit within the available height.  If not, then scale down.
-        float usedHeight = (cellHeightPx * inv.numRows)
-                + (cellLayoutBorderSpacingPx * (inv.numRows - 1));
-        int maxHeight = (availableHeightPx - getTotalWorkspacePadding().y);
+        Point workspacePadding = getTotalWorkspacePadding();
+
+        // Check to see if the icons fit within the available height.
+        float usedHeight = getCellLayoutHeight();
+        final int maxHeight = availableHeightPx - workspacePadding.y;
         float extraHeight = Math.max(0, maxHeight - usedHeight);
-        if (usedHeight > maxHeight) {
-            float scale = maxHeight / usedHeight;
-            updateIconSize(scale, res);
-            extraHeight = 0;
+        float scaleY = maxHeight / usedHeight;
+        boolean shouldScale = scaleY < 1f;
+
+        float scaleX = 1f;
+        if (isScalableGrid) {
+            // We scale to fit the cellWidth and cellHeight in the available space.
+            // The benefit of scalable grids is that we can get consistent aspect ratios between
+            // devices.
+            float usedWidth = (cellWidthPx * inv.numColumns)
+                    + (cellLayoutBorderSpacingPx * (inv.numColumns - 1))
+                    + (desiredWorkspaceLeftRightMarginPx * 2);
+            // We do not subtract padding here, as we also scale the workspace padding if needed.
+            scaleX = availableWidthPx / usedWidth;
+            shouldScale = true;
         }
+
+        if (shouldScale) {
+            float scale = Math.min(scaleX, scaleY);
+            updateIconSize(scale, res);
+            extraHeight = Math.max(0, maxHeight - getCellLayoutHeight());
+        }
+
         updateAvailableFolderCellDimensions(res);
         return Math.round(extraHeight);
     }
 
+    private int getCellLayoutHeight() {
+        return (cellHeightPx * inv.numRows) + (cellLayoutBorderSpacingPx * (inv.numRows - 1));
+    }
+
     /**
      * Updating the iconSize affects many aspects of the launcher layout, such as: iconSizePx,
      * iconTextSizePx, iconDrawablePaddingPx, cellWidth/Height, allApps* variants,
@@ -403,17 +458,21 @@
         // Workspace
         final boolean isVerticalLayout = isVerticalBarLayout();
         float invIconSizeDp = isVerticalLayout ? inv.landscapeIconSize : inv.iconSize;
-        iconSizePx = Math.max(1, (int) (ResourceUtils.pxFromDp(invIconSizeDp, mInfo.metrics)
-                * scale));
-        iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, mInfo.metrics) * scale);
+        iconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mInfo.metrics, scale));
+        iconTextSizePx = pxFromDp(inv.iconTextSize, mInfo.metrics, scale);
         iconDrawablePaddingPx = (int) (iconDrawablePaddingOriginalPx * scale);
 
-        if (ENABLE_FOUR_COLUMNS.get()) {
-            cellHeightPx = iconSizePx + iconDrawablePaddingPx
-                    + Utilities.calculateTextHeight(iconTextSizePx)
-                    + (cellYPaddingPx * 2);
+        setCellLayoutBorderSpacing((int) (cellLayoutBorderSpacingOriginalPx * scale));
+
+        if (isScalableGrid) {
+            cellWidthPx = pxFromDp(inv.minCellWidth, mInfo.metrics, scale);
+            cellHeightPx = pxFromDp(inv.minCellHeight, mInfo.metrics, scale);
+            int cellContentHeight = iconSizePx + iconDrawablePaddingPx
+                    + Utilities.calculateTextHeight(iconTextSizePx);
+            cellYPaddingPx = Math.max(0, cellHeightPx - cellContentHeight) / 2;
+            desiredWorkspaceLeftRightMarginPx = (int) (desiredWorkspaceLeftRightOriginalPx * scale);
         } else {
-            cellYPaddingPx = 0;
+            cellWidthPx = iconSizePx + iconDrawablePaddingPx;
             cellHeightPx = iconSizePx + iconDrawablePaddingPx
                     + Utilities.calculateTextHeight(iconTextSizePx);
             int cellPaddingY = (getCellSize().y - cellHeightPx) / 2;
@@ -426,11 +485,10 @@
                 iconDrawablePaddingPx = cellPaddingY;
             }
         }
-        cellWidthPx = iconSizePx + iconDrawablePaddingPx;
 
         // All apps
         if (allAppsHasDifferentNumColumns()) {
-            allAppsIconSizePx = ResourceUtils.pxFromDp(inv.allAppsIconSize, mInfo.metrics);
+            allAppsIconSizePx = pxFromDp(inv.allAppsIconSize, mInfo.metrics);
             allAppsIconTextSizePx = Utilities.pxFromSp(inv.allAppsIconTextSize, mInfo.metrics);
             allAppsIconDrawablePaddingPx = iconDrawablePaddingOriginalPx;
             // We use 4 below to ensure labels are closer to their corresponding icon.
@@ -474,12 +532,10 @@
     }
 
     private void updateAvailableFolderCellDimensions(Resources res) {
-        int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_padding_top)
-                + res.getDimensionPixelSize(R.dimen.folder_label_padding_bottom)
-                + Utilities.calculateTextHeight(res.getDimension(R.dimen.folder_label_text_size));
-
         updateFolderCellSize(1f, res);
 
+        final int folderBottomPanelSize = res.getDimensionPixelSize(R.dimen.folder_label_height);
+
         // Don't let the folder get too close to the edges of the screen.
         int folderMargin = edgeMarginPx * 2;
         Point totalWorkspacePadding = getTotalWorkspacePadding();
@@ -488,13 +544,14 @@
         float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
                 + ((inv.numFolderRows - 1) * cellLayoutBorderSpacingPx);
         int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y - folderBottomPanelSize
-                - folderMargin;
+                - folderMargin - folderContentPaddingTop;
         float scaleY = contentMaxHeight / contentUsedHeight;
 
         // Check if the icons fit within the available width.
         float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
                 + ((inv.numFolderColumns - 1) * cellLayoutBorderSpacingPx);
-        int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin;
+        int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x - folderMargin
+                - folderContentPaddingLeftRight * 2;
         float scaleX = contentMaxWidth / contentUsedWidth;
 
         float scale = Math.min(scaleX, scaleY);
@@ -504,9 +561,10 @@
     }
 
     private void updateFolderCellSize(float scale, Resources res) {
-        folderChildIconSizePx = (int) (ResourceUtils.pxFromDp(inv.iconSize, mInfo.metrics) * scale);
-        folderChildTextSizePx =
-                (int) (res.getDimensionPixelSize(R.dimen.folder_child_text_size) * scale);
+        float invIconSizeDp = isVerticalBarLayout() ? inv.landscapeIconSize : inv.iconSize;
+        folderChildIconSizePx = Math.max(1, pxFromDp(invIconSizeDp, mInfo.metrics, scale));
+        folderChildTextSizePx = pxFromDp(inv.iconTextSize, mInfo.metrics, scale);
+        folderLabelTextSizePx = (int) (folderChildTextSizePx * folderLabelTextScale);
 
         int textHeight = Utilities.calculateTextHeight(folderChildTextSizePx);
         int cellPaddingX = (int) (res.getDimensionPixelSize(R.dimen.folder_cell_x_padding) * scale);
@@ -588,7 +646,7 @@
             } else {
                 // Pad the top and bottom of the workspace with search/hotseat bar sizes
                 padding.set(desiredWorkspaceLeftRightMarginPx,
-                        workspaceTopPadding + edgeMarginPx,
+                        workspaceTopPadding + (isScalableGrid ? 0 : edgeMarginPx),
                         desiredWorkspaceLeftRightMarginPx,
                         paddingBottom);
             }
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 2a08c50..bb60557 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -111,6 +111,10 @@
     public float allAppsIconSize;
     public float allAppsIconTextSize;
 
+    public float minCellHeight;
+    public float minCellWidth;
+    public float borderSpacing;
+
     private SparseArray<TypedValue> mExtraAttrs;
 
     /**
@@ -123,6 +127,11 @@
      */
     public int numAllAppsColumns;
 
+    /**
+     * Do not query directly. see {@link DeviceProfile#isScalableGrid}.
+     */
+    protected boolean isScalable;
+
     public String dbFile;
     public int defaultLayoutId;
     int demoModeLayoutId;
@@ -154,6 +163,10 @@
         iconTextSize = p.iconTextSize;
         numHotseatIcons = p.numHotseatIcons;
         numAllAppsColumns = p.numAllAppsColumns;
+        isScalable = p.isScalable;
+        minCellHeight = p.minCellHeight;
+        minCellWidth = p.minCellWidth;
+        borderSpacing = p.borderSpacing;
         dbFile = p.dbFile;
         allAppsIconSize = p.allAppsIconSize;
         allAppsIconTextSize = p.allAppsIconTextSize;
@@ -213,6 +226,9 @@
         result.landscapeIconSize = defaultDisplayOption.landscapeIconSize;
         result.allAppsIconSize = Math.min(
                 defaultDisplayOption.allAppsIconSize, myDisplayOption.allAppsIconSize);
+        result.minCellHeight = defaultDisplayOption.minCellHeight;
+        result.minCellWidth = defaultDisplayOption.minCellWidth;
+        result.borderSpacing = defaultDisplayOption.borderSpacing;
 
         devicePaddings = new DevicePaddings(context);
         initGrid(context, myInfo, result);
@@ -259,6 +275,7 @@
         numFolderRows = closestProfile.numFolderRows;
         numFolderColumns = closestProfile.numFolderColumns;
         numAllAppsColumns = closestProfile.numAllAppsColumns;
+        isScalable = closestProfile.isScalable;
 
         mExtraAttrs = closestProfile.extraAttrs;
 
@@ -269,6 +286,10 @@
         iconTextSize = displayOption.iconTextSize;
         fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
 
+        minCellHeight = displayOption.minCellHeight;
+        minCellWidth = displayOption.minCellWidth;
+        borderSpacing = displayOption.borderSpacing;
+
         if (Utilities.isGridOptionsEnabled(context)) {
             allAppsIconSize = displayOption.allAppsIconSize;
             allAppsIconTextSize = displayOption.allAppsIconTextSize;
@@ -589,6 +610,8 @@
         private final int defaultLayoutId;
         private final int demoModeLayoutId;
 
+        private final boolean isScalable;
+
         private final SparseArray<TypedValue> extraAttrs;
 
         public GridOption(Context context, AttributeSet attrs) {
@@ -612,6 +635,9 @@
             numAllAppsColumns = a.getInt(
                     R.styleable.GridDisplayOption_numAllAppsColumns, numColumns);
 
+            isScalable = a.getBoolean(
+                    R.styleable.GridDisplayOption_isScalable, false);
+
             a.recycle();
 
             extraAttrs = Themes.createValueMap(context, attrs,
@@ -626,6 +652,10 @@
         private final float minHeightDps;
         private final boolean canBeDefault;
 
+        private float minCellHeight;
+        private float minCellWidth;
+        private float borderSpacing;
+
         private float iconSize;
         private float iconTextSize;
         private float landscapeIconSize;
@@ -643,6 +673,10 @@
             canBeDefault = a.getBoolean(
                     R.styleable.ProfileDisplayOption_canBeDefault, false);
 
+            minCellHeight = a.getFloat(R.styleable.ProfileDisplayOption_minCellHeightDps, 0);
+            minCellWidth = a.getFloat(R.styleable.ProfileDisplayOption_minCellWidthDps, 0);
+            borderSpacing = a.getFloat(R.styleable.ProfileDisplayOption_borderSpacingDps, 0);
+
             iconSize = a.getFloat(R.styleable.ProfileDisplayOption_iconImageSize, 0);
             landscapeIconSize = a.getFloat(R.styleable.ProfileDisplayOption_landscapeIconSize,
                     iconSize);
@@ -664,6 +698,9 @@
             minWidthDps = 0;
             minHeightDps = 0;
             canBeDefault = false;
+            minCellHeight = 0;
+            minCellWidth = 0;
+            borderSpacing = 0;
         }
 
         private DisplayOption multiply(float w) {
@@ -672,6 +709,9 @@
             allAppsIconSize *= w;
             iconTextSize *= w;
             allAppsIconTextSize *= w;
+            minCellHeight *= w;
+            minCellWidth *= w;
+            borderSpacing *= w;
             return this;
         }
 
@@ -681,6 +721,9 @@
             allAppsIconSize += p.allAppsIconSize;
             iconTextSize += p.iconTextSize;
             allAppsIconTextSize += p.allAppsIconTextSize;
+            minCellHeight += p.minCellHeight;
+            minCellWidth += p.minCellWidth;
+            borderSpacing += p.borderSpacing;
             return this;
         }
     }
diff --git a/src/com/android/launcher3/ResourceUtils.java b/src/com/android/launcher3/ResourceUtils.java
index f60e1f8..ece123d 100644
--- a/src/com/android/launcher3/ResourceUtils.java
+++ b/src/com/android/launcher3/ResourceUtils.java
@@ -60,7 +60,11 @@
     }
 
     public static int pxFromDp(float size, DisplayMetrics metrics) {
-        return size < 0 ? INVALID_RESOURCE_HANDLE : Math.round(
-                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
+        return pxFromDp(size, metrics, 1f);
+    }
+
+    public static int pxFromDp(float size, DisplayMetrics metrics, float scale) {
+        return size < 0 ? INVALID_RESOURCE_HANDLE : Math.round(scale
+                * TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, size, metrics));
     }
 }
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 504b29e..bcb3a54 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -44,6 +44,7 @@
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Pair;
+import android.util.TypedValue;
 import android.view.FocusFinder;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -246,11 +247,16 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        final DeviceProfile dp = mLauncher.getDeviceProfile();
+        final int paddingLeftRight = dp.folderContentPaddingLeftRight;
+
         mContent = findViewById(R.id.folder_content);
+        mContent.setPadding(paddingLeftRight, dp.folderContentPaddingTop, paddingLeftRight, 0);
         mContent.setFolder(this);
 
         mPageIndicator = findViewById(R.id.folder_page_indicator);
         mFolderName = findViewById(R.id.folder_name);
+        mFolderName.setTextSize(TypedValue.COMPLEX_UNIT_PX, dp.folderLabelTextSizePx);
         mFolderName.setOnBackKeyListener(this);
         mFolderName.setOnFocusChangeListener(this);
         mFolderName.setOnEditorActionListener(this);
@@ -262,12 +268,7 @@
         mFolderName.forceDisableSuggestions(true);
 
         mFooter = findViewById(R.id.folder_footer);
-
-        // We find out how tall footer wants to be (it is set to wrap_content), so that
-        // we can allocate the appropriate amount of space for it.
-        int measureSpec = MeasureSpec.UNSPECIFIED;
-        mFooter.measure(measureSpec, measureSpec);
-        mFooterHeight = mFooter.getMeasuredHeight();
+        mFooterHeight = getResources().getDimensionPixelSize(R.dimen.folder_label_height);
 
         if (Utilities.ATLEAST_R) {
             mFolderWindowInsetsAnimationCallback =
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 1cac31e..feb528c 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -324,7 +324,9 @@
 
             final int previewPosX =
                     (int) ((mTmpParams.transX - iconOffsetX + previewItemOffsetX) / folderScale);
-            final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY) / folderScale);
+            final float paddingTop = btv.getPaddingTop() * iconScale;
+            final int previewPosY = (int) ((mTmpParams.transY + previewItemOffsetY - paddingTop)
+                    / folderScale);
 
             final float xDistance = previewPosX - btvLp.x;
             final float yDistance = previewPosY - btvLp.y;