Adding fling-to-delete.

- Also fixing issue where the drop target icon changes color slower than the text.

Change-Id: I0bfa59da5d202016342f1c3de419ebcafd81ff6f
diff --git a/src/com/android/launcher2/AppsCustomizePagedView.java b/src/com/android/launcher2/AppsCustomizePagedView.java
index 3fcff72..a7ff309 100644
--- a/src/com/android/launcher2/AppsCustomizePagedView.java
+++ b/src/com/android/launcher2/AppsCustomizePagedView.java
@@ -760,6 +760,10 @@
         mDraggingWidget = false;
     }
 
+    public boolean supportsFlingToDelete() {
+        return false;
+    }
+
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
diff --git a/src/com/android/launcher2/ButtonDropTarget.java b/src/com/android/launcher2/ButtonDropTarget.java
index dc34d88..e9f8ce8 100644
--- a/src/com/android/launcher2/ButtonDropTarget.java
+++ b/src/com/android/launcher2/ButtonDropTarget.java
@@ -18,7 +18,7 @@
 
 import android.content.Context;
 import android.content.res.Resources;
-import android.graphics.Paint;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.widget.TextView;
@@ -71,6 +71,10 @@
     public void onDrop(DragObject d) {
     }
 
+    public void onFlingToDelete(DragObject d, int x, int y, PointF vec) {
+        // Do nothing
+    }
+
     public void onDragEnter(DragObject d) {
         d.dragView.setColor(mHoverColor);
     }
diff --git a/src/com/android/launcher2/DeleteDropTarget.java b/src/com/android/launcher2/DeleteDropTarget.java
index 4621dea..6f45590 100644
--- a/src/com/android/launcher2/DeleteDropTarget.java
+++ b/src/com/android/launcher2/DeleteDropTarget.java
@@ -16,24 +16,33 @@
 
 package com.android.launcher2;
 
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.TransitionDrawable;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewConfiguration;
 import android.view.ViewGroup;
