Launcher shortcuts animations update.

> The shortcut container closes with an animation
> When opening/closing the animation only the icon scales
and not the title and drag handle
> When dragging the icon, it starts from the original icon position and
moves under the user finger. The container grows to follow the drag view.

Bug: 28980830
Change-Id: Ic0353c30b682d1f018cbf4d62e8a6e8e7d7d4664
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 3f12abf..d668d2a 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -1864,7 +1864,7 @@
             mWorkspace.exitWidgetResizeMode();
 
             closeFolder(alreadyOnHome);
-            closeShortcutsContainer();
+            closeShortcutsContainer(alreadyOnHome);
             exitSpringLoadedDragMode();
 
             // If we are already on home, then just animate back to the workspace,
@@ -1951,8 +1951,7 @@
         // this state is reflected.
         // TODO: Move folderInfo.isOpened out of the model and make it a UI state.
         closeFolder(false);
-
-        closeShortcutsContainer();
+        closeShortcutsContainer(false);
 
         if (mPendingAddInfo.container != ItemInfo.NO_ID && mPendingAddInfo.screenId > -1 &&
                 mWaitingForResult) {
@@ -3130,11 +3129,17 @@
     }
 
     public void closeShortcutsContainer() {
+        closeShortcutsContainer(true);
+    }
+
+    public void closeShortcutsContainer(boolean animate) {
         DeepShortcutsContainer deepShortcutsContainer = getOpenShortcutsContainer();
         if (deepShortcutsContainer != null) {
-            deepShortcutsContainer.cleanupDeferredDrag(true);
-            mDragController.removeDragListener(deepShortcutsContainer);
-            mDragLayer.removeView(deepShortcutsContainer);
+            if (animate) {
+                deepShortcutsContainer.animateClose();
+            } else {
+                deepShortcutsContainer.close();
+            }
         }
     }
 
@@ -3146,7 +3151,8 @@
         // and will be one of the last views.
         for (int i = mDragLayer.getChildCount() - 1; i >= 0; i--) {
             View child = mDragLayer.getChildAt(i);
-            if (child instanceof DeepShortcutsContainer) {
+            if (child instanceof DeepShortcutsContainer
+                    && ((DeepShortcutsContainer) child).isOpen()) {
                 return (DeepShortcutsContainer) child;
             }
         }
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 3c057e6..341c7c8 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -2312,12 +2312,13 @@
                     + "View: " + child + "  tag: " + child.getTag();
             throw new IllegalStateException(msg);
         }
-        beginDragShared(child, new Point(), source, accessible,
-                (ItemInfo) dragObject, new DragPreviewProvider(child));
+        beginDragShared(child, source, accessible, (ItemInfo) dragObject,
+                new DragPreviewProvider(child));
     }
 
