Adding spring effect for folder icons when dragging similar to adaptive icons

Bug: 63889144
Change-Id: Idd1fd13c6343bf3e0a3a0e5ff0be730d41996575
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 97f1ead..4b31486 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1465,23 +1465,25 @@
             mWorkspace.addInScreen(view, info);
         } else {
             // Adding a shortcut to a Folder.
-            final long folderIconId = container;
-            FolderIcon folderIcon = (FolderIcon) mWorkspace.getFirstMatch(new ItemOperator() {
-                @Override
-                public boolean evaluate(ItemInfo info, View view) {
-                    return info != null && info.id == folderIconId;
-                }
-            });
-
+            FolderIcon folderIcon = findFolderIcon(container);
             if (folderIcon != null) {
                 FolderInfo folderInfo = (FolderInfo) folderIcon.getTag();
                 folderInfo.add(info, args.rank, false);
             } else {
-                Log.e(TAG, "Could not find folder with id " + folderIconId + " to add shortcut.");
+                Log.e(TAG, "Could not find folder with id " + container + " to add shortcut.");
             }
         }
     }
 
+    public FolderIcon findFolderIcon(final long folderIconId) {
+        return (FolderIcon) mWorkspace.getFirstMatch(new ItemOperator() {
+            @Override
+            public boolean evaluate(ItemInfo info, View view) {
+                return info != null && info.id == folderIconId;
+            }
+        });
+    }
+
     /**
      * Add a widget to the workspace.
      *
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index c11287a..4985413 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -78,6 +78,7 @@
 
     @Thunk static float sDragAlpha = 1f;
 
+    private boolean mDrawBitmap = true;
     private Bitmap mBitmap;
     private Bitmap mCrossFadeBitmap;
     @Thunk Paint mPaint;
@@ -187,7 +188,8 @@
     }
 
     /**
-     * Initialize {@code #mIconDrawable} only if the icon type is app icon (not shortcut or folder).
+     * Initialize {@code #mIconDrawable} if the item can be represented using
+     * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}.
      */
     @TargetApi(Build.VERSION_CODES.O)
     public void setItemInfo(final ItemInfo info) {
@@ -195,7 +197,8 @@
             return;
         }
         if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
-                info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+                info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
+                info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
             return;
         }
         // Load the adaptive icon on a background thread and add the view in ui thread.