-import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AnimationUtils;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.LinearInterpolator;
 
 import com.android.launcher.R;
 
 public class DeleteDropTarget extends ButtonDropTarget {
+    private static int DELETE_ANIMATION_DURATION = 285;
+    private static int MODE_FLING_DELETE_TO_TRASH = 0;
+    private static int MODE_FLING_DELETE_ALONG_VECTOR = 1;
 
-    private static int DELETE_ANIMATION_DURATION = 300;
+    private final int mFlingDeleteMode = MODE_FLING_DELETE_ALONG_VECTOR;
+
     private ColorStateList mOriginalTextColor;
     private TransitionDrawable mUninstallDrawable;
     private TransitionDrawable mRemoveDrawable;
@@ -223,4 +232,175 @@
     public void onDrop(DragObject d) {
         animateToTrashAndCompleteDrop(d);
     }
+
+    /**
+     * Creates an animation from the current drag view to the delete trash icon.
+     */
+    private AnimatorUpdateListener createFlingToTrashAnimatorListener(final DragLayer dragLayer,
+            DragObject d, PointF vel, ViewConfiguration config) {
+        final Rect to = getIconRect(d.dragView.getMeasuredWidth(), d.dragView.getMeasuredHeight(),
+                mCurrentDrawable.getIntrinsicWidth(), mCurrentDrawable.getIntrinsicHeight());
+        final Rect from = new Rect();
+        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
+
+        // Calculate how far along the velocity vector we should put the intermediate point on
+        // the bezier curve
+        float velocity = Math.abs(vel.length());
+        float vp = Math.min(1f, velocity / (config.getScaledMaximumFlingVelocity() / 2f));
+        int offsetY = (int) (-from.top * vp);
+        int offsetX = (int) (offsetY / (vel.y / vel.x));
+        final float y2 = from.top + offsetY;                        // intermediate t/l
+        final float x2 = from.left + offsetX;
+        final float x1 = from.left;                                 // drag view t/l
+        final float y1 = from.top;
+        final float x3 = to.left;                                   // delete target t/l
+        final float y3 = to.top;
+
+        final TimeInterpolator scaleAlphaInterpolator = new TimeInterpolator() {
+            @Override
+            public float getInterpolation(float t) {
+                return t * t * t * t * t * t * t * t;
+            }
+        };
+        return new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                final DragView dragView = (DragView) dragLayer.getAnimatedView();
+                float t = ((Float) animation.getAnimatedValue()).floatValue();
+                float tp = scaleAlphaInterpolator.getInterpolation(t);
+                float initialScale = dragView.getInitialScale();
+                float finalAlpha = 0.5f;
+                float scale = dragView.getScaleX();
+                float x1o = ((1f - scale) * dragView.getMeasuredWidth()) / 2f;
+                float y1o = ((1f - scale) * dragView.getMeasuredHeight()) / 2f;
+                float x = (1f - t) * (1f - t) * (x1 - x1o) + 2 * (1f - t) * t * (x2 - x1o) +
+                        (t * t) * x3;
+                float y = (1f - t) * (1f - t) * (y1 - y1o) + 2 * (1f - t) * t * (y2 - x1o) +
+                        (t * t) * y3;
+
+                dragView.setTranslationX(x);
+                dragView.setTranslationY(y);
+                dragView.setScaleX(initialScale * (1f - tp));
+                dragView.setScaleY(initialScale * (1f - tp));
+                dragView.setAlpha(finalAlpha + (1f - finalAlpha) * (1f - tp));
+            }
+        };
+    }
+
+    /**
+     * Creates an animation from the current drag view along its current velocity vector.
+     * For this animation, the alpha runs for a fixed duration and we update the position
+     * progressively.
+     */
+    private static class FlingAlongVectorAnimatorUpdateListener implements AnimatorUpdateListener {
+        private static float FRICTION = 0.93f;
+
+        private DragLayer mDragLayer;
+        private PointF mVelocity;
+        private Rect mFrom;
+        private long mPrevTime;
+        private boolean mHasOffsetForScale;
+
+        private final TimeInterpolator mAlphaInterpolator = new DecelerateInterpolator(1.5f);
+
+        public FlingAlongVectorAnimatorUpdateListener(DragLayer dragLayer, PointF vel, Rect from,
+                long startTime) {
+            mDragLayer = dragLayer;
+            mVelocity = vel;
+            mFrom = from;
+            mPrevTime = startTime;
+        }
+
+        @Override
+        public void onAnimationUpdate(ValueAnimator animation) {
+            final DragView dragView = (DragView) mDragLayer.getAnimatedView();
+            float t = ((Float) animation.getAnimatedValue()).floatValue();
+            long curTime = AnimationUtils.currentAnimationTimeMillis();
+
+            if (!mHasOffsetForScale) {
+                mHasOffsetForScale = true;
+                float scale = dragView.getScaleX();
+                float xOffset = ((scale - 1f) * dragView.getMeasuredWidth()) / 2f;
+                float yOffset = ((scale - 1f) * dragView.getMeasuredHeight()) / 2f;
+
+                mFrom.left += xOffset;
+                mFrom.top += yOffset;
+            }
+
+            mFrom.left += (mVelocity.x * (curTime - mPrevTime) / 1000f);
+            mFrom.top += (mVelocity.y * (curTime - mPrevTime) / 1000f);
+
+            dragView.setTranslationX(mFrom.left);
+            dragView.setTranslationY(mFrom.top);
+            dragView.setAlpha(1f - mAlphaInterpolator.getInterpolation(t));
+
+            mVelocity.x *= FRICTION;
+            mVelocity.y *= FRICTION;
+            mPrevTime = curTime;
+        }
+    };
+    private AnimatorUpdateListener createFlingAlongVectorAnimatorListener(final DragLayer dragLayer,
+            DragObject d, PointF vel, final long startTime, final int duration,
+            ViewConfiguration config) {
+        final Rect from = new Rect();
+        dragLayer.getViewRectRelativeToSelf(d.dragView, from);
+
+        return new FlingAlongVectorAnimatorUpdateListener(dragLayer, vel, from, startTime);
+    }
+
+    public void onFlingToDelete(final DragObject d, int x, int y, PointF vel) {
+        // Don't highlight the icon as it's animating
+        d.dragView.setColor(0);
+        d.dragView.updateInitialScaleToCurrentScale();
+
+        if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
+            // Defer animating out the drop target if we are animating to it
+            mSearchDropTargetBar.deferOnDragEnd();
+            mSearchDropTargetBar.finishAnimations();
+        }
+
+        final ViewConfiguration config = ViewConfiguration.get(mLauncher);
+        final DragLayer dragLayer = mLauncher.getDragLayer();
+        final int duration = DELETE_ANIMATION_DURATION;
+        final long startTime = AnimationUtils.currentAnimationTimeMillis();
+
+        // NOTE: Because it takes time for the first frame of animation to actually be
+        // called and we expect the animation to be a continuation of the fling, we have
+        // to account for the time that has elapsed since the fling finished.  And since
+        // we don't have a startDelay, we will always get call to update when we call
+        // start() (which we want to ignore).
+        final TimeInterpolator tInterpolator = new TimeInterpolator() {
+            private int mCount = -1;
+            private float mOffset = 0f;
+
+            @Override
+            public float getInterpolation(float t) {
+                if (mCount < 0) {
+                    mCount++;
+                } else if (mCount == 0) {
+                    mOffset = Math.min(0.5f, (float) (AnimationUtils.currentAnimationTimeMillis() -
+                            startTime) / duration);
+                    mCount++;
+                }
+                return Math.min(1f, mOffset + t);
+            }
+        };
+        AnimatorUpdateListener updateCb = null;
+        if (mFlingDeleteMode == MODE_FLING_DELETE_TO_TRASH) {
+            updateCb = createFlingToTrashAnimatorListener(dragLayer, d, vel, config);
+        } else if (mFlingDeleteMode == MODE_FLING_DELETE_ALONG_VECTOR) {
+            updateCb = createFlingAlongVectorAnimatorListener(dragLayer, d, vel, startTime,
+                    duration, config);
+        }
+        Runnable onAnimationEndRunnable = new Runnable() {
+            @Override
+            public void run() {
+                mSearchDropTargetBar.onDragEnd();
+                mLauncher.exitSpringLoadedDragMode();
+                completeDrop(d);
+            }
+        };
+        dragLayer.animateView(d.dragView, updateCb, duration, tInterpolator, onAnimationEndRunnable,
+                DragLayer.ANIMATION_END_DISAPPEAR, null);
+    }
 }
