Hybrid hotseat predicted icon visuals

Bug:142753423
Test: Manual
Change-Id: I6f056aaec905c8ca357b7cf78a657cdaac84e2f1
diff --git a/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml b/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml
new file mode 100644
index 0000000..70a765a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/res/layout/predicted_app_icon.xml
@@ -0,0 +1,17 @@
+<?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.launcher3.uioverrides.PredictedAppIcon style="@style/BaseIcon.Workspace" />
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
index fe9fd60..17a3d91 100644
--- a/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/HotseatPredictionController.java
@@ -18,6 +18,7 @@
 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
 
 import android.animation.Animator;
+import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.app.prediction.AppPredictionContext;
 import android.app.prediction.AppPredictionManager;
@@ -40,10 +41,9 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragOptions;
 import com.android.launcher3.icons.IconCache;
-import com.android.launcher3.popup.PopupContainerWithArrow;
 import com.android.launcher3.popup.SystemShortcut;
 import com.android.launcher3.shortcuts.ShortcutKey;
-import com.android.launcher3.touch.ItemLongClickListener;
+import com.android.launcher3.uioverrides.PredictedAppIcon;
 import com.android.launcher3.uioverrides.QuickstepLauncher;
 import com.android.launcher3.util.ComponentKey;
 
@@ -81,12 +81,14 @@
     private AppPredictor mAppPredictor;
     private AllAppsStore mAllAppsStore;
 
+    private List<PredictedAppIcon.PredictedIconOutlineDrawing> mOutlineDrawings = new ArrayList<>();
+
     public HotseatPredictionController(Launcher launcher) {
         mLauncher = launcher;
         mHotseat = launcher.getHotseat();
         mAllAppsStore = mLauncher.getAppsView().getAppsStore();
         mAllAppsStore.addUpdateListener(this);
-        mDynamicItemCache = new DynamicItemCache(mLauncher, () -> fillGapsWithPrediction(false));
+        mDynamicItemCache = new DynamicItemCache(mLauncher, this::fillGapsWithPrediction);
         mHotSeatItemsCount = mLauncher.getDeviceProfile().inv.numHotseatIcons;
         launcher.getDeviceProfile().inv.addOnChangeListener(this);
         mHotseat.addOnAttachStateChangeListener(this);
@@ -102,16 +104,17 @@
         mLauncher.getDragController().removeDragListener(this);
     }
 
-    /**
-     * Fills gaps in the hotseat with predictions
-     */
-    public void fillGapsWithPrediction(boolean animate) {
+    private void fillGapsWithPrediction() {
+        fillGapsWithPrediction(false, null);
+    }
+
+    private void fillGapsWithPrediction(boolean animate, Runnable callback) {
         if (mDragObject != null) {
             return;
         }
         List<WorkspaceItemInfo> predictedApps = mapToWorkspaceItemInfo(mComponentKeyMappers);
         int predictionIndex = 0;
-        ArrayList<ItemInfo> newItemsToAdd = new ArrayList<>();
+        ArrayList<WorkspaceItemInfo> newItems = new ArrayList<>();
         for (int rank = 0; rank < mHotSeatItemsCount; rank++) {
             View child = mHotseat.getChildAt(
                     mHotseat.getCellXFromOrder(rank),
@@ -130,21 +133,37 @@
 
             WorkspaceItemInfo predictedItem = predictedApps.get(predictionIndex++);
             if (isPredictedIcon(child)) {
-                BubbleTextView icon = (BubbleTextView) child;
+                PredictedAppIcon icon = (PredictedAppIcon) child;
                 icon.applyFromWorkspaceItem(predictedItem);
+                icon.finishBinding();
             } else {
-                newItemsToAdd.add(predictedItem);
+                newItems.add(predictedItem);
             }
             preparePredictionInfo(predictedItem, rank);
         }
-        mLauncher.bindItems(newItemsToAdd, animate);
-        for (BubbleTextView icon : getPredictedIcons()) {
-            icon.verifyHighRes();
-            icon.setOnLongClickListener((v) -> {
-                PopupContainerWithArrow.showForIcon((BubbleTextView) v);
-                return true;
+        bindItems(newItems, animate, callback);
+    }
+
+    private void bindItems(List<WorkspaceItemInfo> itemsToAdd, boolean animate, Runnable callback) {
+        AnimatorSet animationSet = new AnimatorSet();
+        for (WorkspaceItemInfo item : itemsToAdd) {
+            PredictedAppIcon icon = PredictedAppIcon.createIcon(mHotseat, item);
+            mLauncher.getWorkspace().addInScreenFromBind(icon, item);
+            icon.finishBinding();
+            if (animate) {
+                animationSet.play(ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 0.2f, 1));
+            }
+        }
+        if (animate) {
+            animationSet.addListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    if (callback != null) callback.run();
+                }
             });
-            icon.setBackgroundResource(R.drawable.predicted_icon_background);
+            animationSet.start();
+        } else {
+            if (callback != null) callback.run();
         }
     }
 
@@ -179,6 +198,7 @@
                         .build());
         mAppPredictor.registerPredictionUpdates(mLauncher.getMainExecutor(),
                 this::setPredictedApps);