-    public void beginDragShared(View child, Point relativeTouchPos, DragSource source,
-            boolean accessible, ItemInfo dragObject, DragPreviewProvider previewProvider) {
+
+    public DragView beginDragShared(View child, DragSource source, boolean accessible,
+            ItemInfo dragObject, DragPreviewProvider previewProvider) {
         child.clearFocus();
         child.setPressed(false);
 
@@ -2329,34 +2330,19 @@
         final Bitmap b = previewProvider.createDragBitmap(mCanvas);
         int halfPadding = previewProvider.previewPadding / 2;
 
-        final int bmpWidth = b.getWidth();
-        final int bmpHeight = b.getHeight();
-
-        float scale = mLauncher.getDragLayer().getLocationInDragLayer(child, mTempXY);
-        int dragLayerX = Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2);
-        int dragLayerY = Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 - halfPadding);
+        float scale = previewProvider.getScaleAndPosition(b, mTempXY);
+        int dragLayerX = mTempXY[0];
+        int dragLayerY = mTempXY[1];
 
         DeviceProfile grid = mLauncher.getDeviceProfile();
         Point dragVisualizeOffset = null;
         Rect dragRect = null;
         if (child instanceof BubbleTextView) {
-            BubbleTextView icon = (BubbleTextView) child;
             int iconSize = grid.iconSizePx;
             int top = child.getPaddingTop();
-            int left = (bmpWidth - iconSize) / 2;
+            int left = (b.getWidth() - iconSize) / 2;
             int right = left + iconSize;
             int bottom = top + iconSize;
-            if (icon.isLayoutHorizontal()) {
-                // If the layout is horizontal, then if we are just picking up the icon, then just
-                // use the child position since the icon is top-left aligned.  Otherwise, offset
-                // the drag layer position horizontally so that the icon is under the current
-                // touch position.
-                if (icon.getIcon().getBounds().contains(relativeTouchPos.x, relativeTouchPos.y)) {
-                    dragLayerX = Math.round(mTempXY[0]);
-                } else {
-                    dragLayerX = Math.round(mTempXY[0] + relativeTouchPos.x - (bmpWidth / 2));
-                }
-            }
             dragLayerY += top;
             // Note: The drag region is used to calculate drag layer offsets, but the
             // dragVisualizeOffset in addition to the dragRect (the size) to position the outline.
@@ -2388,6 +2374,7 @@
         if (!FeatureFlags.LAUNCHER3_LEGACY_WORKSPACE_DND) {
             mLauncher.enterSpringLoadedDragMode();
         }
+        return dv;
     }
 
     public boolean transitionStateShouldAllowDrop() {
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index e95f07b..8a2ae94 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -47,7 +47,8 @@
 import java.util.Arrays;
 
 public class DragView extends View {
-    public static int COLOR_CHANGE_DURATION = 120;
+    public static final int COLOR_CHANGE_DURATION = 120;
+    public static final int VIEW_ZOOM_DURATION = 150;
 
     @Thunk static float sDragAlpha = 1f;
 
@@ -73,6 +74,11 @@
     @Thunk float[] mCurrentFilter;
     private ValueAnimator mFilterAnimator;
 
+    private int mLastTouchX;
+    private int mLastTouchY;
+    private int mAnimatedShiftX;
+    private int mAnimatedShiftY;
+
     /**
      * Construct the drag view.
      * <p>
@@ -98,7 +104,7 @@
 
         // Animate the view into the correct position
         mAnim = LauncherAnimUtils.ofFloat(this, 0f, 1f);
-        mAnim.setDuration(150);
+        mAnim.setDuration(VIEW_ZOOM_DURATION);
         mAnim.addUpdateListener(new AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator animation) {
@@ -310,7 +316,6 @@
     /**
      * Create a window containing this view and show it.
      *
-     * @param windowToken obtained from v.getWindowToken() from one of your views
      * @param touchX the x coordinate the user touched in DragLayer coordinates
      * @param touchY the y coordinate the user touched in DragLayer coordinates
      */
@@ -323,8 +328,7 @@
         lp.height = mBitmap.getHeight();
         lp.customPosition = true;
         setLayoutParams(lp);
-        setTranslationX(touchX - mRegistrationX);
-        setTranslationY(touchY - mRegistrationY);
+        move(touchX, touchY);
         // Post the animation to skip other expensive work happening on the first frame
         post(new Runnable() {
             public void run() {
@@ -347,8 +351,32 @@
      * @param touchY the y coordinate the user touched in DragLayer coordinates
      */
     public void move(int touchX, int touchY) {
-        setTranslationX(touchX - mRegistrationX);
-        setTranslationY(touchY - mRegistrationY);
+        mLastTouchX = touchX;
+        mLastTouchY = touchY;
+        applyTranslation();
+    }
+
+    public void animateShift(final int shiftX, final int shiftY) {
+        if (mAnim.isStarted()) {
+            return;
+        }
+        mAnimatedShiftX = shiftX;
+        mAnimatedShiftY = shiftY;
+        applyTranslation();
+        mAnim.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                float fraction = 1 - animation.getAnimatedFraction();
+                mAnimatedShiftX = (int) (fraction * shiftX);
+                mAnimatedShiftY = (int) (fraction * shiftY);
+                applyTranslation();
+            }
+        });
+    }
+
+    private void applyTranslation() {
+        setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX);
+        setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY);
     }
 
     public void remove() {
diff --git a/src/com/android/launcher3/graphics/DragPreviewProvider.java b/src/com/android/launcher3/graphics/DragPreviewProvider.java
index a00bc92..b90c2fd 100644
--- a/src/com/android/launcher3/graphics/DragPreviewProvider.java
+++ b/src/com/android/launcher3/graphics/DragPreviewProvider.java
@@ -25,13 +25,12 @@
 import android.widget.TextView;
 
 import com.android.launcher3.HolographicOutlineHelper;
+import com.android.launcher3.Launcher;
 import com.android.launcher3.PreloadIconDrawable;
 import com.android.launcher3.R;
 import com.android.launcher3.Workspace;
 import com.android.launcher3.folder.FolderIcon;
 
-import java.util.concurrent.atomic.AtomicInteger;
-
 /**
  * A utility class to generate preview bitmap for dragging.
  */
@@ -59,9 +58,7 @@
     }
 
     /**
-     * Draw the View v into the given Canvas.
-     *
-     * @param destCanvas the canvas to draw on
+     * Draws the {@link #mView} into the given {@param destCanvas}.
      */
     private void drawDragView(Canvas destCanvas) {
         destCanvas.save();
@@ -98,7 +95,7 @@
     }
 
     /**
-     * Returns a new bitmap to show when the given View is being dragged around.
+     * Returns a new bitmap to show when the {@link #mView} is being dragged around.
      * Responsibility for the bitmap is transferred to the caller.
      */
     public Bitmap createDragBitmap(Canvas canvas) {
@@ -152,4 +149,12 @@
         }
         return bounds;
     }
