Merge deep shortcuts in rounded rect

- DeepShortcutViews are added to ShortcutsItemView, which
  is in PopupContainerWithArrow
- Moved some shortcut-specific logic to ShortcutsItemView
  (namely, touch/long-click handling for draggin shortcuts)
- Moved round-rect clipping to PopupItemView
- Removed collapseToIcon() logic, including
  PillWidthRevealOutlineProvider, which was only used for
  that purpose. It isn't necessary now that the deep
  shortcuts have no background themselves.
- Replaced focus pill drawable with ripple effect on
  shortcuts and notification view.

Bug: 35766387
Change-Id: I6bc09f1851cfbb806df4bf75a6e435b0f1900c9c
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index 52a83dc..bd12686 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -95,7 +95,7 @@
         return null;
     }
 
-    protected static void closeOpenContainer(Launcher launcher, @FloatingViewType int type) {
+    public static void closeOpenContainer(Launcher launcher, @FloatingViewType int type) {
         AbstractFloatingView view = getOpenView(launcher, type);
         if (view != null) {
             view.close(true);
diff --git a/src/com/android/launcher3/badge/BadgeRenderer.java b/src/com/android/launcher3/badge/BadgeRenderer.java
index 864a65d..1f3e22d 100644
--- a/src/com/android/launcher3/badge/BadgeRenderer.java
+++ b/src/com/android/launcher3/badge/BadgeRenderer.java
@@ -22,7 +22,6 @@
 import android.graphics.Canvas;
 import android.graphics.Paint;
 import android.graphics.Rect;
-import android.graphics.RectF;
 import android.graphics.Shader;
 import android.support.annotation.Nullable;
 
diff --git a/src/com/android/launcher3/notification/NotificationItemView.java b/src/com/android/launcher3/notification/NotificationItemView.java
index 742c90a..efd9a3b 100644
--- a/src/com/android/launcher3/notification/NotificationItemView.java
+++ b/src/com/android/launcher3/notification/NotificationItemView.java
@@ -20,14 +20,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.content.Context;
 import android.content.res.ColorStateList;
-import android.graphics.Bitmap;
-import android.graphics.BitmapShader;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
-import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
@@ -36,11 +29,11 @@
 
 import com.android.launcher3.ItemInfo;
 import com.android.launcher3.R;
+import com.android.launcher3.anim.PillHeightRevealOutlineProvider;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider;
 import com.android.launcher3.popup.PopupItemView;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
-import com.android.launcher3.anim.PillHeightRevealOutlineProvider;
 
 import java.util.List;
 
@@ -54,9 +47,6 @@
 
     private static final Rect sTempRect = new Rect();
 
-    private final Paint mBackgroundClipPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG |
-            Paint.FILTER_BITMAP_FLAG);
-
     private View mDivider;
     private NotificationMainView mMainView;
     private NotificationFooterLayout mFooter;
@@ -85,35 +75,6 @@
         mSwipeHelper.setDisableHardwareLayers(true);
     }
 
-    private void initializeBackgroundClipping(boolean force) {
-        if (force || mBackgroundClipPaint.getShader() == null) {
-            mBackgroundClipPaint.setXfermode(null);
-            mBackgroundClipPaint.setShader(null);
-            Bitmap backgroundBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(),
-                    Bitmap.Config.ALPHA_8);
-            Canvas canvas = new Canvas();
-            canvas.setBitmap(backgroundBitmap);
-            float roundRectRadius = getResources().getDimensionPixelSize(
-                    R.dimen.bg_round_rect_radius);
-            canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(),
-                    roundRectRadius, roundRectRadius, mBackgroundClipPaint);
-            Shader backgroundClipShader = new BitmapShader(backgroundBitmap,
-                    Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
-            mBackgroundClipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
-            mBackgroundClipPaint.setShader(backgroundClipShader);
-        }
-    }
-
-    @Override
-    protected void dispatchDraw(Canvas canvas) {
-        initializeBackgroundClipping(false /* force */);
-        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null,
-                Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
-        super.dispatchDraw(canvas);
-        canvas.drawPaint(mBackgroundClipPaint);
-        canvas.restoreToCount(saveCount);
-    }
-
     public Animator animateHeightRemoval(int heightToRemove) {
         final int newHeight = getHeight() - heightToRemove;
         Animator heightAnimator = new PillHeightRevealOutlineProvider(mPillRect,
@@ -135,11 +96,6 @@
     }
 
     @Override
-    protected float getBackgroundRadius() {
-        return getResources().getDimensionPixelSize(R.dimen.bg_round_rect_radius);
-    }
-
-    @Override
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         if (mMainView.getNotificationInfo() == null) {
             // The notification hasn't been populated yet.
diff --git a/src/com/android/launcher3/notification/NotificationMainView.java b/src/com/android/launcher3/notification/NotificationMainView.java
index b342525..824dbf2 100644
--- a/src/com/android/launcher3/notification/NotificationMainView.java
+++ b/src/com/android/launcher3/notification/NotificationMainView.java
@@ -22,7 +22,9 @@
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
 import android.content.Context;
+import android.content.res.ColorStateList;
 import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.RippleDrawable;
 import android.util.AttributeSet;
 import android.view.MotionEvent;
 import android.view.View;
@@ -71,7 +73,9 @@
 
     public void applyColors(IconPalette iconPalette) {
         mColorBackground = new ColorDrawable(iconPalette.backgroundColor);
-        setBackground(mColorBackground);
+        RippleDrawable rippleDrawable = new RippleDrawable(ColorStateList.valueOf(
+                iconPalette.secondaryColor), mColorBackground, null);
+        setBackground(rippleDrawable);
         mIconPalette = iconPalette;
     }
 
diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
index 7811b96..15dde43 100644
--- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java
+++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java
@@ -27,7 +27,6 @@
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Color;
-import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.ShapeDrawable;
@@ -64,15 +63,13 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragOptions;
-import com.android.launcher3.dragndrop.DragView;
 import com.android.launcher3.graphics.IconPalette;
 import com.android.launcher3.graphics.TriangleShape;
 import com.android.launcher3.notification.NotificationItemView;
 import com.android.launcher3.shortcuts.DeepShortcutView;
-import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider;
+import com.android.launcher3.shortcuts.ShortcutsItemView;
 import com.android.launcher3.util.PackageUserKey;
 
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -84,18 +81,17 @@
  * A container for shortcuts to deep links within apps.
  */
 @TargetApi(Build.VERSION_CODES.N)
-public class PopupContainerWithArrow extends AbstractFloatingView
-        implements View.OnLongClickListener, View.OnTouchListener, DragSource,
+public class PopupContainerWithArrow extends AbstractFloatingView implements DragSource,
         DragController.DragListener {
 
-    private final Point mIconShift = new Point();
-    private final Point mIconLastTouchPos = new Point();
-
     protected final Launcher mLauncher;
     private final int mStartDragThreshold;
     private LauncherAccessibilityDelegate mAccessibilityDelegate;
     private final boolean mIsRtl;
 
+    public ShortcutsItemView mShortcutsItemView;
+    private NotificationItemView mNotificationItemView;
+
     protected BubbleTextView mOriginalIcon;
     private final Rect mTempRect = new Rect();
     private PointF mInterceptTouchDown = new PointF();
@@ -177,6 +173,8 @@
         boolean reverseOrder = mIsAboveIcon;
         if (reverseOrder) {
             removeAllViews();
+            mNotificationItemView = null;
+            mShortcutsItemView = null;
             itemsToPopulate = PopupPopulator.reverseItems(itemsToPopulate);
             addDummyViews(originalIcon, itemsToPopulate, notificationKeys.length > 1);
 
@@ -184,22 +182,12 @@
             orientAboutIcon(originalIcon, arrowHeight + arrowVerticalOffset);
         }
 
-        List<DeepShortcutView> shortcutViews = new ArrayList<>();
-        NotificationItemView notificationView = null;
-        for (int i = 0; i < getChildCount(); i++) {
-            View item = getChildAt(i);
+        List<DeepShortcutView> shortcutViews = mShortcutsItemView.getDeepShortcutViews(reverseOrder);
+        for (int i = 0; i < itemsToPopulate.length; i++) {
             switch (itemsToPopulate[i]) {
-                case SHORTCUT:
-                    if (reverseOrder) {
-                        shortcutViews.add(0, (DeepShortcutView) item);
-                    } else {
-                        shortcutViews.add((DeepShortcutView) item);
-                    }
-                    break;
                 case NOTIFICATION:
-                    notificationView = (NotificationItemView) item;
                     IconPalette iconPalette = originalIcon.getIconPalette();
-                    notificationView.applyColors(iconPalette);
+                    mNotificationItemView.applyColors(iconPalette);
                     break;
             }
         }
@@ -221,28 +209,46 @@
         final Looper workerLooper = LauncherModel.getWorkerLooper();
         new Handler(workerLooper).postAtFrontOfQueue(PopupPopulator.createUpdateRunnable(
                 mLauncher, (ItemInfo) originalIcon.getTag(), new Handler(Looper.getMainLooper()),
-                this, shortcutIds, shortcutViews, notificationKeys, notificationView));
+                this, shortcutIds, shortcutViews, notificationKeys, mNotificationItemView));
     }
 
     private void addDummyViews(BubbleTextView originalIcon,
-            PopupPopulator.Item[] itemsToPopulate, boolean notificationFooterHasIcons) {
+            PopupPopulator.Item[] itemTypesToPopulate, boolean notificationFooterHasIcons) {
         final Resources res = getResources();
-        final int spacing = res.getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
+        final int spacing = res.getDimensionPixelSize(R.dimen.popup_items_spacing);
         final LayoutInflater inflater = mLauncher.getLayoutInflater();
-        int numItems = itemsToPopulate.length;
+        int numItems = itemTypesToPopulate.length;
         for (int i = 0; i < numItems; i++) {
-            final PopupItemView item = (PopupItemView) inflater.inflate(
-                    itemsToPopulate[i].layoutId, this, false);
-            if (itemsToPopulate[i] == PopupPopulator.Item.NOTIFICATION) {
+            PopupPopulator.Item itemTypeToPopulate = itemTypesToPopulate[i];
+            final View item = inflater.inflate(itemTypeToPopulate.layoutId, this, false);
+
+            if (itemTypeToPopulate == PopupPopulator.Item.NOTIFICATION) {
+                mNotificationItemView = (NotificationItemView) item;
                 int footerHeight = notificationFooterHasIcons ?
                         res.getDimensionPixelSize(R.dimen.notification_footer_height) : 0;
                 item.findViewById(R.id.footer).getLayoutParams().height = footerHeight;
             }
-            if (i < numItems - 1) {
-                ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
-            }
+
+            boolean itemIsFollowedByDifferentType = i < numItems - 1
+                    && itemTypesToPopulate[i + 1] != itemTypeToPopulate;
+
             item.setAccessibilityDelegate(mAccessibilityDelegate);
-            addView(item);
+            if (itemTypeToPopulate == PopupPopulator.Item.SHORTCUT) {
+                if (mShortcutsItemView == null) {
+                    mShortcutsItemView = (ShortcutsItemView) inflater.inflate(
+                            R.layout.shortcuts_item, this, false);
+                    addView(mShortcutsItemView);
+                }
+                mShortcutsItemView.addDeepShortcutView((DeepShortcutView) item);
+                if (itemIsFollowedByDifferentType) {
+                    ((LayoutParams) mShortcutsItemView.getLayoutParams()).bottomMargin = spacing;
+                }
+            } else {
+                addView(item);
+                if (itemIsFollowedByDifferentType) {
+                    ((LayoutParams) item.getLayoutParams()).bottomMargin = spacing;
+                }
+            }
         }
         // TODO: update this, since not all items are shortcuts
         setContentDescription(getContext().getString(R.string.shortcuts_menu_description,
@@ -534,49 +540,8 @@
         return true;
     }
 
-    @Override
-    public boolean onTouch(View v, MotionEvent ev) {
-        // Touched a shortcut, update where it was touched so we can drag from there on long click.
-        switch (ev.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-            case MotionEvent.ACTION_MOVE:
-                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
-                break;
-        }
-        return false;
-    }
-
-    public boolean onLongClick(View v) {
-        // Return early if this is not initiated from a touch or not the correct view
-        if (!v.isInTouchMode() || !(v.getParent() instanceof DeepShortcutView)) return false;
-        // Return early if global dragging is not enabled
-        if (!mLauncher.isDraggingEnabled()) return false;
-        // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts)
-        if (mLauncher.getDragController().isDragging()) return false;
-
-        // Long clicked on a shortcut.
-        mDeferContainerRemoval = true;
-        DeepShortcutView sv = (DeepShortcutView) v.getParent();
-        sv.setWillDrawIcon(false);
-
-        // Move the icon to align with the center-top of the touch point
-        mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
-        mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
-
-        DragView dv = mLauncher.getWorkspace().beginDragShared(
-                sv.getBubbleText(), this, sv.getFinalInfo(),
-                new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions());
-        dv.animateShift(-mIconShift.x, -mIconShift.y);
-
-        // TODO: support dragging from within folder without having to close it
-        AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
-        return false;
-    }
-
     public void trimNotifications(Map<PackageUserKey, BadgeInfo> updatedBadges) {
-        final NotificationItemView notificationView =
-                (NotificationItemView) findViewById(R.id.notification_view);
-        if (notificationView == null) {
+        if (mNotificationItemView == null) {
             return;
         }
         ItemInfo originalInfo = (ItemInfo) mOriginalIcon.getTag();
@@ -585,11 +550,11 @@
             AnimatorSet removeNotification = LauncherAnimUtils.createAnimatorSet();
             final int duration = getResources().getInteger(
                     R.integer.config_removeNotificationViewDuration);
-            final int spacing = getResources().getDimensionPixelSize(R.dimen.deep_shortcuts_spacing);
+            final int spacing = getResources().getDimensionPixelSize(R.dimen.popup_items_spacing);
             removeNotification.play(reduceNotificationViewHeight(
-                    notificationView.getHeight() + spacing, duration, notificationView));
+                    mNotificationItemView.getHeight() + spacing, duration, mNotificationItemView));
             final View removeMarginView = mIsAboveIcon ? getItemViewAt(getItemCount() - 2)
-                    : notificationView;
+                    : mNotificationItemView;
             if (removeMarginView != null) {
                 ValueAnimator removeMargin = ValueAnimator.ofFloat(1, 0).setDuration(duration);
                 removeMargin.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@@ -601,12 +566,12 @@
                 });
                 removeNotification.play(removeMargin);
             }
-            Animator fade = ObjectAnimator.ofFloat(notificationView, ALPHA, 0)
+            Animator fade = ObjectAnimator.ofFloat(mNotificationItemView, ALPHA, 0)
                     .setDuration(duration);
             fade.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
-                    removeView(notificationView);
+                    removeView(mNotificationItemView);
                     if (getItemCount() == 0) {
                         close(false);
                         return;
@@ -626,7 +591,7 @@
             removeNotification.start();
             return;
         }
-        notificationView.trimNotifications(badgeInfo.getNotificationKeys());
+        mNotificationItemView.trimNotifications(badgeInfo.getNotificationKeys());
     }
 
     private ObjectAnimator createArrowScaleAnim(float scale) {
@@ -669,8 +634,7 @@
     }
 
     public Animator reduceNotificationViewHeight(int heightToRemove, int duration) {
-        return reduceNotificationViewHeight(heightToRemove, duration,
-                (NotificationItemView) findViewById(R.id.notification_view));
+        return reduceNotificationViewHeight(heightToRemove, duration, mNotificationItemView);
     }
 
     @Override
@@ -702,6 +666,7 @@
     public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
         // Either the original icon or one of the shortcuts was dragged.
         // Hide the container, but don't remove it yet because that interferes with touch events.
+        mDeferContainerRemoval = true;
         animateClose();
     }
 