+
         mAppPredictor.requestPredictionUpdate();
     }
 
@@ -210,7 +230,7 @@
             mComponentKeyMappers.add(new ComponentKeyMapper(key, mDynamicItemCache));
         }
         updateDependencies();
-        fillGapsWithPrediction(false);
+        fillGapsWithPrediction();
     }
 
     private void updateDependencies() {
@@ -219,7 +239,7 @@
     }
 
     private void pinPrediction(ItemInfo info) {
-        BubbleTextView icon = (BubbleTextView) mHotseat.getChildAt(
+        PredictedAppIcon icon = (PredictedAppIcon) mHotseat.getChildAt(
                 mHotseat.getCellXFromOrder(info.rank),
                 mHotseat.getCellYFromOrder(info.rank));
         if (icon == null) {
@@ -230,9 +250,7 @@
                 LauncherSettings.Favorites.CONTAINER_HOTSEAT, workspaceItemInfo.screenId,
                 workspaceItemInfo.cellX, workspaceItemInfo.cellY);
         ObjectAnimator.ofFloat(icon, SCALE_PROPERTY, 1, 0.8f, 1).start();
-        icon.reset();
-        icon.applyFromWorkspaceItem(workspaceItemInfo);
-        icon.setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
+        icon.pin(workspaceItemInfo);
         AppTarget appTarget = getAppTargetFromItemInfo(workspaceItemInfo);
         notifyItemAction(appTarget, AppTargetEvent.ACTION_PIN);
     }
@@ -265,37 +283,35 @@
         return predictedApps;
     }
 
-    private List<BubbleTextView> getPredictedIcons() {
-        List<BubbleTextView> icons = new ArrayList<>();
+    private List<PredictedAppIcon> getPredictedIcons() {
+        List<PredictedAppIcon> icons = new ArrayList<>();
         ViewGroup vg = mHotseat.getShortcutsAndWidgets();
         for (int i = 0; i < vg.getChildCount(); i++) {
             View child = vg.getChildAt(i);
             if (isPredictedIcon(child)) {
-                icons.add((BubbleTextView) child);
+                icons.add((PredictedAppIcon) child);
             }
         }
         return icons;
     }
 
-    private void removePredictedApps(boolean animate) {
-        for (BubbleTextView icon : getPredictedIcons()) {
-            if (animate) {
-                icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
-                    @Override
-                    public void onAnimationSuccess(Animator animator) {
-                        if (icon.getParent() != null) {
-                            mHotseat.removeView(icon);
-                        }
+    private void removePredictedApps(List<PredictedAppIcon.PredictedIconOutlineDrawing> outlines) {
+        for (PredictedAppIcon icon : getPredictedIcons()) {
+            int rank = ((WorkspaceItemInfo) icon.getTag()).rank;
+            outlines.add(new PredictedAppIcon.PredictedIconOutlineDrawing(
+                    mHotseat.getCellXFromOrder(rank), mHotseat.getCellYFromOrder(rank), icon));
+            icon.animate().scaleY(0).scaleX(0).setListener(new AnimationSuccessListener() {
+                @Override
+                public void onAnimationSuccess(Animator animator) {
+                    if (icon.getParent() != null) {
+                        mHotseat.removeView(icon);
                     }
-                });
-            } else {
-                if (icon.getParent() != null) {
-                    mHotseat.removeView(icon);
                 }
-            }
+            });
         }
     }
 
+
     private void notifyItemAction(AppTarget target, int action) {
         if (mAppPredictor != null) {
             mAppPredictor.notifyAppTargetEvent(new AppTargetEvent.Builder(target, action).build());
@@ -304,8 +320,13 @@
 
     @Override
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
-        removePredictedApps(true);
+        removePredictedApps(mOutlineDrawings);
         mDragObject = dragObject;
+        if (mOutlineDrawings.isEmpty()) return;
+        for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+            mHotseat.addDelegatedCellDrawing(outlineDrawing);
+        }
+        mHotseat.invalidate();
     }
 
     @Override
@@ -322,7 +343,14 @@
             }
         }
         mDragObject = null;
-        fillGapsWithPrediction(true);
+        fillGapsWithPrediction(true, () -> {
+            if (mOutlineDrawings.isEmpty()) return;
+            for (PredictedAppIcon.PredictedIconOutlineDrawing outlineDrawing : mOutlineDrawings) {
+                mHotseat.removeDelegatedCellDrawing(outlineDrawing);
+            }
+            mHotseat.invalidate();
+            mOutlineDrawings.clear();
+        });
     }
 
     @Nullable