@@ -205,7 +208,7 @@
             public void run() {
                 LauncherAppState appState = LauncherAppState.getInstance(mLauncher);
                 Object[] outObj = new Object[1];
-                Drawable dr = getFullDrawable(info, appState, outObj);
+                final Drawable dr = getFullDrawable(info, appState, outObj);
 
                 if (dr instanceof AdaptiveIconDrawable) {
                     int w = mBitmap.getWidth();
@@ -249,6 +252,9 @@
                             // Assign the variable on the UI thread to avoid race conditions.
                             mScaledMaskPath = mask;
 
+                            // Do not draw the background in case of folder as its translucent
+                            mDrawBitmap = !(dr instanceof FolderAdaptiveIcon);
+
                             if (info.isDisabled()) {
                                 FastBitmapDrawable d = new FastBitmapDrawable(null);
                                 d.setIsDisabled(true);
@@ -323,6 +329,14 @@
                 return sm.getShortcutIconDrawable(si.get(0),
                         appState.getInvariantDeviceProfile().fillResIconDpi);
             }
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+            FolderAdaptiveIcon icon =  FolderAdaptiveIcon.createFolderAdaptiveIcon(
+                    mLauncher, info.id, new Point(mBitmap.getWidth(), mBitmap.getHeight()));
+            if (icon == null) {
+                return null;
+            }
+            outObj[0] = icon;
+            return icon;
         } else {
             return null;
         }
@@ -350,6 +364,8 @@
             float insetFraction = (iconSize - badgeSize) / iconSize;
             return new InsetDrawable(new FastBitmapDrawable(badge),
                     insetFraction, insetFraction, 0, 0);
+        } else if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
+            return ((FolderAdaptiveIcon) obj).getBadge();
         } else {
             return mLauncher.getPackageManager()
                     .getUserBadgedIcon(new FixedSizeEmptyDrawable(iconSize), info.user);
@@ -405,21 +421,24 @@
     @Override
     protected void onDraw(Canvas canvas) {
         mHasDrawn = true;
-        // Always draw the bitmap to mask anti aliasing due to clipPath
-        boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null;
-        if (crossFade) {
-            int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255;
-            mPaint.setAlpha(alpha);
-        }
-        canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint);
-        if (crossFade) {
-            mPaint.setAlpha((int) (255 * mCrossFadeProgress));
-            final int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
-            float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth();
-            float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight();
-            canvas.scale(sX, sY);
-            canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint);
-            canvas.restoreToCount(saveCount);
+
+        if (mDrawBitmap) {
+            // Always draw the bitmap to mask anti aliasing due to clipPath
+            boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null;
+            if (crossFade) {
+                int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255;
+                mPaint.setAlpha(alpha);
+            }
+            canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint);
+            if (crossFade) {
+                mPaint.setAlpha((int) (255 * mCrossFadeProgress));
+                final int saveCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
+                float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth();
+                float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight();
+                canvas.scale(sX, sY);
+                canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint);
+                canvas.restoreToCount(saveCount);
+            }
         }
 
         if (mScaledMaskPath != null) {
diff --git a/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
new file mode 100644
index 0000000..c905460
--- /dev/null
+++ b/src/com/android/launcher3/dragndrop/FolderAdaptiveIcon.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2017 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.dragndrop;
+
+import android.annotation.TargetApi;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.Point;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.Log;
+
+import com.android.launcher3.Launcher;
+import com.android.launcher3.MainThreadExecutor;
+import com.android.launcher3.R;
+import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.folder.PreviewBackground;
+import com.android.launcher3.util.Preconditions;
+
+import java.util.concurrent.Callable;
+
+/**
+ * {@link AdaptiveIconDrawable} representation of a {@link FolderIcon}
+ */
+@TargetApi(Build.VERSION_CODES.O)
+public class FolderAdaptiveIcon extends AdaptiveIconDrawable {
+    private static final String TAG = "FolderAdaptiveIcon";
+
+    private final Drawable mBadge;
+    private final Path mMask;
+
+    private FolderAdaptiveIcon(Drawable bg, Drawable fg, Drawable badge, Path mask) {
+        super(bg, fg);
+        mBadge = badge;
+        mMask = mask;
+    }
+
+    @Override
+    public Path getIconMask() {
+        return mMask;
+    }
+
+    public Drawable getBadge() {
+        return mBadge;
+    }
+
+    public static FolderAdaptiveIcon createFolderAdaptiveIcon(
+            final Launcher launcher, final long folderId, Point dragViewSize) {
+        Preconditions.assertNonUiThread();
+        int margin = launcher.getResources()
+                .getDimensionPixelSize(R.dimen.blur_size_medium_outline);
+
+        // Allocate various bitmaps on the background thread, because why not!
+        final Bitmap badge = Bitmap.createBitmap(
+                dragViewSize.x - margin, dragViewSize.y - margin, Bitmap.Config.ARGB_8888);
+
+        // The bitmap for the preview is generated larger than needed to allow for the spring effect
+        float sizeScaleFactor = 1 + 2 * AdaptiveIconDrawable.getExtraInsetFraction();
+        final Bitmap preview = Bitmap.createBitmap(
+                (int) (dragViewSize.x * sizeScaleFactor), (int) (dragViewSize.y * sizeScaleFactor),
+                Bitmap.Config.ARGB_8888);
+
+        // Create the actual drawable on the UI thread to avoid race conditions with
+        // FolderIcon draw pass
+        try {
+            return new MainThreadExecutor().submit(new Callable<FolderAdaptiveIcon>() {
+                @Override
+                public FolderAdaptiveIcon call() throws Exception {
+                    FolderIcon icon = launcher.findFolderIcon(folderId);
+                    return icon == null ? null : createDrawableOnUiThread(icon, badge, preview);
+                }
+            }).get();
+        } catch (Exception e) {
+            Log.e(TAG, "Unable to create folder icon", e);
+            return null;
+        }
+    }
+
+    /**
+     * Initializes various bitmaps on the UI thread and returns the final drawable.
+     */
+    private static FolderAdaptiveIcon createDrawableOnUiThread(FolderIcon icon,
+            Bitmap badgeBitmap, Bitmap previewBitmap) {
+        Preconditions.assertUIThread();
+        float margin = icon.getResources().getDimension(R.dimen.blur_size_medium_outline) / 2;
+
+        Canvas c = new Canvas();
+        PreviewBackground bg = icon.getFolderBackground();
+
+        // Initialize badge
+        c.setBitmap(badgeBitmap);
+        bg.drawShadow(c);
+        bg.drawBackgroundStroke(c);
+        icon.drawBadge(c);
+
+        // Initialize preview
+        float shiftFactor = AdaptiveIconDrawable.getExtraInsetFraction() /
+                (1 + 2 * AdaptiveIconDrawable.getExtraInsetFraction());
+        float previewShiftX = shiftFactor * previewBitmap.getWidth();
+        float previewShiftY = shiftFactor * previewBitmap.getHeight();
+
+        c.setBitmap(previewBitmap);
+        c.translate(previewShiftX, previewShiftY);
+        icon.getPreviewItemManager().draw(c);
+        c.setBitmap(null);
+
+        // Initialize mask
+        Path mask = new Path();
+        Matrix m = new Matrix();
+        m.setTranslate(margin, margin);
+        bg.getClipPath().transform(m, mask);
+
+        ShiftedBitmapDrawable badge = new ShiftedBitmapDrawable(badgeBitmap, margin, margin);
+        ShiftedBitmapDrawable foreground = new ShiftedBitmapDrawable(previewBitmap,
+                margin - previewShiftX, margin - previewShiftY);
+
+        return new FolderAdaptiveIcon(new ColorDrawable(bg.getBgColor()), foreground, badge, mask);
+    }
+
+    /**
+     * A simple drawable which draws a bitmap at a fixed position irrespective of the bounds
+     */
+    private static class ShiftedBitmapDrawable extends Drawable {
+
+        private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
+        private final Bitmap mBitmap;
+        private final float mShiftX;
+        private final float mShiftY;
+
+        ShiftedBitmapDrawable(Bitmap bitmap, float shiftX, float shiftY) {
+            mBitmap = bitmap;
+            mShiftX = shiftX;
+            mShiftY = shiftY;
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            canvas.drawBitmap(mBitmap, mShiftX, mShiftY, mPaint);
+        }
+
+        @Override
+        public void setAlpha(int i) { }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+            mPaint.setColorFilter(colorFilter);
+        }
+
+        @Override
+        public int getOpacity() {
+            return PixelFormat.TRANSLUCENT;
+        }
+    }
+}
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 84ec184..6533b04 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -440,6 +440,14 @@
         invalidate();
     }
 