+
+    public float getScaleAndPosition(Bitmap preview, int[] outPos) {
+        float scale = Launcher.getLauncher(mView.getContext())
+                .getDragLayer().getLocationInDragLayer(mView, outPos);
+        outPos[0] = Math.round(outPos[0] - (preview.getWidth() - scale * mView.getWidth()) / 2);
+        outPos[1] = Math.round(outPos[1] - (1 - scale) * preview.getHeight() / 2 - previewPadding / 2);
+        return scale;
+    }
 }
diff --git a/src/com/android/launcher3/graphics/ScaledPreviewProvider.java b/src/com/android/launcher3/graphics/ScaledPreviewProvider.java
deleted file mode 100644
index a7d121b..0000000
--- a/src/com/android/launcher3/graphics/ScaledPreviewProvider.java
+++ /dev/null
@@ -1,89 +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.graphics;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.view.View;
-import android.widget.TextView;
-
-import com.android.launcher3.HolographicOutlineHelper;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.R;
-import com.android.launcher3.Workspace;
-import com.android.launcher3.graphics.DragPreviewProvider;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Extension of {@link DragPreviewProvider} which generates bitmaps scaled to the default icon size
- */
-public class ScaledPreviewProvider extends DragPreviewProvider {
-
-    public ScaledPreviewProvider(View v) {
-        super(v);
-    }
-
-    @Override
-    public Bitmap createDragOutline(Canvas canvas) {
-        if (mView instanceof TextView) {
-            Bitmap b = drawScaledPreview(canvas);
-
-            final int outlineColor = mView.getResources().getColor(R.color.outline_color);
-            HolographicOutlineHelper.obtain(mView.getContext())
-                    .applyExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor);
-            canvas.setBitmap(null);
-            return b;
-        }
-        return super.createDragOutline(canvas);
-    }
-
-    @Override
-    public Bitmap createDragBitmap(Canvas canvas) {
-        if (mView instanceof TextView) {
-            Bitmap b = drawScaledPreview(canvas);
-            canvas.setBitmap(null);
-            return b;
-
-        } else {
-            return super.createDragBitmap(canvas);
-        }
-    }
-
-    private Bitmap drawScaledPreview(Canvas canvas) {
-        Drawable d = Workspace.getTextViewIcon((TextView) mView);
-        Rect bounds = getDrawableBounds(d);
-
-        int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
-
-        final Bitmap b = Bitmap.createBitmap(
-                size + DRAG_BITMAP_PADDING,
-                size + DRAG_BITMAP_PADDING,
-                Bitmap.Config.ARGB_8888);
-
-        canvas.setBitmap(b);
-        canvas.save(Canvas.MATRIX_SAVE_FLAG);
-        canvas.translate(DRAG_BITMAP_PADDING / 2, DRAG_BITMAP_PADDING / 2);
-        canvas.scale(((float) size) / bounds.width(), ((float) size) / bounds.height(), 0, 0);
-        canvas.translate(bounds.left, bounds.top);
-        d.draw(canvas);
-        canvas.restore();
-        return b;
-    }
-}
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
index c48d160..450d36d 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutTextView.java
@@ -21,7 +21,6 @@
 import android.util.AttributeSet;
 
 import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.R;
 
 /**
  * A {@link BubbleTextView} that has the shortcut icon on the left and drag handle on the right.
@@ -41,10 +40,7 @@
     }
 
     @Override
-    /** Use the BubbleTextView icon for the start and the drag handle for the end. */
     protected void applyCompoundDrawables(Drawable icon) {
-        Drawable dragHandle = getResources().getDrawable(R.drawable.deep_shortcuts_drag_handle);
-        dragHandle.setBounds(0, 0, dragHandle.getIntrinsicWidth(), dragHandle.getIntrinsicHeight());
-        setCompoundDrawablesRelative(icon, null, dragHandle, null);
+        // The icon is drawn in a separate view.
     }
 }
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutView.java b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
index 7cb2d43..b651f25 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutView.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutView.java
@@ -17,21 +17,20 @@
 package com.android.launcher3.shortcuts;
 
 import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
 import android.content.Context;
-import android.content.res.Resources;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.AttributeSet;
-import android.view.animation.DecelerateInterpolator;
+import android.view.View;
 import android.widget.FrameLayout;
 
-import com.android.launcher3.BubbleTextView;
-import com.android.launcher3.LauncherAnimUtils;
-import com.android.launcher3.LauncherViewPropertyAnimator;
+import com.android.launcher3.IconCache;
+import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.R;
+import com.android.launcher3.ShortcutInfo;
+import com.android.launcher3.Utilities;
 import com.android.launcher3.util.PillRevealOutlineProvider;