@@ -351,8 +379,7 @@
 
     @Override
     public void onAppsUpdated() {
-        updateDependencies();
-        fillGapsWithPrediction(false);
+        fillGapsWithPrediction();
     }
 
     @Override
@@ -375,7 +402,7 @@
     }
 
     private static boolean isPredictedIcon(View view) {
-        return view instanceof BubbleTextView && view.getTag() instanceof WorkspaceItemInfo
+        return view instanceof PredictedAppIcon && view.getTag() instanceof WorkspaceItemInfo
                 && ((WorkspaceItemInfo) view.getTag()).container
                 == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
     }
@@ -396,9 +423,8 @@
 
     private static AppTarget getAppTargetFromItemInfo(ItemInfo info) {
         if (info.getTargetComponent() == null) return null;
-        return new AppTarget.Builder(
-                new AppTargetId("app:" + info.getTargetComponent().getPackageName()),
-                info.getTargetComponent().getPackageName(), info.user).setClassName(
-                info.getTargetComponent().getClassName()).build();
+        ComponentName cn = info.getTargetComponent();
+        return new AppTarget.Builder(new AppTargetId("app:" + cn.getPackageName()),
+                cn.getPackageName(), info.user).setClassName(cn.getClassName()).build();
     }
 }
diff --git a/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
new file mode 100644
index 0000000..e41c75a
--- /dev/null
+++ b/quickstep/recents_ui_overrides/src/com/android/launcher3/uioverrides/PredictedAppIcon.java
@@ -0,0 +1,188 @@
+/*
+ * 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.launcher3.uioverrides;
+
+import static com.android.launcher3.graphics.IconShape.getShape;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.core.graphics.ColorUtils;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.WorkspaceItemInfo;
+import com.android.launcher3.graphics.IconPalette;
+import com.android.launcher3.icons.IconNormalizer;
+import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.touch.ItemClickHandler;
+import com.android.launcher3.touch.ItemLongClickListener;
+
+/**
+ * A BubbleTextView with a ring around it's drawable
+ */
+public class PredictedAppIcon extends BubbleTextView {
+
+    private static final float RING_EFFECT_RATIO = 0.12f;
+
+    private DeviceProfile mDeviceProfile;
+    private final Paint mIconRingPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private boolean mIsPinned = false;
+    private int mNormalizedIconRadius;
+
+
+    public PredictedAppIcon(Context context) {
+        this(context, null, 0);
+    }
+
+    public PredictedAppIcon(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public PredictedAppIcon(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        mDeviceProfile = Launcher.getLauncher(context).getDeviceProfile();
+        mNormalizedIconRadius = IconNormalizer.getNormalizedCircleSize(getIconSize()) / 2;
+        setOnClickListener(ItemClickHandler.INSTANCE);
+        setOnFocusChangeListener(Launcher.getLauncher(context).mFocusHandler);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        int count = canvas.save();
+        if (!mIsPinned) {
+            drawEffect(canvas);
+            canvas.translate(getWidth() * RING_EFFECT_RATIO, getHeight() * RING_EFFECT_RATIO);
+            canvas.scale(1 - 2 * RING_EFFECT_RATIO, 1 - 2 * RING_EFFECT_RATIO);
+        }
+        super.onDraw(canvas);
+        canvas.restoreToCount(count);
+    }
+
+    @Override
+    public void applyFromWorkspaceItem(WorkspaceItemInfo info) {
+        super.applyFromWorkspaceItem(info);
+        int color = IconPalette.getMutedColor(info.bitmap.color, 0.54f);
+        mIconRingPaint.setColor(ColorUtils.setAlphaComponent(color, 200));
+    }
+
+    /**
+     * Removes prediction ring from app icon
+     */
+    public void pin(WorkspaceItemInfo info) {
+        if (mIsPinned) return;
+        applyFromWorkspaceItem(info);
+        setOnLongClickListener(ItemLongClickListener.INSTANCE_WORKSPACE);
+        mIsPinned = true;
+        invalidate();
+    }
+
+    /**
+     * prepares prediction icon for usage after bind
+     */
+    public void finishBinding() {
+        setOnLongClickListener((v) -> {
+            PopupContainerWithArrow.showForIcon((BubbleTextView) v);
+            if (getParent() != null) {
+                getParent().requestDisallowInterceptTouchEvent(true);
+            }
+            return true;
+        });
+        setTextVisibility(false);
+        verifyHighRes();
+    }
+
+    @Override
+    public void getIconBounds(Rect outBounds) {
+        super.getIconBounds(outBounds);
+        if (!mIsPinned) {
+            int predictionInset = (int) (getIconSize() * RING_EFFECT_RATIO);
+            outBounds.inset(predictionInset, predictionInset);
+        }
+    }
+
+    private int getOutlineOffsetX() {
+        return (getMeasuredWidth() / 2) - mNormalizedIconRadius;
+    }
+
+    private int getOutlineOffsetY() {
+        return getPaddingTop() + mDeviceProfile.folderIconOffsetYPx;
+    }
+
+    private void drawEffect(Canvas canvas) {
+        getShape().drawShape(canvas, getOutlineOffsetX(), getOutlineOffsetY(),
+                mNormalizedIconRadius, mIconRingPaint);
+    }
+
+    /**
+     * Creates and returns a new instance of PredictedAppIcon from WorkspaceItemInfo
+     */
+    public static PredictedAppIcon createIcon(ViewGroup parent, WorkspaceItemInfo info) {
+        PredictedAppIcon icon = (PredictedAppIcon) LayoutInflater.from(parent.getContext())
+                .inflate(R.layout.predicted_app_icon, parent, false);
+        icon.applyFromWorkspaceItem(info);
+        return icon;
+    }
+
+    /**
+     * Draws Predicted Icon outline on cell layout
+     */
+    public static class PredictedIconOutlineDrawing extends CellLayout.DelegatedCellDrawing {
+
+        private int mOffsetX;
+        private int mOffsetY;
+        private int mIconRadius;
+        private Paint mOutlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+        public PredictedIconOutlineDrawing(int cellX, int cellY, PredictedAppIcon icon) {
+            mDelegateCellX = cellX;
+            mDelegateCellY = cellY;
+            mOffsetX = icon.getOutlineOffsetX();
+            mOffsetY = icon.getOutlineOffsetY();
+            mIconRadius = icon.mNormalizedIconRadius;
+            mOutlinePaint.setStyle(Paint.Style.STROKE);
+            mOutlinePaint.setStrokeWidth(5);
+            mOutlinePaint.setPathEffect(new DashPathEffect(new float[]{15, 15}, 0));
+            mOutlinePaint.setColor(Color.argb(100, 245, 245, 245));
+        }
+
+        /**
+         * Draws predicted app icon outline under CellLayout
+         */
+        @Override
+        public void drawUnderItem(Canvas canvas) {
+            getShape().drawShape(canvas, mOffsetX, mOffsetY, mIconRadius, mOutlinePaint);
+        }
+
+        /**
+         * Draws PredictedAppIcon outline over CellLayout
+         */
+        @Override
+        public void drawOverItem(Canvas canvas) {
+            // Does nothing
+        }
+    }
+}
diff --git a/src/com/android/launcher3/CellLayout.java b/src/com/android/launcher3/CellLayout.java
index 976ccd5..89bec98 100644
--- a/src/com/android/launcher3/CellLayout.java
+++ b/src/com/android/launcher3/CellLayout.java
@@ -110,7 +110,7 @@
 
     private OnTouchListener mInterceptTouchListener;
 
-    private final ArrayList<PreviewBackground> mFolderBackgrounds = new ArrayList<>();
+    private final ArrayList<DelegatedCellDrawing> mDelegatedCellDrawings = new ArrayList<>();
     final PreviewBackground mFolderLeaveBehind = new PreviewBackground();
 
     private static final int[] BACKGROUND_STATE_ACTIVE = new int[] { android.R.attr.state_active };
@@ -219,8 +219,8 @@
         mPreviousReorderDirection[0] = INVALID_DIRECTION;
         mPreviousReorderDirection[1] = INVALID_DIRECTION;
 
-        mFolderLeaveBehind.delegateCellX = -1;
-        mFolderLeaveBehind.delegateCellY = -1;
+        mFolderLeaveBehind.mDelegateCellX = -1;
+        mFolderLeaveBehind.mDelegateCellY = -1;
 
         setAlwaysDrawnWithCacheEnabled(false);
         final Resources res = getResources();
@@ -466,21 +466,18 @@
             }
         }
 