diff --git a/src/com/android/launcher2/DragController.java b/src/com/android/launcher2/DragController.java
index 019a749..2a88925 100644
--- a/src/com/android/launcher2/DragController.java
+++ b/src/com/android/launcher2/DragController.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.IBinder;
@@ -26,6 +27,7 @@
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
+import android.view.VelocityTracker;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.inputmethod.InputMethodManager;
@@ -60,6 +62,9 @@
     static final int SCROLL_LEFT = 0;
     static final int SCROLL_RIGHT = 1;
 
+    private static final float MAX_FLING_DEGREES = 35f;
+    private static final int FLING_TO_DELETE_THRESHOLD_Y_VELOCITY = -1400;
+
     private Launcher mLauncher;
     private Handler mHandler;
     private final Vibrator mVibrator = new Vibrator();
@@ -86,8 +91,8 @@
 
     /** Who can receive drop events */
     private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>();
-
     private ArrayList<DragListener> mListeners = new ArrayList<DragListener>();
+    private DropTarget mFlingToDeleteDropTarget;
 
     /** The window token used as the parent for the DragView. */
     private IBinder mWindowToken;
@@ -111,6 +116,9 @@
     private int mTmpPoint[] = new int[2];
     private Rect mDragLayerRect = new Rect();
 
+    protected int mFlingToDeleteThresholdVelocity;
+    private VelocityTracker mVelocityTracker;
+
     /**
      * Interface to receive notifications when a drag starts or stops
      */
@@ -141,6 +149,10 @@
         mLauncher = launcher;
         mHandler = new Handler();
         mScrollZone = launcher.getResources().getDimensionPixelSize(R.dimen.scroll_zone);
