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