+    public PreviewBackground getFolderBackground() {
+        return mBackground;
+    }
+
+    public PreviewItemManager getPreviewItemManager() {
+        return mPreviewItemManager;
+    }
+
     @Override
     protected void dispatchDraw(Canvas canvas) {
         super.dispatchDraw(canvas);
@@ -463,14 +471,11 @@
         } else {
             saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
             if (mPreviewLayoutRule.clipToBackground()) {
-                mBackground.clipCanvasSoftware(canvas, Region.Op.INTERSECT);
+                canvas.clipPath(mBackground.getClipPath(), Region.Op.INTERSECT);
             }
         }
 
-        // The items are drawn in coordinates relative to the preview offset
-        canvas.translate(mBackground.basePreviewOffsetX, mBackground.basePreviewOffsetY);
         mPreviewItemManager.draw(canvas);
-        canvas.translate(-mBackground.basePreviewOffsetX, -mBackground.basePreviewOffsetY);
 
         if (mPreviewLayoutRule.clipToBackground() && canvas.isHardwareAccelerated()) {
             mBackground.clipCanvasHardware(canvas);
@@ -481,6 +486,10 @@
             mBackground.drawBackgroundStroke(canvas);
         }
 
+        drawBadge(canvas);
+    }
+
+    public void drawBadge(Canvas canvas) {
         if ((mBadgeInfo != null && mBadgeInfo.hasBadge()) || mBadgeScale > 0) {
             int offsetX = mBackground.getOffsetX();
             int offsetY = mBackground.getOffsetY();
diff --git a/src/com/android/launcher3/folder/PreviewBackground.java b/src/com/android/launcher3/folder/PreviewBackground.java
index 61490ee..eba5d98 100644
--- a/src/com/android/launcher3/folder/PreviewBackground.java
+++ b/src/com/android/launcher3/folder/PreviewBackground.java
@@ -195,19 +195,28 @@
         invalidate();
     }
 
+    public int getBgColor() {
+        int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
+        return ColorUtils.setAlphaComponent(mBgColor, alpha);
+    }
+
     public void drawBackground(Canvas canvas) {
         mPaint.setStyle(Paint.Style.FILL);
-        int alpha = (int) Math.min(MAX_BG_OPACITY, BG_OPACITY * mColorMultiplier);
-        mPaint.setColor(ColorUtils.setAlphaComponent(mBgColor, alpha));
+        mPaint.setColor(getBgColor());
 
         drawCircle(canvas, 0 /* deltaRadius */);
 
-        // Draw shadow.
+        drawShadow(canvas);
+    }
+
+    public void drawShadow(Canvas canvas) {
         if (mShadowShader == null) {
             return;
         }
+
         float radius = getScaledRadius();
         float shadowRadius = radius + mStrokeWidth;
+        mPaint.setStyle(Paint.Style.FILL);
         mPaint.setColor(Color.BLACK);
         int offsetX = getOffsetX();
         int offsetY = getOffsetY();
@@ -219,7 +228,7 @@
 
         } else {
             saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);
-            clipCanvasSoftware(canvas, Region.Op.DIFFERENCE);
+            canvas.clipPath(getClipPath(), Region.Op.DIFFERENCE);
         }
 
         mShaderMatrix.setScale(shadowRadius, shadowRadius);