+        mVelocityTracker = VelocityTracker.obtain();
+
+        float density = launcher.getResources().getDisplayMetrics().density;
+        mFlingToDeleteThresholdVelocity = (int) (FLING_TO_DELETE_THRESHOLD_Y_VELOCITY * density);
     }
 
     public boolean dragging() {
@@ -378,15 +390,35 @@
         if (mDragging) {
             mDragging = false;
             clearScrollRunnable();
-            for (DragListener listener : mListeners) {
-                listener.onDragEnd();
-            }
+            boolean isDeferred = false;
             if (mDragObject.dragView != null) {
-                if (!mDragObject.deferDragViewCleanupPostAnimation) {
+                isDeferred = mDragObject.deferDragViewCleanupPostAnimation;
+                if (!isDeferred) {
                     mDragObject.dragView.remove();
                 }
                 mDragObject.dragView = null;
             }
+
+            // Only end the drag if we are not deferred
+            if (!isDeferred) {
+                for (DragListener listener : mListeners) {
+                    listener.onDragEnd();
+                }
+            }
+        }
+
+        releaseVelocityTracker();
+    }
+
+    /**
+     * This only gets called as a result of drag view cleanup being deferred in endDrag();
+     */
+    void onDeferredEndDrag(DragView dragView) {
+        dragView.remove();
+
+        // If we skipped calling onDragEnd() before, do it now
+        for (DragListener listener : mListeners) {
+            listener.onDragEnd();
         }
     }
 
@@ -408,8 +440,11 @@
             Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging="
                     + mDragging);
         }
-        final int action = ev.getAction();
 
+        // Update the velocity tracker
+        acquireVelocityTrackerAndAddMovement(ev);
+
+        final int action = ev.getAction();
         final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
         final int dragLayerX = dragLayerPos[0];
         final int dragLayerY = dragLayerPos[1];
@@ -425,7 +460,12 @@
                 break;
             case MotionEvent.ACTION_UP:
                 if (mDragging) {
-                    drop(dragLayerX, dragLayerY);
+                    PointF vec = isFlingingToDelete(mDragObject.dragSource);
+                    if (vec != null) {
+                        dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
+                    } else {
+                        drop(dragLayerX, dragLayerY);
+                    }
                 }
                 endDrag();
                 break;
@@ -529,6 +569,9 @@
             return false;
         }
 
+        // Update the velocity tracker
+        acquireVelocityTrackerAndAddMovement(ev);
+
         final int action = ev.getAction();
         final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
         final int dragLayerX = dragLayerPos[0];
@@ -553,10 +596,15 @@
         case MotionEvent.ACTION_UP:
             // Ensure that we've processed a move event at the current pointer location.
             handleMoveEvent(dragLayerX, dragLayerY);
-
             mHandler.removeCallbacks(mScrollRunnable);
+
             if (mDragging) {
-                drop(dragLayerX, dragLayerY);
+                PointF vec = isFlingingToDelete(mDragObject.dragSource);
+                if (vec != null) {
+                    dropOnFlingToDeleteTarget(dragLayerX, dragLayerY, vec);
+                } else {
+                    drop(dragLayerX, dragLayerY);
+                }
             }
             endDrag();
             break;
@@ -569,6 +617,58 @@
         return true;
     }
 