+import com.android.launcher3.util.PillWidthRevealOutlineProvider;
 
 /**
  * A {@link android.widget.FrameLayout} that contains a {@link DeepShortcutView}.
@@ -39,10 +38,12 @@
  */
 public class DeepShortcutView extends FrameLayout {
 
-    private int mRadius;
-    private Rect mPillRect;
+    private static final Point sTempPoint = new Point();
 
-    private BubbleTextView mBubbleText;
+    private final Rect mPillRect;
+
+    private DeepShortcutTextView mBubbleText;
+    private View mIconView;
 
     public DeepShortcutView(Context context) {
         this(context, null, 0);
@@ -55,70 +56,116 @@
     public DeepShortcutView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
 
-        mRadius = getResources().getDimensionPixelSize(R.dimen.bg_pill_radius);
         mPillRect = new Rect();
     }
 
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
-        mBubbleText = (BubbleTextView) findViewById(R.id.deep_shortcut);
+        mIconView = findViewById(R.id.deep_shortcut_icon);
+        mBubbleText = (DeepShortcutTextView) findViewById(R.id.deep_shortcut);
     }
 
-    public BubbleTextView getBubbleText() {
+    public DeepShortcutTextView getBubbleText() {
         return mBubbleText;
     }
 
+    public void setWillDrawIcon(boolean willDraw) {
+        mIconView.setVisibility(willDraw ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    public boolean willDrawIcon() {
+        return mIconView.getVisibility() == View.VISIBLE;
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         mPillRect.set(0, 0, getMeasuredWidth(), getMeasuredHeight());
     }
 
-    @Override
-    public void setPivotX(float pivotX) {
-        super.setPivotX(pivotX);
-        mBubbleText.setPivotX(pivotX);
+    public void applyShortcutInfo(ShortcutInfo info) {
+        IconCache cache = LauncherAppState.getInstance().getIconCache();
+        mBubbleText.applyFromShortcutInfo(info, cache);
+        mIconView.setBackground(mBubbleText.getIcon());
     }
 
-    @Override
-    public void setPivotY(float pivotY) {
-        super.setPivotY(pivotY);
-        mBubbleText.setPivotY(pivotY);
+    public View getIconView() {
+        return mIconView;
     }
 
     /**
-     * Creates an animator to play when the shortcut container is being opened.
+     * Creates an animator to play when the shortcut container is being opened or closed.
      */
-    public Animator createOpenAnimation(long animationDelay, boolean isContainerAboveIcon) {
-        final Resources res = getResources();
-        setVisibility(INVISIBLE);
+    public Animator createOpenCloseAnimation(
+            boolean isContainerAboveIcon, boolean pivotLeft, boolean isReverse) {
+        Point center = getIconCenter();
+        return new ZoomRevealOutlineProvider(center.x, center.y, mPillRect,
+                this, mIconView, isContainerAboveIcon, pivotLeft)
+                .createRevealAnimator(this, isReverse);
+    }
 
-        AnimatorSet openAnimation = LauncherAnimUtils.createAnimatorSet();
+    /**
+     * 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);
+    }
 
-        Animator reveal = new PillRevealOutlineProvider((int) getPivotX(), (int) getPivotY(),
-                mPillRect, mRadius).createRevealAnimator(this);
-        reveal.addListener(new AnimatorListenerAdapter() {
-            @Override
-            public void onAnimationStart(Animator animation) {
-                setVisibility(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;
+    }
 
-        float transY = res.getDimensionPixelSize(R.dimen.deep_shortcut_anim_translation_y);
-        Animator translationY = ObjectAnimator.ofFloat(this, TRANSLATION_Y,
-                isContainerAboveIcon ? transY : -transY, 0);
+    /**
+     * Extension of {@link PillRevealOutlineProvider} which scales the icon based on the height.
+     */
+    private static class ZoomRevealOutlineProvider extends PillRevealOutlineProvider {
 
-        // Only scale mBubbleText (the icon and text, not the background).
-        mBubbleText.setScaleX(0);
-        mBubbleText.setScaleY(0);
-        LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mBubbleText)
-                .scaleX(1).scaleY(1);
+        private final View mTranslateView;
+        private final View mZoomView;
 
-        openAnimation.playTogether(reveal, translationY, scale);
-        openAnimation.setStartDelay(animationDelay);
-        openAnimation.setDuration(res.getInteger(R.integer.config_deepShortcutOpenDuration));
-        openAnimation.setInterpolator(new DecelerateInterpolator());
-        return openAnimation;
+        private final float mFullHeight;
+        private final float mTranslateYMultiplier;
+
+        private final boolean mPivotLeft;
+        private final float mTranslateX;
+
+        public ZoomRevealOutlineProvider(int x, int y, Rect pillRect,
+                View translateView, View zoomView, boolean isContainerAboveIcon, boolean pivotLeft) {
+            super(x, y, pillRect);
+            mTranslateView = translateView;
+            mZoomView = zoomView;
+            mFullHeight = pillRect.height();
+
+            mTranslateYMultiplier = isContainerAboveIcon ? 0.5f : -0.5f;
+
+            mPivotLeft = pivotLeft;
+            mTranslateX = pivotLeft ? pillRect.height() / 2 : pillRect.right - pillRect.height() / 2;
+        }
+
+        @Override
+        public void setProgress(float progress) {
+            super.setProgress(progress);
+
+            mZoomView.setScaleX(progress);
+            mZoomView.setScaleY(progress);
+
+            float height = mOutline.height();
+            mTranslateView.setTranslationY(mTranslateYMultiplier * (mFullHeight - height));
+
+            float pivotX = mPivotLeft ? (mOutline.left + height / 2) : (mOutline.right - height / 2);
+            mTranslateView.setTranslationX(mTranslateX - pivotX);
+        }
     }
 }
diff --git a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
index a693f15..3a513f1 100644
--- a/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
+++ b/src/com/android/launcher3/shortcuts/DeepShortcutsContainer.java
@@ -17,6 +17,7 @@
 package com.android.launcher3.shortcuts;
 
 import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.annotation.TargetApi;
 import android.content.ComponentName;
@@ -39,6 +40,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
+import android.view.animation.DecelerateInterpolator;
 import android.widget.LinearLayout;
 
 import com.android.launcher3.BubbleTextView;
@@ -50,6 +52,7 @@
 import com.android.launcher3.LauncherAppState;
 import com.android.launcher3.LauncherModel;
 import com.android.launcher3.LauncherViewPropertyAnimator;
+import com.android.launcher3.LogAccelerateInterpolator;
 import com.android.launcher3.R;
 import com.android.launcher3.ShortcutInfo;
 import com.android.launcher3.Utilities;
@@ -59,7 +62,6 @@
 import com.android.launcher3.dragndrop.DragController;
 import com.android.launcher3.dragndrop.DragLayer;
 import com.android.launcher3.dragndrop.DragView;
-import com.android.launcher3.graphics.ScaledPreviewProvider;
 import com.android.launcher3.graphics.TriangleShape;
 import com.android.launcher3.logging.UserEventDispatcher;
 import com.android.launcher3.userevent.nano.LauncherLogProto;
@@ -78,6 +80,8 @@
         UserEventDispatcher.LaunchSourceProvider {
     private static final String TAG = "ShortcutsContainer";
 
+    private final Point mIconShift = new Point();
+
     private final Launcher mLauncher;
     private final DeepShortcutManager mDeepShortcutsManager;
     private final int mDragDeadzone;
@@ -96,8 +100,12 @@
     private boolean mIsLeftAligned;
     private boolean mIsAboveIcon;
     private View mArrow;
+
+    private Animator mOpenCloseAnimator;
+    private boolean mDeferContainerRemoval;
+    private boolean mIsOpen;
+
     private boolean mSrcIconDragStarted;
-    private LauncherViewPropertyAnimator mArrowHoverAnimator;
     private boolean mIsRtl;
     private int mArrowHorizontalOffset;
 
@@ -167,7 +175,6 @@
         final int arrowVerticalOffset = resources.getDimensionPixelSize(
                 R.dimen.deep_shortcuts_arrow_vertical_offset);
         mArrow = addArrowView(mArrowHorizontalOffset, arrowVerticalOffset, arrowWidth, arrowHeight);
-        mArrowHoverAnimator = new LauncherViewPropertyAnimator(mArrow);
 
         animateOpen();
 
@@ -218,9 +225,9 @@
 
         @Override
         public void run() {
+            DeepShortcutView shortcutViewContainer = getShortcutAt(mShortcutChildIndex);
+            shortcutViewContainer.applyShortcutInfo(mShortcutChildInfo);
             BubbleTextView shortcutView = getShortcutAt(mShortcutChildIndex).getBubbleText();
-            shortcutView.applyFromShortcutInfo(mShortcutChildInfo,
-                    LauncherAppState.getInstance().getIconCache());
             // Use the long label as long as it exists and fits.
             int availableWidth = shortcutView.getWidth() - shortcutView.getTotalPaddingLeft()
                     - shortcutView.getTotalPaddingRight();
@@ -248,30 +255,54 @@
 
     private void animateOpen() {
         setVisibility(View.VISIBLE);
+        mIsOpen = true;
 
         final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
         final int shortcutCount = getShortcutCount();
-        final int pivotX = mIsLeftAligned ? mArrowHorizontalOffset
-                : getMeasuredWidth() - mArrowHorizontalOffset;
-        final int pivotY = getShortcutAt(0).getMeasuredHeight() / 2;
+
+        final long duration = getResources().getInteger(
+                R.integer.config_deepShortcutOpenDuration);
+        final long stagger = getResources().getInteger(
+                R.integer.config_deepShortcutOpenStagger);
+
+        // Animate shortcuts
+        DecelerateInterpolator interpolator = new DecelerateInterpolator();
         for (int i = 0; i < shortcutCount; i++) {
-            DeepShortcutView deepShortcutView = getShortcutAt(i);
-            deepShortcutView.setPivotX(pivotX);
-            deepShortcutView.setPivotY(pivotY);
+            final DeepShortcutView deepShortcutView = getShortcutAt(i);
+            deepShortcutView.setVisibility(INVISIBLE);
+
+            Animator anim = deepShortcutView.createOpenCloseAnimation(
+                    mIsAboveIcon, mIsLeftAligned, false);
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationStart(Animator animation) {
+                    deepShortcutView.setVisibility(VISIBLE);
+                }
+            });
+            anim.setDuration(duration);
             int animationIndex = mIsAboveIcon ? shortcutCount - i - 1 : i;
-            long animationDelay = animationIndex * getResources().getInteger(
-                    R.integer.config_deepShortcutOpenStagger);
-            shortcutAnims.play(deepShortcutView.createOpenAnimation(animationDelay, mIsAboveIcon));
+            anim.setStartDelay(stagger * animationIndex);
+            anim.setInterpolator(interpolator);
+            shortcutAnims.play(anim);
         }
+        shortcutAnims.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mOpenCloseAnimator = null;
+            }
+        });
+
+        // Animate the arrow
         mArrow.setScaleX(0);
         mArrow.setScaleY(0);
-        final long shortcutAnimDuration = shortcutAnims.getChildAnimations().get(0).getDuration();
-        final long arrowScaleDelay = shortcutAnimDuration / 6;
-        final long arrowScaleDuration = shortcutAnimDuration - arrowScaleDelay;
+        final long arrowScaleDelay = duration / 6;
+        final long arrowScaleDuration = duration - arrowScaleDelay;
         Animator arrowScale = new LauncherViewPropertyAnimator(mArrow).scaleX(1).scaleY(1);
         arrowScale.setStartDelay(arrowScaleDelay);
         arrowScale.setDuration(arrowScaleDuration);
         shortcutAnims.play(arrowScale);
+
+        mOpenCloseAnimator = shortcutAnims;
         shortcutAnims.start();
     }
 
@@ -432,7 +463,6 @@
         Utilities.translateEventCoordinates(this, mLauncher.getDragLayer(), ev);
         final int dragLayerX = (int) ev.getX();
         final int dragLayerY = (int) ev.getY();
-        int shortcutCount = getShortcutCount();
         if (action == MotionEvent.ACTION_MOVE) {
             if (mLastX != 0 || mLastY != 0) {
                 mDistanceDragged += Math.hypot(mLastX - x, mLastY - y);
@@ -441,8 +471,6 @@
             mLastY = y;
 
             if (shouldStartDeferredDrag((int) x, (int) y)) {
-            DeepShortcutView topShortcut = getShortcutAt(0);
-            DeepShortcutView bottomShortcut = getShortcutAt(shortcutCount - 1);
                 mSrcIconDragStarted = true;
                 cleanupDeferredDrag(true);
                 mDeferredDragIcon.getParent().requestDisallowInterceptTouchEvent(false);
@@ -480,7 +508,7 @@
         return distFromTouchDown > mStartDragThreshold;
     }
 
-    public void cleanupDeferredDrag(boolean updateSrcVisibility) {
+    private void cleanupDeferredDrag(boolean updateSrcVisibility) {
         if (mDragView != null) {
             mDragView.remove();
         }
@@ -502,8 +530,8 @@
     }
 
     public boolean onLongClick(View v) {
-        // Return early if this is not initiated from a touch
-        if (!v.isInTouchMode()) return false;
+        // 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 if global dragging is not enabled
         if (!mLauncher.isDraggingEnabled()) return false;
 
@@ -514,8 +542,20 @@
         mLauncher.getModel().updateShortcutInfo(unbadgedInfo.mDetail, badged);
 
         // Long clicked on a shortcut.
-        mLauncher.getWorkspace().beginDragShared(v, mIconLastTouchPos, this, false, badged,
-                new ScaledPreviewProvider(v));
+
+        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, false, badged,
+                new ShortcutDragPreviewProvider(sv.getIconView(), mIconShift));
+        dv.animateShift(-mIconShift.x, -mIconShift.y);
+
         // TODO: support dragging from within folder without having to close it
         mLauncher.closeFolder();
         return false;
@@ -560,13 +600,25 @@
     public void onDragStart(DragSource source, ItemInfo info, int dragAction) {
         // 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.
-        setVisibility(INVISIBLE);
+        animateClose();
     }
 
     @Override
     public void onDragEnd() {
-        // Now remove the container.
-        mLauncher.closeShortcutsContainer();
+        if (mIsOpen) {
+            animateClose();
+        } else {
+            if (mOpenCloseAnimator != null) {
+                // Close animation is running.
+                mDeferContainerRemoval = false;
+            } else {
+                // Close animation is not running.
+                if (mDeferContainerRemoval) {
+                    mDeferContainerRemoval = false;
+                    mLauncher.getDragLayer().removeView(this);
+                }
+            }
+        }
     }
 
     @Override
@@ -576,6 +628,105 @@
         targetParent.containerType = LauncherLogProto.DEEPSHORTCUTS;
     }
 
+    public void animateClose() {
+        if (!mIsOpen) {
+            return;
+        }
+        if (mOpenCloseAnimator != null) {
+            mOpenCloseAnimator.cancel();
+        }
+        mIsOpen = false;
+        mLauncher.getDragController().removeDragListener(this);
+
+        final AnimatorSet shortcutAnims = LauncherAnimUtils.createAnimatorSet();
+        final int numShortcuts = getShortcutCount();
+        final long duration = getResources().getInteger(
+                R.integer.config_deepShortcutCloseDuration);
+        final long stagger = getResources().getInteger(
+                R.integer.config_deepShortcutCloseStagger);
+
+        long arrowDelay = (numShortcuts - 1) * stagger + (duration * 4 / 6);
+        int firstShortcutIndex = mIsAboveIcon ? (numShortcuts - 1) : 0;
+        LogAccelerateInterpolator interpolator = new LogAccelerateInterpolator(100, 0);
+        for (int i = 0; i < numShortcuts; i++) {
+            final DeepShortcutView view = getShortcutAt(i);
+            Animator anim;
+            if (view.willDrawIcon()) {
+                anim = view.createOpenCloseAnimation(mIsAboveIcon, mIsLeftAligned, true);
+                int animationIndex = mIsAboveIcon ? i : numShortcuts - i - 1;
+                anim.setStartDelay(stagger * animationIndex);
+                anim.setDuration(duration);
+                anim.setInterpolator(interpolator);
+            } 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();
+                LauncherViewPropertyAnimator anim2 = new LauncherViewPropertyAnimator(view)
+                        .scaleX(scale)
+                        .scaleY(scale)
+                        .translationX(mIconShift.x)
+                        .translationY(mIconShift.y);
+                anim2.setDuration(DragView.VIEW_ZOOM_DURATION);
+                shortcutAnims.play(anim2);
+
+                if (i == firstShortcutIndex) {
+                    arrowDelay = 0;
+                }
+            }
+            anim.addListener(new AnimatorListenerAdapter() {
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    view.setVisibility(INVISIBLE);
+                }
+            });
+            shortcutAnims.play(anim);
+        }
+        Animator arrowAnim = new LauncherViewPropertyAnimator(mArrow)
+                .scaleX(0).scaleY(0).setDuration(duration / 6);
+        arrowAnim.setStartDelay(arrowDelay);
+        shortcutAnims.play(arrowAnim);
+
+        shortcutAnims.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mOpenCloseAnimator = null;
+                if (mDeferContainerRemoval) {
+                    setVisibility(INVISIBLE);
+                } else {
+                    close();
+                }
+            }
+        });
+        mOpenCloseAnimator = shortcutAnims;
+        shortcutAnims.start();
+    }
+
+    /**
+     * Closes the folder without animation.
+     */
+    public void close() {
+        if (mOpenCloseAnimator != null) {
+            mOpenCloseAnimator.cancel();
+            mOpenCloseAnimator = null;
+        }
+        mIsOpen = false;
+        mDeferContainerRemoval = false;
+        cleanupDeferredDrag(true);
+        mLauncher.getDragController().removeDragListener(this);
+        mLauncher.getDragLayer().removeView(this);
+    }
+
+    public boolean isOpen() {
+        return mIsOpen;
+    }
+
     /**
      * Shows the shortcuts container for {@param icon}
      * @return the container if shown or null.
diff --git a/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
new file mode 100644
index 0000000..a25e475
--- /dev/null
+++ b/src/com/android/launcher3/shortcuts/ShortcutDragPreviewProvider.java
@@ -0,0 +1,103 @@
+/*
+ * 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.shortcuts;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.HolographicOutlineHelper;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.graphics.DragPreviewProvider;
+
+/**
+ * Extension of {@link DragPreviewProvider} which generates bitmaps scaled to the default icon size.
+ */
+public class ShortcutDragPreviewProvider extends DragPreviewProvider {
+
+    private final Point mPositionShift;
+
+    public ShortcutDragPreviewProvider(View icon, Point shift) {
+        super(icon);
+        mPositionShift = shift;
+    }
+
+    @Override
+    public Bitmap createDragOutline(Canvas canvas) {
+        Bitmap b = drawScaledPreview(canvas);
+
+        final int outlineColor = mView.getResources().getColor(R.color.outline_color);
+        HolographicOutlineHelper.obtain(mView.getContext())
+                .applyExpensiveOutlineWithBlur(b, canvas, outlineColor, outlineColor);
+        canvas.setBitmap(null);
+        return b;
+    }
+
+    @Override
+    public Bitmap createDragBitmap(Canvas canvas) {
+        Bitmap b = drawScaledPreview(canvas);
+        canvas.setBitmap(null);
+        return b;
+    }
+
+    private Bitmap drawScaledPreview(Canvas canvas) {
+        Drawable d = mView.getBackground();
+        Rect bounds = getDrawableBounds(d);
+
+        int size = Launcher.getLauncher(mView.getContext()).getDeviceProfile().iconSizePx;
+
+        final Bitmap b = Bitmap.createBitmap(
+                size + DRAG_BITMAP_PADDING,
+                size + DRAG_BITMAP_PADDING,
+                Bitmap.Config.ARGB_8888);
+
+        canvas.setBitmap(b);
+        canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        canvas.translate(DRAG_BITMAP_PADDING / 2, DRAG_BITMAP_PADDING / 2);
+        canvas.scale(((float) size) / bounds.width(), ((float) size) / bounds.height(), 0, 0);
+        canvas.translate(bounds.left, bounds.top);
+        d.draw(canvas);
+        canvas.restore();
+        return b;
+    }
+
+    @Override
+    public float getScaleAndPosition(Bitmap preview, int[] outPos) {
+        Launcher launcher = Launcher.getLauncher(mView.getContext());
+        int iconSize = getDrawableBounds(mView.getBackground()).width();
+        float scale = launcher.getDragLayer().getLocationInDragLayer(mView, outPos);
+
+        int iconLeft = mView.getPaddingStart();
+        if (Utilities.isRtl(mView.getResources())) {
+            iconLeft = mView.getWidth() - iconSize - iconLeft;
+        }
+
+        outPos[0] += Math.round(scale * iconLeft + (scale * iconSize - preview.getWidth()) / 2 +
+                mPositionShift.x);
+        outPos[1] += Math.round((scale * mView.getHeight() - preview.getHeight()) / 2
+                + mPositionShift.y);
+        float size = launcher.getDeviceProfile().iconSizePx;
+        return scale * iconSize / size;
+    }
+}
diff --git a/src/com/android/launcher3/util/PillRevealOutlineProvider.java b/src/com/android/launcher3/util/PillRevealOutlineProvider.java
index 09ff9bd..3f1e11a 100644
--- a/src/com/android/launcher3/util/PillRevealOutlineProvider.java
+++ b/src/com/android/launcher3/util/PillRevealOutlineProvider.java
@@ -31,19 +31,18 @@
 
     private int mCenterX;
     private int mCenterY;