@@ -295,12 +304,11 @@
                 radius - deltaRadius, mPaint);
     }
 
-    // It is the callers responsibility to save and restore the canvas layers.
-    void clipCanvasSoftware(Canvas canvas, Region.Op op) {
+    public Path getClipPath() {
         mPath.reset();
         float r = getScaledRadius();
         mPath.addCircle(r + getOffsetX(), r + getOffsetY(), r, Path.Direction.CW);
-        canvas.clipPath(mPath, op);
+        return mPath;
     }
 
     // It is the callers responsibility to save and restore the canvas layers.
diff --git a/src/com/android/launcher3/folder/PreviewItemManager.java b/src/com/android/launcher3/folder/PreviewItemManager.java
index 2ecb54c..2d979a6 100644
--- a/src/com/android/launcher3/folder/PreviewItemManager.java
+++ b/src/com/android/launcher3/folder/PreviewItemManager.java
@@ -146,6 +146,10 @@
     }
 
     public void draw(Canvas canvas) {
+        // The items are drawn in coordinates relative to the preview offset
+        PreviewBackground bg = mIcon.getFolderBackground();
+        canvas.translate(bg.basePreviewOffsetX, bg.basePreviewOffsetY);
+
         float firstPageItemsTransX = 0;
         if (mShouldSlideInFirstPage) {
             drawParams(canvas, mCurrentPageParams, mCurrentPageItemsTransX);
@@ -154,6 +158,7 @@
         }
 
         drawParams(canvas, mFirstPageParams, firstPageItemsTransX);
+        canvas.translate(-bg.basePreviewOffsetX, -bg.basePreviewOffsetY);
     }
 
     public void onParamsChanged() {