+    /**
+     * Determines whether the user flung the current item to delete it.
+     *
+     * @return the vector at which the item was flung, or null if no fling was detected.
+     */
+    private PointF isFlingingToDelete(DragSource source) {
+        if (mFlingToDeleteDropTarget == null) return null;
+        if (!source.supportsFlingToDelete()) return null;
+
+        ViewConfiguration config = ViewConfiguration.get(mLauncher);
+        mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity());
+
+        if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) {
+            // Do a quick dot product test to ensure that we are flinging upwards
+            PointF vel = new PointF(mVelocityTracker.getXVelocity(),
+                    mVelocityTracker.getYVelocity());
+            PointF upVec = new PointF(0f, -1f);
+            float theta = (float) Math.acos(((vel.x * upVec.x) + (vel.y * upVec.y)) /
+                    (vel.length() * upVec.length()));
+            if (theta <= Math.toRadians(MAX_FLING_DEGREES)) {
+                return vel;
+            }
+        }
+        return null;
+    }
+
+    private void dropOnFlingToDeleteTarget(float x, float y, PointF vel) {
+        final int[] coordinates = mCoordinatesTemp;
+
+        mDragObject.x = coordinates[0];
+        mDragObject.y = coordinates[1];
+        mDragObject.dragComplete = true;
+
+        // Clean up dragging on the target if it's not the current fling delete target otherwise,
+        // start dragging to it.
+        if (mLastDropTarget != null && mFlingToDeleteDropTarget != mLastDropTarget) {
+            mLastDropTarget.onDragExit(mDragObject);
+        }
+
+        // Drop onto the fling-to-delete target
+        boolean accepted = false;
+        mFlingToDeleteDropTarget.onDragEnter(mDragObject);
+        mFlingToDeleteDropTarget.onDragExit(mDragObject);
+        if (mFlingToDeleteDropTarget.acceptDrop(mDragObject)) {
+            mFlingToDeleteDropTarget.onFlingToDelete(mDragObject, mDragObject.x, mDragObject.y,
+                    vel);
+            accepted = true;
+        }
+        mDragObject.dragSource.onDropCompleted((View) mFlingToDeleteDropTarget, mDragObject,
+                accepted);
+    }
+
     private void drop(float x, float y) {
         final int[] coordinates = mCoordinatesTemp;
         final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
@@ -659,6 +759,27 @@
     }
 
     /**
+     * Sets the current fling-to-delete drop target.
+     */
+    public void setFlingToDeleteDropTarget(DropTarget target) {
+        mFlingToDeleteDropTarget = target;
+    }
+
+    private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+    }
+
+    private void releaseVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    /**
      * Set which view scrolls for touch events near the edge of the screen.
      */
     public void setScrollView(View v) {
diff --git a/src/com/android/launcher2/DragLayer.java b/src/com/android/launcher2/DragLayer.java
index ce5c8c5..4754235 100644
--- a/src/com/android/launcher2/DragLayer.java
+++ b/src/com/android/launcher2/DragLayer.java
@@ -18,15 +18,15 @@
 
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
-import android.animation.ObjectAnimator;
 import android.animation.TimeInterpolator;
 import android.animation.ValueAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PointF;
 import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
@@ -34,6 +34,7 @@
 import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
+import android.view.animation.AnimationUtils;
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.widget.FrameLayout;
@@ -542,35 +543,17 @@
             duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration));
         }
 
-        if (mDropAnim != null) {
-            mDropAnim.cancel();
-        }
-
-        if (mFadeOutAnim != null) {
-            mFadeOutAnim.cancel();
-        }
-
-        // Show the drop view if it was previously hidden
-        mDropView = view;
-        mDropView.cancelAnimation();
-        mDropView.resetLayoutParams();
-        mDropAnim = new ValueAnimator();
+        // Fall back to cubic ease out interpolator for the animation if none is specified
+        TimeInterpolator interpolator = null;
         if (alphaInterpolator == null || motionInterpolator == null) {
-            mDropAnim.setInterpolator(mCubicEaseOutInterpolator);
+            interpolator = mCubicEaseOutInterpolator;
         }
 
-        if (anchorView != null) {
-            mAnchorViewInitialScrollX = anchorView.getScrollX();
-        }
-        mAnchorView = anchorView;
-
+        // Animate the view
         final float initAlpha = view.getAlpha();