@@ -723,7 +688,6 @@
     @Override
     public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) {
         target.itemType = ItemType.DEEPSHORTCUT;
-        target.rank = info.rank;
         targetParent.containerType = ContainerType.DEEPSHORTCUTS;
     }
 
@@ -765,38 +729,17 @@
         for (int i = firstOpenItemIndex; i < firstOpenItemIndex + numOpenShortcuts; i++) {
             final PopupItemView view = getItemViewAt(i);
             Animator anim;
-            if (view.willDrawIcon()) {
-                anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
-                int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
-                        : numOpenShortcuts - i - 1;
-                anim.setStartDelay(stagger * animationIndex);
+            anim = view.createCloseAnimation(mIsAboveIcon, mIsLeftAligned, duration);
+            int animationIndex = mIsAboveIcon ? i - firstOpenItemIndex
+                    : numOpenShortcuts - i - 1;
+            anim.setStartDelay(stagger * animationIndex);
 
-                Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
-                // Don't start fading until the arrow is gone.
-                fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
-                fadeAnim.setDuration(duration - arrowScaleDuration);
-                fadeAnim.setInterpolator(fadeInterpolator);
-                shortcutAnims.play(fadeAnim);
-            } else {
-                // The view is being dragged. Animate it such that it collapses with the drag view
-                anim = view.collapseToIcon();
-                anim.setDuration(DragView.VIEW_ZOOM_DURATION);
-
-                // Scale and translate the view to follow the drag view.
-                Point iconCenter = view.getIconCenter();
-                view.setPivotX(iconCenter.x);
-                view.setPivotY(iconCenter.y);
-
-                float scale = ((float) mLauncher.getDeviceProfile().iconSizePx) / view.getHeight();
-                Animator anim2 = LauncherAnimUtils.ofPropertyValuesHolder(view,
-                        new PropertyListBuilder()
-                                .scale(scale)
-                                .translationX(mIconShift.x)
-                                .translationY(mIconShift.y)
-                                .build())
-                        .setDuration(DragView.VIEW_ZOOM_DURATION);
-                shortcutAnims.play(anim2);
-            }
+            Animator fadeAnim = ObjectAnimator.ofFloat(view, View.ALPHA, 0);
+            // Don't start fading until the arrow is gone.
+            fadeAnim.setStartDelay(stagger * animationIndex + arrowScaleDuration);
+            fadeAnim.setDuration(duration - arrowScaleDuration);
+            fadeAnim.setInterpolator(fadeInterpolator);
+            shortcutAnims.play(fadeAnim);
             anim.addListener(new AnimatorListenerAdapter() {
                 @Override
                 public void onAnimationEnd(Animator animation) {
diff --git a/src/com/android/launcher3/popup/PopupItemView.java b/src/com/android/launcher3/popup/PopupItemView.java
index b3d7155..dc4f415 100644
--- a/src/com/android/launcher3/popup/PopupItemView.java
+++ b/src/com/android/launcher3/popup/PopupItemView.java
@@ -21,8 +21,15 @@
 import android.animation.ValueAnimator;
 import android.content.Context;
 import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Paint;
 import android.graphics.Point;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
 import android.graphics.Rect;
+import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.view.View;
 import android.widget.FrameLayout;
@@ -31,7 +38,6 @@
 import com.android.launcher3.R;
 import com.android.launcher3.Utilities;
 import com.android.launcher3.util.PillRevealOutlineProvider;
-import com.android.launcher3.util.PillWidthRevealOutlineProvider;
 
 /**
  * An abstract {@link FrameLayout} that supports animating an item's content
@@ -47,6 +53,9 @@
 
     protected View mIconView;
 
+    private final Paint mBackgroundClipPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG |
+            Paint.FILTER_BITMAP_FLAG);
+
     public PopupItemView(Context context) {
         this(context, null, 0);
     }
@@ -73,12 +82,35 @@
         mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
     }
 
-    protected ColorStateList getAttachedArrowColor() {
-        return getBackgroundTintList();
+    protected void initializeBackgroundClipping(boolean force) {
+        if (force || mBackgroundClipPaint.getShader() == null) {
+            mBackgroundClipPaint.setXfermode(null);
+            mBackgroundClipPaint.setShader(null);
+            Bitmap backgroundBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(),
+                    Bitmap.Config.ALPHA_8);
+            Canvas canvas = new Canvas();
+            canvas.setBitmap(backgroundBitmap);
+            canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(),
+                    getBackgroundRadius(), getBackgroundRadius(), mBackgroundClipPaint);
+            Shader backgroundClipShader = new BitmapShader(backgroundBitmap,
+                    Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+            mBackgroundClipPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
+            mBackgroundClipPaint.setShader(backgroundClipShader);
+        }
     }
 
-    public boolean willDrawIcon() {
-        return true;
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        initializeBackgroundClipping(false /* force */);
+        int saveCount = canvas.saveLayer(0, 0, getWidth(), getHeight(), null,
+                Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
+        super.dispatchDraw(canvas);
+        canvas.drawPaint(mBackgroundClipPaint);
+        canvas.restoreToCount(saveCount);
+    }
+
+    protected ColorStateList getAttachedArrowColor() {
+        return getBackgroundTintList();
     }
 
     /**
@@ -126,17 +158,6 @@
     }
 
     /**
-     * Creates an animator which clips the container to form a circle around the icon.
-     */
-    public Animator collapseToIcon() {
-        int halfHeight = getMeasuredHeight() / 2;
-        int iconCenterX = getIconCenter().x;
-        return new PillWidthRevealOutlineProvider(mPillRect,
-                iconCenterX - halfHeight, iconCenterX + halfHeight)
-                        .createRevealAnimator(this, true);
-    }
-
-    /**
      * Returns the position of the center of the icon relative to the container.
      */
     public Point getIconCenter() {
@@ -148,7 +169,7 @@
     }
 
     protected float getBackgroundRadius() {
-        return getResources().getDimensionPixelSize(R.dimen.bg_pill_radius);
+        return getResources().getDimensionPixelSize(R.dimen.bg_round_rect_radius);
     }
 
     /**
@@ -182,8 +203,10 @@
         public void setProgress(float progress) {
             super.setProgress(progress);
 
-            mZoomView.setScaleX(progress);
-            mZoomView.setScaleY(progress);
+            if (mZoomView != null) {
+                mZoomView.setScaleX(progress);
+                mZoomView.setScaleY(progress);
+            }
 
             float height = mOutline.height();
             mTranslateView.setTranslationY(mTranslateYMultiplier * (mFullHeight - height));
diff --git a/src/com/android/launcher3/popup/PopupPopulator.java b/src/com/android/launcher3/popup/PopupPopulator.java
index d2814ee..39c2db2 100644
--- a/src/com/android/launcher3/popup/PopupPopulator.java
+++ b/src/com/android/launcher3/popup/PopupPopulator.java
@@ -196,7 +196,8 @@
 
         @Override
         public void run() {
-            mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail, mContainer);
+            mShortcutChild.applyShortcutInfo(mShortcutChildInfo, mDetail,
+                    mContainer.mShortcutsItemView);
         }
     }
 
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index 2f07c9a..47a023e 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -17,26 +17,30 @@
 package com.android.launcher3.shortcuts;
 
 import android.content.Context;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
+import android.widget.FrameLayout;
 
 import com.android.launcher3.Launcher;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
-import com.android.launcher3.popup.PopupContainerWithArrow;
-import com.android.launcher3.popup.PopupItemView;
+import com.android.launcher3.Utilities;
 
 /**
  * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}.
  * This lets us animate the DeepShortcutView (icon and text) separately from the background.
  */
-public class DeepShortcutView extends PopupItemView {
+public class DeepShortcutView extends FrameLayout {
+
+    private static final Point sTempPoint = new Point();
 
     private final Rect mPillRect;
 
     private DeepShortcutTextView mBubbleText;
+    private View mIconView;
 
     private ShortcutInfo mInfo;
     private ShortcutInfoCompat mDetail;
@@ -59,6 +63,7 @@
     protected void onFinishInflate() {
         super.onFinishInflate();
         mBubbleText = (DeepShortcutTextView) findViewById(R.id.deep_shortcut);
+        mIconView = findViewById(R.id.icon);
     }
 
     public DeepShortcutTextView getBubbleText() {
@@ -73,6 +78,17 @@
         return mIconView.getVisibility() == View.VISIBLE;
     }
 
+    /**
+     * Returns the position of the center of the icon relative to the container.
+     */
+    public Point getIconCenter() {
+        sTempPoint.y = sTempPoint.x = getMeasuredHeight() / 2;
+        if (Utilities.isRtl(getResources())) {
+            sTempPoint.x = getMeasuredWidth() - sTempPoint.x;
+        }
+        return sTempPoint;
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@@ -81,7 +97,7 @@
 
     /** package private **/
     public void applyShortcutInfo(ShortcutInfo info, ShortcutInfoCompat detail,
-            PopupContainerWithArrow container) {
+            ShortcutsItemView container) {
         mInfo = info;
         mDetail = detail;
         mBubbleText.applyFromShortcutInfo(info);
diff --git a/src/com/android/launcher3/shortcuts/ShortcutsItemView.java b/src/com/android/launcher3/shortcuts/ShortcutsItemView.java
new file mode 100644
index 0000000..349c4c9
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/ShortcutsItemView.java
@@ -0,0 +1,180 @@
+/*
+ * 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.shortcuts;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.content.Context;
+import android.graphics.Point;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.ItemInfo;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.LauncherAnimUtils;
+import com.android.launcher3.R;
+import com.android.launcher3.anim.PropertyListBuilder;
+import com.android.launcher3.dragndrop.DragOptions;
+import com.android.launcher3.dragndrop.DragView;
+import com.android.launcher3.logging.UserEventDispatcher.LogContainerProvider;
+import com.android.launcher3.popup.PopupContainerWithArrow;
+import com.android.launcher3.popup.PopupItemView;
+import com.android.launcher3.userevent.nano.LauncherLogProto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A {@link PopupItemView} that contains all of the {@link DeepShortcutView}s for an app.
+ */
+public class ShortcutsItemView extends PopupItemView implements View.OnLongClickListener,
+        View.OnTouchListener, LogContainerProvider {
+
+    private Launcher mLauncher;
+    private LinearLayout mDeepShortcutsLayout;
+    private final Point mIconShift = new Point();
+    private final Point mIconLastTouchPos = new Point();
+
+    public ShortcutsItemView(Context context) {
+        this(context, null, 0);
+    }
+
+    public ShortcutsItemView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ShortcutsItemView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        mLauncher = Launcher.getLauncher(context);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mDeepShortcutsLayout = (LinearLayout) findViewById(R.id.deep_shortcuts);
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent ev) {
+        // Touched a shortcut, update where it was touched so we can drag from there on long click.
+        switch (ev.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_MOVE:
+                mIconLastTouchPos.set((int) ev.getX(), (int) ev.getY());
+                break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onLongClick(View v) {
+        // Return early if this is not initiated from a touch or not the correct view
+        if (!v.isInTouchMode() || !(v.getParent() instanceof DeepShortcutView)) return false;
+        // Return early if global dragging is not enabled
+        if (!mLauncher.isDraggingEnabled()) return false;
+        // Return early if an item is already being dragged (e.g. when long-pressing two shortcuts)
+        if (mLauncher.getDragController().isDragging()) return false;
+
+        // Long clicked on a shortcut.
+        DeepShortcutView sv = (DeepShortcutView) v.getParent();
+        sv.setWillDrawIcon(false);
+
+        // Move the icon to align with the center-top of the touch point
+        mIconShift.x = mIconLastTouchPos.x - sv.getIconCenter().x;
+        mIconShift.y = mIconLastTouchPos.y - mLauncher.getDeviceProfile().iconSizePx;
+
+        DragView dv = mLauncher.getWorkspace().beginDragShared(sv.getBubbleText(),
+                (PopupContainerWithArrow) getParent(), sv.getFinalInfo(),
+                new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift), new DragOptions());
+        dv.animateShift(-mIconShift.x, -mIconShift.y);
+
+        // TODO: support dragging from within folder without having to close it
+        AbstractFloatingView.closeOpenContainer(mLauncher, AbstractFloatingView.TYPE_FOLDER);
+        return false;
+    }
+
+    public void addDeepShortcutView(DeepShortcutView deepShortcutView) {
+        if (getNumDeepShortcuts() > 0) {
+            getDeepShortcutAt(getNumDeepShortcuts() - 1).findViewById(R.id.divider)
+                    .setVisibility(VISIBLE);
+        }
+        mDeepShortcutsLayout.addView(deepShortcutView);
+    }
+
+    private DeepShortcutView getDeepShortcutAt(int index) {
+        return (DeepShortcutView) mDeepShortcutsLayout.getChildAt(index);
+    }
+
+    private int getNumDeepShortcuts() {
+        return mDeepShortcutsLayout.getChildCount();
+    }
+
+    public List<DeepShortcutView> getDeepShortcutViews(boolean reverseOrder) {
+        int numDeepShortcuts = getNumDeepShortcuts();
+        List<DeepShortcutView> deepShortcutViews = new ArrayList<>(numDeepShortcuts);
+        for (int i = 0; i < numDeepShortcuts; i++) {
+            DeepShortcutView deepShortcut = getDeepShortcutAt(i);
+            if (reverseOrder) {
+                deepShortcutViews.add(0, deepShortcut);
+            } else {
+                deepShortcutViews.add(deepShortcut);
+            }
+        }
+        return deepShortcutViews;
+    }
+
+    @Override
+    public Animator createOpenAnimation(boolean isContainerAboveIcon, boolean pivotLeft) {
+        AnimatorSet openAnimation = LauncherAnimUtils.createAnimatorSet();
+        openAnimation.play(super.createOpenAnimation(isContainerAboveIcon, pivotLeft));
+        for (int i = 0; i < getNumDeepShortcuts(); i++) {
+            View deepShortcutIcon = getDeepShortcutAt(i).getIconView();
+            deepShortcutIcon.setScaleX(0);
+            deepShortcutIcon.setScaleY(0);
+            openAnimation.play(LauncherAnimUtils.ofPropertyValuesHolder(
+                    deepShortcutIcon, new PropertyListBuilder().scale(1).build()));
+        }
+        return openAnimation;
+    }
+
+    @Override
+    public Animator createCloseAnimation(boolean isContainerAboveIcon, boolean pivotLeft,
+            long duration) {
+        AnimatorSet closeAnimation = LauncherAnimUtils.createAnimatorSet();
+        closeAnimation.play(super.createCloseAnimation(isContainerAboveIcon, pivotLeft, duration));
+        for (int i = 0; i < getNumDeepShortcuts(); i++) {
+            View deepShortcutIcon = getDeepShortcutAt(i).getIconView();
+            deepShortcutIcon.setScaleX(1);
+            deepShortcutIcon.setScaleY(1);
+            closeAnimation.play(LauncherAnimUtils.ofPropertyValuesHolder(
+                    deepShortcutIcon, new PropertyListBuilder().scale(0).build()));
+        }
+        return closeAnimation;
+    }
+
+    @Override
+    public void fillInLogContainerData(View v, ItemInfo info, LauncherLogProto.Target target,
+            LauncherLogProto.Target targetParent) {
+        target.itemType = LauncherLogProto.ItemType.DEEPSHORTCUT;
+        target.rank = info.rank;
+        targetParent.containerType = LauncherLogProto.ContainerType.DEEPSHORTCUTS;
+    }
+}
diff --git a/src/com/android/launcher3/util/PillWidthRevealOutlineProvider.java b/src/com/android/launcher3/util/PillWidthRevealOutlineProvider.java
deleted file mode 100644
index 89dda3b..0000000
--- a/src/com/android/launcher3/util/PillWidthRevealOutlineProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2016 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.util;
-
-import android.graphics.Rect;
-
-/**
- * Extension of {@link PillRevealOutlineProvider} which only changes the width of the pill.
- */
-public class PillWidthRevealOutlineProvider extends PillRevealOutlineProvider {
-
-    private final int mStartLeft;
-    private final int mStartRight;
-
-    public PillWidthRevealOutlineProvider(Rect pillRect, int left, int right) {
-        super(0, 0, pillRect);
-        mOutline.set(pillRect);
-        mStartLeft = left;
-        mStartRight = right;
-    }
-
-    @Override
-    public void setProgress(float progress) {
-        mOutline.left = (int) (progress * mPillRect.left + (1 - progress) * mStartLeft);
-        mOutline.right = (int) (progress * mPillRect.right + (1 - progress) * mStartRight);
-    }
-}