DragController.java revision 0f97b7874fe29f36e7a40d2fe4b45d1e0e24fe1b
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.launcher3.dragndrop;
18
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Bitmap;
23import android.graphics.Point;
24import android.graphics.PointF;
25import android.graphics.Rect;
26import android.os.Handler;
27import android.os.IBinder;
28import android.util.Log;
29import android.view.DragEvent;
30import android.view.HapticFeedbackConstants;
31import android.view.KeyEvent;
32import android.view.MotionEvent;
33import android.view.VelocityTracker;
34import android.view.View;
35import android.view.ViewConfiguration;
36import android.view.inputmethod.InputMethodManager;
37
38import com.android.launcher3.DragSource;
39import com.android.launcher3.DropTarget;
40import com.android.launcher3.ItemInfo;
41import com.android.launcher3.Launcher;
42import com.android.launcher3.PagedView;
43import com.android.launcher3.R;
44import com.android.launcher3.ShortcutInfo;
45import com.android.launcher3.Utilities;
46import com.android.launcher3.accessibility.DragViewStateAnnouncer;
47import com.android.launcher3.util.Thunk;
48
49import java.util.ArrayList;
50import java.util.HashSet;
51
52/**
53 * Class for initiating a drag within a view or across multiple views.
54 */
55public class DragController implements DragDriver.EventListener {
56    private static final String TAG = "Launcher.DragController";
57
58    /** Indicates the drag is a move.  */
59    public static int DRAG_ACTION_MOVE = 0;
60
61    /** Indicates the drag is a copy.  */
62    public static int DRAG_ACTION_COPY = 1;
63
64    public static final int SCROLL_DELAY = 500;
65    public static final int RESCROLL_DELAY = PagedView.PAGE_SNAP_ANIMATION_DURATION + 150;
66
67    private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
68
69    private static final int SCROLL_OUTSIDE_ZONE = 0;
70    private static final int SCROLL_WAITING_IN_ZONE = 1;
71
72    public static final int SCROLL_NONE = -1;
73    public static final int SCROLL_LEFT = 0;
74    public static final int SCROLL_RIGHT = 1;
75
76    private static final float MAX_FLING_DEGREES = 35f;
77
78    @Thunk Launcher mLauncher;
79    private Handler mHandler;
80
81    // temporaries to avoid gc thrash
82    private Rect mRectTemp = new Rect();
83    private final int[] mCoordinatesTemp = new int[2];
84    private final boolean mIsRtl;
85
86    /**
87     * Drag driver for the current drag/drop operation, or null if there is no active DND operation.
88     * It's null during accessible drag operations.
89     */
90    private DragDriver mDragDriver = null;
91
92    /** Whether or not an accessible drag operation is in progress. */
93    private boolean mIsAccessibleDrag;
94
95    /** X coordinate of the down event. */
96    private int mMotionDownX;
97
98    /** Y coordinate of the down event. */
99    private int mMotionDownY;
100
101    /** the area at the edge of the screen that makes the workspace go left
102     *   or right while you're dragging.
103     */
104    private final int mScrollZone;
105
106    private DropTarget.DragObject mDragObject;
107
108    /** Who can receive drop events */
109    private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>();
110    private ArrayList<DragListener> mListeners = new ArrayList<DragListener>();
111    private DropTarget mFlingToDeleteDropTarget;
112
113    /** The window token used as the parent for the DragView. */
114    private IBinder mWindowToken;
115
116    /** The view that will be scrolled when dragging to the left and right edges of the screen. */
117    private View mScrollView;
118
119    private View mMoveTarget;
120
121    @Thunk DragScroller mDragScroller;
122    @Thunk int mScrollState = SCROLL_OUTSIDE_ZONE;
123    private ScrollRunnable mScrollRunnable = new ScrollRunnable();
124
125    private DropTarget mLastDropTarget;
126
127    private InputMethodManager mInputMethodManager;
128
129    @Thunk int mLastTouch[] = new int[2];
130    @Thunk long mLastTouchUpTime = -1;
131    @Thunk int mDistanceSinceScroll = 0;
132
133    private int mTmpPoint[] = new int[2];
134    private Rect mDragLayerRect = new Rect();
135
136    protected final int mFlingToDeleteThresholdVelocity;
137    private VelocityTracker mVelocityTracker;
138
139    /**
140     * Interface to receive notifications when a drag starts or stops
141     */
142    public interface DragListener {
143        /**
144         * A drag has begun
145         *
146         * @param source An object representing where the drag originated
147         * @param info The data associated with the object that is being dragged
148         * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE}
149         *        or {@link DragController#DRAG_ACTION_COPY}
150         */
151        void onDragStart(DragSource source, ItemInfo info, int dragAction);
152
153        /**
154         * The drag has ended
155         */
156        void onDragEnd();
157    }
158
159    /**
160     * Used to create a new DragLayer from XML.
161     *
162     * @param context The application's context.
163     */
164    public DragController(Launcher launcher) {
165        Resources r = launcher.getResources();
166        mLauncher = launcher;
167        mHandler = new Handler();
168        mScrollZone = r.getDimensionPixelSize(R.dimen.scroll_zone);
169        mVelocityTracker = VelocityTracker.obtain();
170
171        mFlingToDeleteThresholdVelocity =
172                r.getDimensionPixelSize(R.dimen.drag_flingToDeleteMinVelocity);
173        mIsRtl = Utilities.isRtl(r);
174    }
175
176    /**
177     * Starts a drag.
178     *
179     * @param v The view that is being dragged
180     * @param bmp The bitmap that represents the view being dragged
181     * @param source An object representing where the drag originated
182     * @param dragInfo The data associated with the object that is being dragged
183     * @param viewImageBounds the position of the image inside the view
184     * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or
185     *        {@link #DRAG_ACTION_COPY}
186     */
187    public void startDrag(View v, Bitmap bmp, DragSource source, ItemInfo dragInfo,
188            Rect viewImageBounds, int dragAction, float initialDragViewScale) {
189        int[] loc = mCoordinatesTemp;
190        mLauncher.getDragLayer().getLocationInDragLayer(v, loc);
191        int dragLayerX = loc[0] + viewImageBounds.left
192                + (int) ((initialDragViewScale * bmp.getWidth() - bmp.getWidth()) / 2);
193        int dragLayerY = loc[1] + viewImageBounds.top
194                + (int) ((initialDragViewScale * bmp.getHeight() - bmp.getHeight()) / 2);
195
196        startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, null,
197                null, initialDragViewScale, false);
198
199        if (dragAction == DRAG_ACTION_MOVE) {
200            v.setVisibility(View.GONE);
201        }
202    }
203
204    /**
205     * Starts a drag.
206     *
207     * @param b The bitmap to display as the drag image.  It will be re-scaled to the
208     *          enlarged size.
209     * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
210     * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
211     * @param source An object representing where the drag originated
212     * @param dragInfo The data associated with the object that is being dragged
213     * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or
214     *        {@link #DRAG_ACTION_COPY}
215     * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
216     *          Makes dragging feel more precise, e.g. you can clip out a transparent border
217     * @param accessible whether this drag should occur in accessibility mode
218     */
219    public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
220            DragSource source, ItemInfo dragInfo, int dragAction, Point dragOffset, Rect dragRegion,
221            float initialDragViewScale, boolean accessible) {
222        if (PROFILE_DRAWING_DURING_DRAG) {
223            android.os.Debug.startMethodTracing("Launcher");
224        }
225
226        // Hide soft keyboard, if visible
227        if (mInputMethodManager == null) {
228            mInputMethodManager = (InputMethodManager)
229                    mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE);
230        }
231        mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
232
233        for (DragListener listener : mListeners) {
234            listener.onDragStart(source, dragInfo, dragAction);
235        }
236
237        final int registrationX = mMotionDownX - dragLayerX;
238        final int registrationY = mMotionDownY - dragLayerY;
239
240        final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
241        final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
242
243        mIsAccessibleDrag = accessible;
244        mLastDropTarget = null;
245
246        mDragObject = new DropTarget.DragObject();
247
248        float finalDragViewScale = mLauncher.getWorkspace().getDragShrinkFactor();
249        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
250                registrationY, 0, 0, b.getWidth(), b.getHeight(),
251                initialDragViewScale, finalDragViewScale);
252
253        mDragObject.dragComplete = false;
254        if (mIsAccessibleDrag) {
255            // For an accessible drag, we assume the view is being dragged from the center.
256            mDragObject.xOffset = b.getWidth() / 2;
257            mDragObject.yOffset = b.getHeight() / 2;
258            mDragObject.accessibleDrag = true;
259        } else {
260            mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
261            mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
262            mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
263
264            mDragDriver = DragDriver.create(this, dragInfo, dragView);
265        }
266
267        mDragObject.dragSource = source;
268        mDragObject.dragInfo = dragInfo;
269
270        if (dragOffset != null) {
271            dragView.setDragVisualizeOffset(new Point(dragOffset));
272        }
273        if (dragRegion != null) {
274            dragView.setDragRegion(new Rect(dragRegion));
275        }
276
277        mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
278        dragView.show(mMotionDownX, mMotionDownY);
279        handleMoveEvent(mMotionDownX, mMotionDownY);
280        return dragView;
281    }
282
283    /**
284     * Call this from a drag source view like this:
285     *
286     * <pre>
287     *  @Override
288     *  public boolean dispatchKeyEvent(KeyEvent event) {
289     *      return mDragController.dispatchKeyEvent(this, event)
290     *              || super.dispatchKeyEvent(event);
291     * </pre>
292     */
293    public boolean dispatchKeyEvent(KeyEvent event) {
294        return mDragDriver != null;
295    }
296
297    public boolean isDragging() {
298        return mDragDriver != null || mIsAccessibleDrag;
299    }
300
301    /**
302     * Stop dragging without dropping.
303     */
304    public void cancelDrag() {
305        if (isDragging()) {
306            if (mLastDropTarget != null) {
307                mLastDropTarget.onDragExit(mDragObject);
308            }
309            mDragObject.deferDragViewCleanupPostAnimation = false;
310            mDragObject.cancelled = true;
311            mDragObject.dragComplete = true;
312            mDragObject.dragSource.onDropCompleted(null, mDragObject, false, false);
313        }
314        endDrag();
315    }
316
317    public void onAppsRemoved(final ArrayList<String> packageNames, HashSet<ComponentName> cns) {
318        // Cancel the current drag if we are removing an app that we are dragging
319        if (mDragObject != null) {
320            Object rawDragInfo = mDragObject.dragInfo;
321            if (rawDragInfo instanceof ShortcutInfo) {
322                ShortcutInfo dragInfo = (ShortcutInfo) rawDragInfo;
323                for (ComponentName componentName : cns) {
324                    // Added null checks to prevent NPE we've seen in the wild
325                    if (dragInfo != null && dragInfo.intent != null) {
326                        ComponentName cn = dragInfo.intent.getComponent();
327                        boolean isSameComponent = cn != null && (cn.equals(componentName) ||
328                                packageNames.contains(cn.getPackageName()));
329                        if (isSameComponent) {
330                            cancelDrag();
331                            return;
332                        }
333                    }
334                }
335            }
336        }
337    }
338
339    private void endDrag() {
340        if (isDragging()) {
341            mDragDriver = null;
342            mIsAccessibleDrag = false;
343            clearScrollRunnable();
344            boolean isDeferred = false;
345            if (mDragObject.dragView != null) {
346                isDeferred = mDragObject.deferDragViewCleanupPostAnimation;
347                if (!isDeferred) {
348                    mDragObject.dragView.remove();
349                }
350                mDragObject.dragView = null;
351            }
352
353            // Only end the drag if we are not deferred
354            if (!isDeferred) {
355                for (DragListener listener : new ArrayList<>(mListeners)) {
356                    listener.onDragEnd();
357                }
358            }
359        }
360
361        releaseVelocityTracker();
362    }
363
364    /**
365     * This only gets called as a result of drag view cleanup being deferred in endDrag();
366     */
367    void onDeferredEndDrag(DragView dragView) {
368        dragView.remove();
369
370        if (mDragObject.deferDragViewCleanupPostAnimation) {
371            // If we skipped calling onDragEnd() before, do it now
372            for (DragListener listener : new ArrayList<>(mListeners)) {
373                listener.onDragEnd();
374            }
375        }
376    }
377
378    public void onDeferredEndFling(DropTarget.DragObject d) {
379        d.dragSource.onFlingToDeleteCompleted();
380    }
381
382    /**
383     * Clamps the position to the drag layer bounds.
384     */
385    private int[] getClampedDragLayerPos(float x, float y) {
386        mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect);
387        mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1));
388        mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1));
389        return mTmpPoint;
390    }
391
392    public long getLastGestureUpTime() {
393        if (mDragDriver != null) {
394            return System.currentTimeMillis();
395        } else {
396            return mLastTouchUpTime;
397        }
398    }
399
400    public void resetLastGestureUpTime() {
401        mLastTouchUpTime = -1;
402    }
403
404    @Override
405    public void onDriverDragMove(float x, float y) {
406        final int[] dragLayerPos = getClampedDragLayerPos(x, y);
407
408        handleMoveEvent(dragLayerPos[0], dragLayerPos[1]);
409    }
410
411    @Override
412    public void onDriverDragExitWindow() {
413        if (mLastDropTarget != null) {
414            mLastDropTarget.onDragExit(mDragObject);
415            mLastDropTarget = null;
416        }
417    }
418
419    @Override
420    public void onDriverDragEnd(float x, float y, DropTarget dropTargetOverride) {
421        final int[] dragLayerPos = getClampedDragLayerPos(x, y);
422        final int dragLayerX = dragLayerPos[0];
423        final int dragLayerY = dragLayerPos[1];
424
425        DropTarget dropTarget;
426        PointF vec = null;
427
428        if (dropTargetOverride != null) {
429            dropTarget = dropTargetOverride;
430        } else {
431            vec = isFlingingToDelete(mDragObject.dragSource);
432            if (vec != null) {
433                dropTarget = mFlingToDeleteDropTarget;
434            } else {
435                dropTarget = findDropTarget((int) x, (int) y, mCoordinatesTemp);
436            }
437        }
438
439        drop(dropTarget, x, y, vec);
440
441        endDrag();
442    }
443
444    @Override
445    public void onDriverDragCancel() {
446        cancelDrag();
447    }
448
449    /**
450     * Call this from a drag source view.
451     */
452    public boolean onInterceptTouchEvent(MotionEvent ev) {
453        @SuppressWarnings("all") // suppress dead code warning
454        final boolean debug = false;
455        if (debug) {
456            Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " Dragging="
457                    + (mDragDriver != null));
458        }
459
460        if (mIsAccessibleDrag) {
461            return false;
462        }
463
464        // Update the velocity tracker
465        acquireVelocityTrackerAndAddMovement(ev);
466
467        final int action = ev.getAction();
468        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
469        final int dragLayerX = dragLayerPos[0];
470        final int dragLayerY = dragLayerPos[1];
471
472        switch (action) {
473            case MotionEvent.ACTION_DOWN:
474                // Remember location of down touch
475                mMotionDownX = dragLayerX;
476                mMotionDownY = dragLayerY;
477                break;
478            case MotionEvent.ACTION_UP:
479                mLastTouchUpTime = System.currentTimeMillis();
480                break;
481        }
482
483        return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
484    }
485
486    /**
487     * Call this from a drag source view.
488     */
489    public boolean onDragEvent(DragEvent event) {
490        return mDragDriver != null && mDragDriver.onDragEvent(event);
491    }
492
493    /**
494     * Call this from a drag view.
495     */
496    public void onDragViewAnimationEnd() {
497        if (mDragDriver != null) {
498            mDragDriver.onDragViewAnimationEnd();
499        }
500    }
501
502    /**
503     * Sets the view that should handle move events.
504     */
505    public void setMoveTarget(View view) {
506        mMoveTarget = view;
507    }
508
509    public boolean dispatchUnhandledMove(View focused, int direction) {
510        return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction);
511    }
512
513    private void clearScrollRunnable() {
514        mHandler.removeCallbacks(mScrollRunnable);
515        if (mScrollState == SCROLL_WAITING_IN_ZONE) {
516            mScrollState = SCROLL_OUTSIDE_ZONE;
517            mScrollRunnable.setDirection(SCROLL_RIGHT);
518            mDragScroller.onExitScrollArea();
519            mLauncher.getDragLayer().onExitScrollArea();
520        }
521    }
522
523    private void handleMoveEvent(int x, int y) {
524        mDragObject.dragView.move(x, y);
525
526        // Drop on someone?
527        final int[] coordinates = mCoordinatesTemp;
528        DropTarget dropTarget = findDropTarget(x, y, coordinates);
529        mDragObject.x = coordinates[0];
530        mDragObject.y = coordinates[1];
531        checkTouchMove(dropTarget);
532
533        // Check if we are hovering over the scroll areas
534        mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y);
535        mLastTouch[0] = x;
536        mLastTouch[1] = y;
537        checkScrollState(x, y);
538    }
539
540    public void forceTouchMove() {
541        int[] dummyCoordinates = mCoordinatesTemp;
542        DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates);
543        mDragObject.x = dummyCoordinates[0];
544        mDragObject.y = dummyCoordinates[1];
545        checkTouchMove(dropTarget);
546    }
547
548    private void checkTouchMove(DropTarget dropTarget) {
549        if (dropTarget != null) {
550            if (mLastDropTarget != dropTarget) {
551                if (mLastDropTarget != null) {
552                    mLastDropTarget.onDragExit(mDragObject);
553                }
554                dropTarget.onDragEnter(mDragObject);
555            }
556            dropTarget.onDragOver(mDragObject);
557        } else {
558            if (mLastDropTarget != null) {
559                mLastDropTarget.onDragExit(mDragObject);
560            }
561        }
562        mLastDropTarget = dropTarget;
563    }
564
565    @Thunk void checkScrollState(int x, int y) {
566        final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();
567        final int delay = mDistanceSinceScroll < slop ? RESCROLL_DELAY : SCROLL_DELAY;
568        final DragLayer dragLayer = mLauncher.getDragLayer();
569        final int forwardDirection = mIsRtl ? SCROLL_RIGHT : SCROLL_LEFT;
570        final int backwardsDirection = mIsRtl ? SCROLL_LEFT : SCROLL_RIGHT;
571
572        if (x < mScrollZone) {
573            if (mScrollState == SCROLL_OUTSIDE_ZONE) {
574                mScrollState = SCROLL_WAITING_IN_ZONE;
575                if (mDragScroller.onEnterScrollArea(x, y, forwardDirection)) {
576                    dragLayer.onEnterScrollArea(forwardDirection);
577                    mScrollRunnable.setDirection(forwardDirection);
578                    mHandler.postDelayed(mScrollRunnable, delay);
579                }
580            }
581        } else if (x > mScrollView.getWidth() - mScrollZone) {
582            if (mScrollState == SCROLL_OUTSIDE_ZONE) {
583                mScrollState = SCROLL_WAITING_IN_ZONE;
584                if (mDragScroller.onEnterScrollArea(x, y, backwardsDirection)) {
585                    dragLayer.onEnterScrollArea(backwardsDirection);
586                    mScrollRunnable.setDirection(backwardsDirection);
587                    mHandler.postDelayed(mScrollRunnable, delay);
588                }
589            }
590        } else {
591            clearScrollRunnable();
592        }
593    }
594
595    /**
596     * Call this from a drag source view.
597     */
598    public boolean onTouchEvent(MotionEvent ev) {
599        if (mDragDriver == null || mIsAccessibleDrag) {
600            return false;
601        }
602
603        // Update the velocity tracker
604        acquireVelocityTrackerAndAddMovement(ev);
605
606        final int action = ev.getAction();
607        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
608        final int dragLayerX = dragLayerPos[0];
609        final int dragLayerY = dragLayerPos[1];
610
611        switch (action) {
612            case MotionEvent.ACTION_DOWN:
613                // Remember where the motion event started
614                mMotionDownX = dragLayerX;
615                mMotionDownY = dragLayerY;
616
617                if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) {
618                    mScrollState = SCROLL_WAITING_IN_ZONE;
619                    mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
620                } else {
621                    mScrollState = SCROLL_OUTSIDE_ZONE;
622                }
623                break;
624            case MotionEvent.ACTION_UP:
625            case MotionEvent.ACTION_CANCEL:
626                mHandler.removeCallbacks(mScrollRunnable);
627                break;
628        }
629
630        return mDragDriver.onTouchEvent(ev);
631    }
632
633    /**
634     * Since accessible drag and drop won't cause the same sequence of touch events, we manually
635     * inject the appropriate state.
636     */
637    public void prepareAccessibleDrag(int x, int y) {
638        mMotionDownX = x;
639        mMotionDownY = y;
640    }
641
642    /**
643     * As above, since accessible drag and drop won't cause the same sequence of touch events,
644     * we manually ensure appropriate drag and drop events get emulated for accessible drag.
645     */
646    public void completeAccessibleDrag(int[] location) {
647        final int[] coordinates = mCoordinatesTemp;
648
649        // We make sure that we prime the target for drop.
650        DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates);
651        mDragObject.x = coordinates[0];
652        mDragObject.y = coordinates[1];
653        checkTouchMove(dropTarget);
654
655        dropTarget.prepareAccessibilityDrop();
656        // Perform the drop
657        drop(dropTarget, location[0], location[1], null);
658        endDrag();
659    }
660
661    /**
662     * Determines whether the user flung the current item to delete it.
663     *
664     * @return the vector at which the item was flung, or null if no fling was detected.
665     */
666    private PointF isFlingingToDelete(DragSource source) {
667        if (mFlingToDeleteDropTarget == null) return null;
668        if (!source.supportsFlingToDelete()) return null;
669
670        ViewConfiguration config = ViewConfiguration.get(mLauncher);
671        mVelocityTracker.computeCurrentVelocity(1000, config.getScaledMaximumFlingVelocity());
672        PointF vel = new PointF(mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity());
673        float theta = MAX_FLING_DEGREES + 1;
674        if (mVelocityTracker.getYVelocity() < mFlingToDeleteThresholdVelocity) {
675            // Do a quick dot product test to ensure that we are flinging upwards
676            PointF upVec = new PointF(0f, -1f);
677            theta = getAngleBetweenVectors(vel, upVec);
678        } else if (mLauncher.getDeviceProfile().isVerticalBarLayout() &&
679                mVelocityTracker.getXVelocity() < mFlingToDeleteThresholdVelocity) {
680            // Remove icon is on left side instead of top, so check if we are flinging to the left.
681            PointF leftVec = new PointF(-1f, 0f);
682            theta = getAngleBetweenVectors(vel, leftVec);
683        }
684        if (theta <= Math.toRadians(MAX_FLING_DEGREES)) {
685            return vel;
686        }
687        return null;
688    }
689
690    private float getAngleBetweenVectors(PointF vec1, PointF vec2) {
691        return (float) Math.acos(((vec1.x * vec2.x) + (vec1.y * vec2.y)) /
692                (vec1.length() * vec2.length()));
693    }
694
695    void drop(DropTarget dropTarget, float x, float y, PointF flingVel) {
696        final int[] coordinates = mCoordinatesTemp;
697
698        mDragObject.x = coordinates[0];
699        mDragObject.y = coordinates[1];
700
701        // Move dragging to the final target.
702        if (dropTarget != mLastDropTarget) {
703            if (mLastDropTarget != null) {
704                mLastDropTarget.onDragExit(mDragObject);
705            }
706            mLastDropTarget = dropTarget;
707            if (dropTarget != null) {
708                dropTarget.onDragEnter(mDragObject);
709            }
710        }
711
712        mDragObject.dragComplete = true;
713
714        // Drop onto the target.
715        boolean accepted = false;
716        if (dropTarget != null) {
717            dropTarget.onDragExit(mDragObject);
718            if (dropTarget.acceptDrop(mDragObject)) {
719                if (flingVel != null) {
720                    dropTarget.onFlingToDelete(mDragObject, flingVel);
721                } else {
722                    dropTarget.onDrop(mDragObject);
723                }
724                accepted = true;
725            }
726        }
727        final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null;
728        mDragObject.dragSource.onDropCompleted(
729                dropTargetAsView, mDragObject, flingVel != null, accepted);
730    }
731
732    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
733        final Rect r = mRectTemp;
734
735        final ArrayList<DropTarget> dropTargets = mDropTargets;
736        final int count = dropTargets.size();
737        for (int i=count-1; i>=0; i--) {
738            DropTarget target = dropTargets.get(i);
739            if (!target.isDropEnabled())
740                continue;
741
742            target.getHitRectRelativeToDragLayer(r);
743
744            mDragObject.x = x;
745            mDragObject.y = y;
746            if (r.contains(x, y)) {
747
748                dropCoordinates[0] = x;
749                dropCoordinates[1] = y;
750                mLauncher.getDragLayer().mapCoordInSelfToDescendent((View) target, dropCoordinates);
751
752                return target;
753            }
754        }
755        return null;
756    }
757
758    public void setDragScoller(DragScroller scroller) {
759        mDragScroller = scroller;
760    }
761
762    public void setWindowToken(IBinder token) {
763        mWindowToken = token;
764    }
765
766    /**
767     * Sets the drag listner which will be notified when a drag starts or ends.
768     */
769    public void addDragListener(DragListener l) {
770        mListeners.add(l);
771    }
772
773    /**
774     * Remove a previously installed drag listener.
775     */
776    public void removeDragListener(DragListener l) {
777        mListeners.remove(l);
778    }
779
780    /**
781     * Add a DropTarget to the list of potential places to receive drop events.
782     */
783    public void addDropTarget(DropTarget target) {
784        mDropTargets.add(target);
785    }
786
787    /**
788     * Don't send drop events to <em>target</em> any more.
789     */
790    public void removeDropTarget(DropTarget target) {
791        mDropTargets.remove(target);
792    }
793
794    /**
795     * Sets the current fling-to-delete drop target.
796     */
797    public void setFlingToDeleteDropTarget(DropTarget target) {
798        mFlingToDeleteDropTarget = target;
799    }
800
801    private void acquireVelocityTrackerAndAddMovement(MotionEvent ev) {
802        if (mVelocityTracker == null) {
803            mVelocityTracker = VelocityTracker.obtain();
804        }
805        mVelocityTracker.addMovement(ev);
806    }
807
808    private void releaseVelocityTracker() {
809        if (mVelocityTracker != null) {
810            mVelocityTracker.recycle();
811            mVelocityTracker = null;
812        }
813    }
814
815    /**
816     * Set which view scrolls for touch events near the edge of the screen.
817     */
818    public void setScrollView(View v) {
819        mScrollView = v;
820    }
821
822    private class ScrollRunnable implements Runnable {
823        private int mDirection;
824
825        ScrollRunnable() {
826        }
827
828        public void run() {
829            if (mDragScroller != null) {
830                if (mDirection == SCROLL_LEFT) {
831                    mDragScroller.scrollLeft();
832                } else {
833                    mDragScroller.scrollRight();
834                }
835                mScrollState = SCROLL_OUTSIDE_ZONE;
836                mDistanceSinceScroll = 0;
837                mDragScroller.onExitScrollArea();
838                mLauncher.getDragLayer().onExitScrollArea();
839
840                if (isDragging()) {
841                    // Check the scroll again so that we can requeue the scroller if necessary
842                    checkScrollState(mLastTouch[0], mLastTouch[1]);
843                }
844            }
845        }
846
847        void setDirection(int direction) {
848            mDirection = direction;
849        }
850    }
851}
852