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