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