-    private Rect mPillRect;
+    protected Rect mPillRect;
 
     /**
      * @param x reveal center x
      * @param y reveal center y
      * @param pillRect round rect that represents the final pill shape
-     * @param pillRectRadius radius of the round rect
      */
-    public PillRevealOutlineProvider(int x, int y, Rect pillRect, float pillRectRadius) {
+    public PillRevealOutlineProvider(int x, int y, Rect pillRect) {
         mCenterX = x;
         mCenterY = y;
         mPillRect = pillRect;
-        mOutlineRadius = pillRectRadius;
+        mOutlineRadius = pillRect.height() / 2f;
     }
 
     @Override
@@ -62,5 +61,6 @@
         mOutline.top = Math.max(mPillRect.top, mCenterY - currentSize);
         mOutline.right = Math.min(mPillRect.right, mCenterX + currentSize);
         mOutline.bottom = Math.min(mPillRect.bottom, mCenterY + currentSize);
+        mOutlineRadius = mOutline.height() / 2;
     }
 }
diff --git a/src/com/android/launcher3/util/PillWidthRevealOutlineProvider.java b/src/com/android/launcher3/util/PillWidthRevealOutlineProvider.java
new file mode 100644
index 0000000..89dda3b
--- /dev/null
+++ b/src/com/android/launcher3/util/PillWidthRevealOutlineProvider.java
@@ -0,0 +1,41 @@
+/*
+ * 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);
+    }
+}
diff --git a/src/com/android/launcher3/util/RevealOutlineAnimation.java b/src/com/android/launcher3/util/RevealOutlineAnimation.java
index 4447c3b..cd98882 100644
--- a/src/com/android/launcher3/util/RevealOutlineAnimation.java
+++ b/src/com/android/launcher3/util/RevealOutlineAnimation.java
@@ -29,7 +29,12 @@
     abstract void setProgress(float progress);
 
     public ValueAnimator createRevealAnimator(final View revealView) {
-        ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
+        return createRevealAnimator(revealView, false);
+    }
+
+    public ValueAnimator createRevealAnimator(final View revealView, boolean isReversed) {
+        ValueAnimator va =
+                isReversed ? ValueAnimator.ofFloat(1f, 0f) : ValueAnimator.ofFloat(0f, 1f);
         final float elevation = revealView.getElevation();
 
         va.addListener(new AnimatorListenerAdapter() {
@@ -54,7 +59,7 @@
         va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
             @Override
             public void onAnimationUpdate(ValueAnimator arg0) {
-                float progress = arg0.getAnimatedFraction();
+                float progress = (Float) arg0.getAnimatedValue();
                 setProgress(progress);
                 revealView.invalidateOutline();
                 if (!Utilities.ATLEAST_LOLLIPOP_MR1) {