-        for (int i = 0; i < mFolderBackgrounds.size(); i++) {
-            PreviewBackground bg = mFolderBackgrounds.get(i);
-            cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation);
+        for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
+            DelegatedCellDrawing cellDrawing = mDelegatedCellDrawings.get(i);
+            cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation);
             canvas.save();
             canvas.translate(mTempLocation[0], mTempLocation[1]);
-            bg.drawBackground(canvas);
-            if (!bg.isClipping) {
-                bg.drawBackgroundStroke(canvas);
-            }
+            cellDrawing.drawUnderItem(canvas);
             canvas.restore();
         }
 
-        if (mFolderLeaveBehind.delegateCellX >= 0 && mFolderLeaveBehind.delegateCellY >= 0) {
-            cellToPoint(mFolderLeaveBehind.delegateCellX,
-                    mFolderLeaveBehind.delegateCellY, mTempLocation);
+        if (mFolderLeaveBehind.mDelegateCellX >= 0 && mFolderLeaveBehind.mDelegateCellY >= 0) {
+            cellToPoint(mFolderLeaveBehind.mDelegateCellX,
+                    mFolderLeaveBehind.mDelegateCellY, mTempLocation);
             canvas.save();
             canvas.translate(mTempLocation[0], mTempLocation[1]);
             mFolderLeaveBehind.drawLeaveBehind(canvas);
@@ -492,23 +489,28 @@
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
 
-        for (int i = 0; i < mFolderBackgrounds.size(); i++) {
-            PreviewBackground bg = mFolderBackgrounds.get(i);
-            if (bg.isClipping) {
-                cellToPoint(bg.delegateCellX, bg.delegateCellY, mTempLocation);
-                canvas.save();
-                canvas.translate(mTempLocation[0], mTempLocation[1]);
-                bg.drawBackgroundStroke(canvas);
-                canvas.restore();
-            }
+        for (int i = 0; i < mDelegatedCellDrawings.size(); i++) {
+            DelegatedCellDrawing bg = mDelegatedCellDrawings.get(i);
+            cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation);
+            canvas.save();
+            canvas.translate(mTempLocation[0], mTempLocation[1]);
+            bg.drawOverItem(canvas);
+            canvas.restore();
         }
     }
 
-    public void addFolderBackground(PreviewBackground bg) {
-        mFolderBackgrounds.add(bg);
+    /**
+     * Add Delegated cell drawing
+     */
+    public void addDelegatedCellDrawing(DelegatedCellDrawing bg) {
+        mDelegatedCellDrawings.add(bg);
     }
-    public void removeFolderBackground(PreviewBackground bg) {
-        mFolderBackgrounds.remove(bg);
+
+    /**
+     * Remove item from DelegatedCellDrawings
+     */
+    public void removeDelegatedCellDrawing(DelegatedCellDrawing bg) {
+        mDelegatedCellDrawings.remove(bg);
     }
 
     public void setFolderLeaveBehindCell(int x, int y) {
@@ -516,14 +518,14 @@
         mFolderLeaveBehind.setup(getContext(), mActivity, null,
                 child.getMeasuredWidth(), child.getPaddingTop());
 
-        mFolderLeaveBehind.delegateCellX = x;
-        mFolderLeaveBehind.delegateCellY = y;
+        mFolderLeaveBehind.mDelegateCellX = x;
+        mFolderLeaveBehind.mDelegateCellY = y;
         invalidate();
     }
 
     public void clearFolderLeaveBehind() {
-        mFolderLeaveBehind.delegateCellX = -1;
-        mFolderLeaveBehind.delegateCellY = -1;
+        mFolderLeaveBehind.mDelegateCellX = -1;
+        mFolderLeaveBehind.mDelegateCellY = -1;
         invalidate();
     }
 
@@ -2744,6 +2746,24 @@
     }
 
     /**
+     * A Delegated cell Drawing for drawing on CellLayout
+     */
+    public abstract static class DelegatedCellDrawing {
+        public int mDelegateCellX;
+        public int mDelegateCellY;
+
+        /**
+         * Draw under CellLayout
+         */
+        public abstract void drawUnderItem(Canvas canvas);
+
+        /**
+         * Draw over CellLayout
+         */
+        public abstract void drawOverItem(Canvas canvas);
+    }
+
+    /**
      * Returns whether an item can be placed in this CellLayout (after rearranging and/or resizing
      * if necessary).
      */
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index b2c0ca7..2d177d2 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -48,7 +48,7 @@
  * This object represents a FolderIcon preview background. It stores drawing / measurement
  * information, handles drawing, and animation (accept state <--> rest state).
  */
-public class PreviewBackground {
+public class PreviewBackground extends CellLayout.DelegatedCellDrawing {
 
     private static final int CONSUMPTION_ANIMATION_DURATION = 100;
 
@@ -76,8 +76,6 @@
     int basePreviewOffsetY;
 
     private CellLayout mDrawingDelegate;
-    public int delegateCellX;
-    public int delegateCellY;
 
     // When the PreviewBackground is drawn under an icon (for creating a folder) the border
     // should not occlude the icon
@@ -124,6 +122,27 @@
                 }
             };
 
+    /**
+     * Draws folder background under cell layout
+     */
+    @Override
+    public void drawUnderItem(Canvas canvas) {
+        drawBackground(canvas);
+        if (!isClipping) {
+            drawBackgroundStroke(canvas);
+        }
+    }
+
+    /**
+     * Draws folder background on cell layout
+     */
+    @Override
+    public void drawOverItem(Canvas canvas) {
+        if (isClipping) {
+            drawBackgroundStroke(canvas);
+        }
+    }
+
     public void setup(Context context, ActivityContext activity, View invalidateDelegate,
                       int availableSpaceX, int topPadding) {
         mInvalidateDelegate = invalidateDelegate;
@@ -317,19 +336,19 @@
 
     private void delegateDrawing(CellLayout delegate, int cellX, int cellY) {
         if (mDrawingDelegate != delegate) {
-            delegate.addFolderBackground(this);
+            delegate.addDelegatedCellDrawing(this);
         }
 
         mDrawingDelegate = delegate;
-        delegateCellX = cellX;
-        delegateCellY = cellY;
+        mDelegateCellX = cellX;
+        mDelegateCellY = cellY;
 
         invalidate();
     }
 
     private void clearDrawingDelegate() {
         if (mDrawingDelegate != null) {
-            mDrawingDelegate.removeFolderBackground(this);
+            mDrawingDelegate.removeDelegatedCellDrawing(this);
         }
 
         mDrawingDelegate = null;
@@ -395,8 +414,8 @@
         // is saved and restored at the beginning of the animation, since cancelling the
         // existing animation can clear the delgate.
         CellLayout cl = mDrawingDelegate;
-        int cellX = delegateCellX;
-        int cellY = delegateCellY;
+        int cellX = mDelegateCellX;
+        int cellY = mDelegateCellY;
         animateScale(1f, 1f, () -> delegateDrawing(cl, cellX, cellY), this::clearDrawingDelegate);
     }