DragController.java revision c57b7a8233d32c6dd8de5057c570afe3f50e3ef2
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.launcher2;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.Rect;
22import android.graphics.RectF;
23import android.os.Handler;
24import android.os.IBinder;
25import android.os.Vibrator;
26import android.util.Log;
27import android.view.KeyEvent;
28import android.view.MotionEvent;
29import android.view.View;
30import android.view.ViewConfiguration;
31import android.view.inputmethod.InputMethodManager;
32
33import com.android.launcher.R;
34
35import java.util.ArrayList;
36
37/**
38 * Class for initiating a drag within a view or across multiple views.
39 */
40public class DragController {
41    @SuppressWarnings({"UnusedDeclaration"})
42    private static final String TAG = "Launcher.DragController";
43
44    /** Indicates the drag is a move.  */
45    public static int DRAG_ACTION_MOVE = 0;
46
47    /** Indicates the drag is a copy.  */
48    public static int DRAG_ACTION_COPY = 1;
49
50    private static final int SCROLL_DELAY = 600;
51    private static final int VIBRATE_DURATION = 35;
52
53    private static final boolean PROFILE_DRAWING_DURING_DRAG = false;
54
55    private static final int SCROLL_OUTSIDE_ZONE = 0;
56    private static final int SCROLL_WAITING_IN_ZONE = 1;
57
58    static final int SCROLL_NONE = -1;
59    static final int SCROLL_LEFT = 0;
60    static final int SCROLL_RIGHT = 1;
61
62    private Launcher mLauncher;
63    private Handler mHandler;
64    private final Vibrator mVibrator = new Vibrator();
65
66    // temporaries to avoid gc thrash
67    private Rect mRectTemp = new Rect();
68    private final int[] mCoordinatesTemp = new int[2];
69
70    /** Whether or not we're dragging. */
71    private boolean mDragging;
72
73    /** X coordinate of the down event. */
74    private int mMotionDownX;
75
76    /** Y coordinate of the down event. */
77    private int mMotionDownY;
78
79    /** the area at the edge of the screen that makes the workspace go left
80     *   or right while you're dragging.
81     */
82    private int mScrollZone;
83
84    private DropTarget.DragObject mDragObject;
85
86    /** Who can receive drop events */
87    private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>();
88
89    private ArrayList<DragListener> mListeners = new ArrayList<DragListener>();
90
91    /** The window token used as the parent for the DragView. */
92    private IBinder mWindowToken;
93
94    /** The view that will be scrolled when dragging to the left and right edges of the screen. */
95    private View mScrollView;
96
97    private View mMoveTarget;
98
99    private DragScroller mDragScroller;
100    private int mScrollState = SCROLL_OUTSIDE_ZONE;
101    private ScrollRunnable mScrollRunnable = new ScrollRunnable();
102
103    private RectF mDeleteRegion;
104    private DropTarget mLastDropTarget;
105
106    private InputMethodManager mInputMethodManager;
107
108    private int mLastTouch[] = new int[2];
109    private int mDistanceSinceScroll = 0;
110
111    private int mTmpPoint[] = new int[2];
112    private Rect mDragLayerRect = new Rect();
113
114    /**
115     * Interface to receive notifications when a drag starts or stops
116     */
117    interface DragListener {
118
119        /**
120         * A drag has begun
121         *
122         * @param source An object representing where the drag originated
123         * @param info The data associated with the object that is being dragged
124         * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE}
125         *        or {@link DragController#DRAG_ACTION_COPY}
126         */
127        void onDragStart(DragSource source, Object info, int dragAction);
128
129        /**
130         * The drag has ended
131         */
132        void onDragEnd();
133    }
134
135    /**
136     * Used to create a new DragLayer from XML.
137     *
138     * @param context The application's context.
139     */
140    public DragController(Launcher launcher) {
141        mLauncher = launcher;
142        mHandler = new Handler();
143        mScrollZone = launcher.getResources().getDimensionPixelSize(R.dimen.scroll_zone);
144    }
145
146    public boolean dragging() {
147        return mDragging;
148    }
149
150    /**
151     * Starts a drag.
152     *
153     * @param v The view that is being dragged
154     * @param source An object representing where the drag originated
155     * @param dragInfo The data associated with the object that is being dragged
156     * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or
157     *        {@link #DRAG_ACTION_COPY}
158     */
159    public void startDrag(View v, DragSource source, Object dragInfo, int dragAction) {
160        startDrag(v, source, dragInfo, dragAction, null);
161    }
162
163    /**
164     * Starts a drag.
165     *
166     * @param v The view that is being dragged
167     * @param source An object representing where the drag originated
168     * @param dragInfo The data associated with the object that is being dragged
169     * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or
170     *        {@link #DRAG_ACTION_COPY}
171     * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
172     *          Makes dragging feel more precise, e.g. you can clip out a transparent border
173     */
174    public void startDrag(View v, DragSource source, Object dragInfo, int dragAction,
175            Rect dragRegion) {
176        Bitmap b = getViewBitmap(v);
177
178        if (b == null) {
179            // out of memory?
180            return;
181        }
182
183        int[] loc = mCoordinatesTemp;
184        mLauncher.getDragLayer().getLocationInDragLayer(v, loc);
185        int dragLayerX = loc[0];
186        int dragLayerY = loc[1];
187
188        startDrag(b, dragLayerX, dragLayerY, source, dragInfo, dragAction, dragRegion);
189        b.recycle();
190
191        if (dragAction == DRAG_ACTION_MOVE) {
192            v.setVisibility(View.GONE);
193        }
194    }
195
196    /**
197     * Starts a drag.
198     *
199     * @param v The view that is being dragged
200     * @param bmp The bitmap that represents the view being dragged
201     * @param source An object representing where the drag originated
202     * @param dragInfo The data associated with the object that is being dragged
203     * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or
204     *        {@link #DRAG_ACTION_COPY}
205     * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
206     *          Makes dragging feel more precise, e.g. you can clip out a transparent border
207     */
208    public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction,
209            Rect dragRegion) {
210        int[] loc = mCoordinatesTemp;
211        mLauncher.getDragLayer().getLocationInDragLayer(v, loc);
212        int dragLayerX = loc[0];
213        int dragLayerY = loc[1];
214
215        startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, dragRegion);
216
217        if (dragAction == DRAG_ACTION_MOVE) {
218            v.setVisibility(View.GONE);
219        }
220    }
221
222    /**
223     * Starts a drag.
224     *
225     * @param b The bitmap to display as the drag image.  It will be re-scaled to the
226     *          enlarged size.
227     * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
228     * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
229     * @param source An object representing where the drag originated
230     * @param dragInfo The data associated with the object that is being dragged
231     * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or
232     *        {@link #DRAG_ACTION_COPY}
233     */
234    public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,
235            DragSource source, Object dragInfo, int dragAction) {
236        startDrag(b, dragLayerX, dragLayerY, source, dragInfo, dragAction, null);
237    }
238
239    /**
240     * Starts a drag.
241     *
242     * @param b The bitmap to display as the drag image.  It will be re-scaled to the
243     *          enlarged size.
244     * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
245     * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
246     * @param source An object representing where the drag originated
247     * @param dragInfo The data associated with the object that is being dragged
248     * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or
249     *        {@link #DRAG_ACTION_COPY}
250     * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
251     *          Makes dragging feel more precise, e.g. you can clip out a transparent border
252     */
253    public void startDrag(Bitmap b, int dragLayerX, int dragLayerY,
254            DragSource source, Object dragInfo, int dragAction, Rect dragRegion) {
255        if (PROFILE_DRAWING_DURING_DRAG) {
256            android.os.Debug.startMethodTracing("Launcher");
257        }
258
259        // Hide soft keyboard, if visible
260        if (mInputMethodManager == null) {
261            mInputMethodManager = (InputMethodManager)
262                    mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE);
263        }
264        mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
265
266        for (DragListener listener : mListeners) {
267            listener.onDragStart(source, dragInfo, dragAction);
268        }
269
270        final int registrationX = mMotionDownX - dragLayerX;
271        final int registrationY = mMotionDownY - dragLayerY;
272
273        final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
274        final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
275
276        mDragging = true;
277
278        mDragObject = new DropTarget.DragObject();
279
280        mDragObject.dragComplete = false;
281        mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
282        mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
283        mDragObject.dragSource = source;
284        mDragObject.dragInfo = dragInfo;
285
286        mVibrator.vibrate(VIBRATE_DURATION);
287
288        final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
289                registrationY, 0, 0, b.getWidth(), b.getHeight());
290
291        if (dragRegion != null) {
292            dragView.setDragRegion(new Rect(dragRegion));
293        }
294
295        dragView.show(mMotionDownX, mMotionDownY);
296        handleMoveEvent(mMotionDownX, mMotionDownY);
297    }
298
299    /**
300     * Draw the view into a bitmap.
301     */
302    Bitmap getViewBitmap(View v) {
303        v.clearFocus();
304        v.setPressed(false);
305
306        boolean willNotCache = v.willNotCacheDrawing();
307        v.setWillNotCacheDrawing(false);
308
309        // Reset the drawing cache background color to fully transparent
310        // for the duration of this operation
311        int color = v.getDrawingCacheBackgroundColor();
312        v.setDrawingCacheBackgroundColor(0);
313        float alpha = v.getAlpha();
314        v.setAlpha(1.0f);
315
316        if (color != 0) {
317            v.destroyDrawingCache();
318        }
319        v.buildDrawingCache();
320        Bitmap cacheBitmap = v.getDrawingCache();
321        if (cacheBitmap == null) {
322            Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException());
323            return null;
324        }
325
326        Bitmap bitmap = Bitmap.createBitmap(cacheBitmap);
327
328        // Restore the view
329        v.destroyDrawingCache();
330        v.setAlpha(alpha);
331        v.setWillNotCacheDrawing(willNotCache);
332        v.setDrawingCacheBackgroundColor(color);
333
334        return bitmap;
335    }
336
337    /**
338     * Call this from a drag source view like this:
339     *
340     * <pre>
341     *  @Override
342     *  public boolean dispatchKeyEvent(KeyEvent event) {
343     *      return mDragController.dispatchKeyEvent(this, event)
344     *              || super.dispatchKeyEvent(event);
345     * </pre>
346     */
347    @SuppressWarnings({"UnusedDeclaration"})
348    public boolean dispatchKeyEvent(KeyEvent event) {
349        return mDragging;
350    }
351
352    public boolean isDragging() {
353        return mDragging;
354    }
355
356    /**
357     * Stop dragging without dropping.
358     */
359    public void cancelDrag() {
360        if (mDragging) {
361            if (mLastDropTarget != null) {
362                mLastDropTarget.onDragExit(mDragObject);
363            }
364            mDragObject.dragComplete = true;
365            mDragObject.dragSource.onDropCompleted(null, mDragObject, false);
366        }
367        endDrag();
368    }
369
370    private void endDrag() {
371        if (mDragging) {
372            mDragging = false;
373            for (DragListener listener : mListeners) {
374                listener.onDragEnd();
375            }
376            if (mDragObject.dragView != null) {
377                mDragObject.dragView.remove();
378                mDragObject.dragView = null;
379            }
380        }
381    }
382
383    /**
384     * Clamps the position to the drag layer bounds.
385     */
386    private int[] getClampedDragLayerPos(float x, float y) {
387        mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect);
388        mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1));
389        mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1));
390        return mTmpPoint;
391    }
392
393    /**
394     * Call this from a drag source view.
395     */
396    public boolean onInterceptTouchEvent(MotionEvent ev) {
397        if (false) {
398            Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging="
399                    + mDragging);
400        }
401        final int action = ev.getAction();
402
403        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
404        final int dragLayerX = dragLayerPos[0];
405        final int dragLayerY = dragLayerPos[1];
406
407        switch (action) {
408            case MotionEvent.ACTION_MOVE:
409                break;
410            case MotionEvent.ACTION_DOWN:
411                // Remember location of down touch
412                mMotionDownX = dragLayerX;
413                mMotionDownY = dragLayerY;
414                mLastDropTarget = null;
415                break;
416            case MotionEvent.ACTION_UP:
417                if (mDragging) {
418                    drop(dragLayerX, dragLayerY);
419                }
420                endDrag();
421                break;
422            case MotionEvent.ACTION_CANCEL:
423                cancelDrag();
424                break;
425        }
426
427        return mDragging;
428    }
429
430    /**
431     * Sets the view that should handle move events.
432     */
433    void setMoveTarget(View view) {
434        mMoveTarget = view;
435    }
436
437    public boolean dispatchUnhandledMove(View focused, int direction) {
438        return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction);
439    }
440
441    private void handleMoveEvent(int x, int y) {
442        mDragObject.dragView.move(x, y);
443
444        // Drop on someone?
445        final int[] coordinates = mCoordinatesTemp;
446        DropTarget dropTarget = findDropTarget(x, y, coordinates);
447        mDragObject.x = coordinates[0];
448        mDragObject.y = coordinates[1];
449        if (dropTarget != null) {
450            DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject);
451            if (delegate != null) {
452                dropTarget = delegate;
453            }
454
455            if (mLastDropTarget != dropTarget) {
456                if (mLastDropTarget != null) {
457                    mLastDropTarget.onDragExit(mDragObject);
458                }
459                dropTarget.onDragEnter(mDragObject);
460            }
461            dropTarget.onDragOver(mDragObject);
462        } else {
463            if (mLastDropTarget != null) {
464                mLastDropTarget.onDragExit(mDragObject);
465            }
466        }
467        mLastDropTarget = dropTarget;
468
469        // Scroll, maybe, but not if we're in the delete region.
470        boolean inDeleteRegion = false;
471        if (mDeleteRegion != null) {
472            inDeleteRegion = mDeleteRegion.contains(x, y);
473        }
474
475        // After a scroll, the touch point will still be in the scroll region.
476        // Rather than scrolling immediately, require a bit of twiddling to scroll again
477        final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();
478        mDistanceSinceScroll +=
479            Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2));
480        mLastTouch[0] = x;
481        mLastTouch[1] = y;
482
483        if (!inDeleteRegion && x < mScrollZone) {
484            if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) {
485                mScrollState = SCROLL_WAITING_IN_ZONE;
486                mScrollRunnable.setDirection(SCROLL_LEFT);
487                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
488                mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT);
489            }
490        } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) {
491            if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) {
492                mScrollState = SCROLL_WAITING_IN_ZONE;
493                mScrollRunnable.setDirection(SCROLL_RIGHT);
494                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
495                mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT);
496            }
497        } else {
498            if (mScrollState == SCROLL_WAITING_IN_ZONE) {
499                mScrollState = SCROLL_OUTSIDE_ZONE;
500                mScrollRunnable.setDirection(SCROLL_RIGHT);
501                mHandler.removeCallbacks(mScrollRunnable);
502                mDragScroller.onExitScrollArea();
503            }
504        }
505    }
506
507    /**
508     * Call this from a drag source view.
509     */
510    public boolean onTouchEvent(MotionEvent ev) {
511        if (!mDragging) {
512            return false;
513        }
514
515        final int action = ev.getAction();
516        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
517        final int dragLayerX = dragLayerPos[0];
518        final int dragLayerY = dragLayerPos[1];
519
520        switch (action) {
521        case MotionEvent.ACTION_DOWN:
522            // Remember where the motion event started
523            mMotionDownX = dragLayerX;
524            mMotionDownY = dragLayerY;
525
526            if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) {
527                mScrollState = SCROLL_WAITING_IN_ZONE;
528                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
529            } else {
530                mScrollState = SCROLL_OUTSIDE_ZONE;
531            }
532            break;
533        case MotionEvent.ACTION_MOVE:
534            handleMoveEvent(dragLayerX, dragLayerY);
535            break;
536        case MotionEvent.ACTION_UP:
537            // Ensure that we've processed a move event at the current pointer location.
538            handleMoveEvent(dragLayerX, dragLayerY);
539
540            mHandler.removeCallbacks(mScrollRunnable);
541            if (mDragging) {
542                drop(dragLayerX, dragLayerY);
543            }
544            endDrag();
545            break;
546        case MotionEvent.ACTION_CANCEL:
547            cancelDrag();
548            break;
549        }
550
551        return true;
552    }
553
554    private void drop(float x, float y) {
555        final int[] coordinates = mCoordinatesTemp;
556        final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
557
558        mDragObject.x = coordinates[0];
559        mDragObject.y = coordinates[1];
560        boolean accepted = false;
561        if (dropTarget != null) {
562            mDragObject.dragComplete = true;
563            dropTarget.onDragExit(mDragObject);
564            if (dropTarget.acceptDrop(mDragObject)) {
565                dropTarget.onDrop(mDragObject);
566                accepted = true;
567            }
568        }
569        mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, accepted);
570    }
571
572    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
573        final Rect r = mRectTemp;
574
575        final ArrayList<DropTarget> dropTargets = mDropTargets;
576        final int count = dropTargets.size();
577        for (int i=count-1; i>=0; i--) {
578            DropTarget target = dropTargets.get(i);
579            if (!target.isDropEnabled())
580                continue;
581
582            target.getHitRect(r);
583
584            // Convert the hit rect to DragLayer coordinates
585            target.getLocationInDragLayer(dropCoordinates);
586            r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop());
587
588            mDragObject.x = x;
589            mDragObject.y = y;
590            if (r.contains(x, y)) {
591                DropTarget delegate = target.getDropTargetDelegate(mDragObject);
592                if (delegate != null) {
593                    target = delegate;
594                    target.getLocationInDragLayer(dropCoordinates);
595                }
596
597                // Make dropCoordinates relative to the DropTarget
598                dropCoordinates[0] = x - dropCoordinates[0];
599                dropCoordinates[1] = y - dropCoordinates[1];
600
601                return target;
602            }
603        }
604        return null;
605    }
606
607    public void setDragScoller(DragScroller scroller) {
608        mDragScroller = scroller;
609    }
610
611    public void setWindowToken(IBinder token) {
612        mWindowToken = token;
613    }
614
615    /**
616     * Sets the drag listner which will be notified when a drag starts or ends.
617     */
618    public void addDragListener(DragListener l) {
619        mListeners.add(l);
620    }
621
622    /**
623     * Remove a previously installed drag listener.
624     */
625    public void removeDragListener(DragListener l) {
626        mListeners.remove(l);
627    }
628
629    /**
630     * Add a DropTarget to the list of potential places to receive drop events.
631     */
632    public void addDropTarget(DropTarget target) {
633        mDropTargets.add(target);
634    }
635
636    /**
637     * Don't send drop events to <em>target</em> any more.
638     */
639    public void removeDropTarget(DropTarget target) {
640        mDropTargets.remove(target);
641    }
642
643    /**
644     * Set which view scrolls for touch events near the edge of the screen.
645     */
646    public void setScrollView(View v) {
647        mScrollView = v;
648    }
649
650    /**
651     * Specifies the delete region.  We won't scroll on touch events over the delete region.
652     *
653     * @param region The rectangle in DragLayer coordinates of the delete region.
654     */
655    void setDeleteRegion(RectF region) {
656        mDeleteRegion = region;
657    }
658
659    DragView getDragView() {
660        return mDragObject.dragView;
661    }
662
663    private class ScrollRunnable implements Runnable {
664        private int mDirection;
665
666        ScrollRunnable() {
667        }
668
669        public void run() {
670            if (mDragScroller != null) {
671                if (mDirection == SCROLL_LEFT) {
672                    mDragScroller.scrollLeft();
673                } else {
674                    mDragScroller.scrollRight();
675                }
676                mScrollState = SCROLL_OUTSIDE_ZONE;
677                mDistanceSinceScroll = 0;
678                mDragScroller.onExitScrollArea();
679            }
680        }
681
682        void setDirection(int direction) {
683            mDirection = direction;
684        }
685    }
686}
687