-        final float dropViewScale = mDropView.getScaleX();
-
-        mDropAnim.setDuration(duration);
-        mDropAnim.setFloatValues(0.0f, 1.0f);
-        mDropAnim.removeAllUpdateListeners();
-        mDropAnim.addUpdateListener(new AnimatorUpdateListener() {
+        final float dropViewScale = view.getScaleX();
+        AnimatorUpdateListener updateCb = new AnimatorUpdateListener() {
+            @Override
             public void onAnimationUpdate(ValueAnimator animation) {
                 final float percent = (Float) animation.getAnimatedValue();
                 final int width = view.getMeasuredWidth();
@@ -603,7 +586,35 @@
                 mDropView.setScaleY(scaleY);
                 mDropView.setAlpha(alpha);
             }
-        });
+        };
+        animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle,
+                anchorView);
+    }
+
+    public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration,
+            TimeInterpolator interpolator, final Runnable onCompleteRunnable,
+            final int animationEndStyle, View anchorView) {
+        // Clean up the previous animations
+        if (mDropAnim != null) mDropAnim.cancel();
+        if (mFadeOutAnim != null) mFadeOutAnim.cancel();
+
+        // Show the drop view if it was previously hidden
+        mDropView = view;
+        mDropView.cancelAnimation();
+        mDropView.resetLayoutParams();
+
+        // Set the anchor view if the page is scrolling
+        if (anchorView != null) {
+            mAnchorViewInitialScrollX = anchorView.getScrollX();
+        }
+        mAnchorView = anchorView;
+
+        // Create and start the animation
+        mDropAnim = new ValueAnimator();
+        mDropAnim.setInterpolator(interpolator);
+        mDropAnim.setDuration(duration);
+        mDropAnim.setFloatValues(0f, 1f);
+        mDropAnim.addUpdateListener(updateCb);
         mDropAnim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
                 if (onCompleteRunnable != null) {
@@ -629,7 +640,7 @@
             mDropAnim.cancel();
         }
         if (mDropView != null) {
-            mDropView.remove();
+            mDragController.onDeferredEndDrag(mDropView);
         }
         mDropView = null;
         invalidate();
@@ -655,7 +666,7 @@
         mFadeOutAnim.addListener(new AnimatorListenerAdapter() {
             public void onAnimationEnd(Animator animation) {
                 if (mDropView != null) {
-                    mDropView.remove();
+                    mDragController.onDeferredEndDrag(mDropView);
                 }
                 mDropView = null;
                 invalidate();
diff --git a/src/com/android/launcher2/DragSource.java b/src/com/android/launcher2/DragSource.java
index 06f5ee1..a654b93 100644
--- a/src/com/android/launcher2/DragSource.java
+++ b/src/com/android/launcher2/DragSource.java
@@ -25,5 +25,6 @@
  *
  */
 public interface DragSource {
+    boolean supportsFlingToDelete();
     void onDropCompleted(View target, DragObject d, boolean success);
 }
diff --git a/src/com/android/launcher2/DragView.java b/src/com/android/launcher2/DragView.java
index 3090e8f..5636f99 100644
--- a/src/com/android/launcher2/DragView.java
+++ b/src/com/android/launcher2/DragView.java
@@ -51,6 +51,7 @@
     ValueAnimator mAnim;
     private float mOffsetX = 0.0f;
     private float mOffsetY = 0.0f;
+    private float mInitialScale = 1f;
 
     /**
      * Construct the drag view.
@@ -67,6 +68,7 @@
             int left, int top, int width, int height, final float initialScale) {
         super(launcher);
         mDragLayer = launcher.getDragLayer();
+        mInitialScale = initialScale;
 
         final Resources res = getResources();
         final float offsetX = res.getDimensionPixelSize(R.dimen.dragViewOffsetX);
@@ -151,6 +153,14 @@
         return mDragRegion;
     }
 
+    public float getInitialScale() {
+        return mInitialScale;
+    }
+
+    public void updateInitialScaleToCurrentScale() {
+        mInitialScale = getScaleX();
+    }
+
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight());
@@ -233,6 +243,11 @@
      */
     public void show(int touchX, int touchY) {
         mDragLayer.addView(this);
+
+        // Enable hw-layers on this view
+        setLayerType(View.LAYER_TYPE_HARDWARE, null);
+
+        // Start the pick-up animation
         DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0);
         lp.width = mBitmap.getWidth();
         lp.height = mBitmap.getHeight();
@@ -267,6 +282,9 @@
 
     void remove() {
         if (getParent() != null) {
+            // Disable hw-layers on this view
+            setLayerType(View.LAYER_TYPE_NONE, null);
+
             mDragLayer.removeView(DragView.this);
         }
     }
diff --git a/src/com/android/launcher2/DropTarget.java b/src/com/android/launcher2/DropTarget.java
index e49f782..fb714c6 100644
--- a/src/com/android/launcher2/DropTarget.java
+++ b/src/com/android/launcher2/DropTarget.java
@@ -16,6 +16,7 @@
 
 package com.android.launcher2;
 
+import android.graphics.PointF;
 import android.graphics.Rect;
 
 /**
@@ -92,6 +93,13 @@
     void onDragExit(DragObject dragObject);
 
     /**
+     * Handle an object being dropped as a result of flinging to delete and will be called in place
+     * of onDrop().  (This is only called on objects that are set as the DragController's
+     * fling-to-delete target.
+     */
+    void onFlingToDelete(DragObject dragObject, int x, int y, PointF vec);
+
+    /**
      * Allows a DropTarget to delegate drag and drop events to another object.
      *
      * Most subclasses will should just return null from this method.
diff --git a/src/com/android/launcher2/Folder.java b/src/com/android/launcher2/Folder.java
index 07e76c9..3e8c4a3 100644
--- a/src/com/android/launcher2/Folder.java
+++ b/src/com/android/launcher2/Folder.java
@@ -22,6 +22,7 @@
 import android.animation.PropertyValuesHolder;
 import android.content.Context;
 import android.content.res.Resources;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.text.InputType;
@@ -35,7 +36,6 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
-import android.view.View.MeasureSpec;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.inputmethod.EditorInfo;
@@ -653,6 +653,10 @@
         updateItemLocationsInDatabase();
     }
 
+    public boolean supportsFlingToDelete() {
+        return true;
+    }
+
     private void updateItemLocationsInDatabase() {
         ArrayList<View> list = getItemsInReadingOrder();
         for (int i = 0; i < list.size(); i++) {
@@ -924,6 +928,10 @@
         mInfo.add(item);
     }
 
+    public void onFlingToDelete(DragObject d, int x, int y, PointF vec) {
+        // Do nothing
+    }
+
     public void onAdd(ShortcutInfo item) {
         mItemsInvalidated = true;
         // If the item was dropped onto this open folder, we have done the work associated
diff --git a/src/com/android/launcher2/SearchDropTargetBar.java b/src/com/android/launcher2/SearchDropTargetBar.java
index 03ca38f..76d7076 100644
--- a/src/com/android/launcher2/SearchDropTargetBar.java
+++ b/src/com/android/launcher2/SearchDropTargetBar.java
@@ -69,6 +69,7 @@
         dragController.addDragListener(mDeleteDropTarget);
         dragController.addDropTarget(mInfoDropTarget);
         dragController.addDropTarget(mDeleteDropTarget);
+        dragController.setFlingToDeleteDropTarget(mDeleteDropTarget);
         mInfoDropTarget.setLauncher(launcher);
         mDeleteDropTarget.setLauncher(launcher);
     }
@@ -153,6 +154,13 @@
         });
     }
 
+    public void finishAnimations() {
+        mDropTargetBarFadeInAnim.end();
+        mDropTargetBarFadeOutAnim.end();
+        mQSBSearchBarFadeInAnim.end();
+        mQSBSearchBarFadeOutAnim.end();
+    }
+
     private void cancelAnimations() {
         mDropTargetBarFadeInAnim.cancel();
         mDropTargetBarFadeOutAnim.cancel();
diff --git a/src/com/android/launcher2/Workspace.java b/src/com/android/launcher2/Workspace.java
index 76070c4..79c262d 100644
--- a/src/com/android/launcher2/Workspace.java
+++ b/src/com/android/launcher2/Workspace.java
@@ -38,6 +38,7 @@
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Point;
+import android.graphics.PointF;
 import android.graphics.Rect;
 import android.graphics.Region.Op;
 import android.graphics.drawable.Drawable;
@@ -2333,6 +2334,10 @@
         }
     }
 
+    public void onFlingToDelete(DragObject d, int x, int y, PointF vec) {
+        // Do nothing
+    }
+
     public void setFinalScrollForPageChange(int screen) {
         if (screen >= 0) {
             mSavedScrollX = getScrollX();
@@ -3325,6 +3330,10 @@
         }
     }
 
+    public boolean supportsFlingToDelete() {
+        return true;
+    }
+
     public boolean isDropEnabled() {
         return true;
     }