DragController.java revision 36cc09b07b19198f4ea886583cef462ade27192c
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.cancelled = true;
365            mDragObject.dragComplete = true;
366            mDragObject.dragSource.onDropCompleted(null, mDragObject, false);
367        }
368        endDrag();
369    }
370
371    private void endDrag() {
372        if (mDragging) {
373            mDragging = false;
374            for (DragListener listener : mListeners) {
375                listener.onDragEnd();
376            }
377            if (mDragObject.dragView != null) {
378                mDragObject.dragView.remove();
379                mDragObject.dragView = null;
380            }
381        }
382    }
383
384    /**
385     * Clamps the position to the drag layer bounds.
386     */
387    private int[] getClampedDragLayerPos(float x, float y) {
388        mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect);
389        mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1));
390        mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1));
391        return mTmpPoint;
392    }
393
394    /**
395     * Call this from a drag source view.
396     */
397    public boolean onInterceptTouchEvent(MotionEvent ev) {
398        if (false) {
399            Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging="
400                    + mDragging);
401        }
402        final int action = ev.getAction();
403
404        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
405        final int dragLayerX = dragLayerPos[0];
406        final int dragLayerY = dragLayerPos[1];
407
408        switch (action) {
409            case MotionEvent.ACTION_MOVE:
410                break;
411            case MotionEvent.ACTION_DOWN:
412                // Remember location of down touch
413                mMotionDownX = dragLayerX;
414                mMotionDownY = dragLayerY;
415                mLastDropTarget = null;
416                break;
417            case MotionEvent.ACTION_UP:
418                if (mDragging) {
419                    drop(dragLayerX, dragLayerY);
420                }
421                endDrag();
422                break;
423            case MotionEvent.ACTION_CANCEL:
424                cancelDrag();
425                break;
426        }
427
428        return mDragging;
429    }
430
431    /**
432     * Sets the view that should handle move events.
433     */
434    void setMoveTarget(View view) {
435        mMoveTarget = view;
436    }
437
438    public boolean dispatchUnhandledMove(View focused, int direction) {
439        return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction);
440    }
441
442    private void handleMoveEvent(int x, int y) {
443        mDragObject.dragView.move(x, y);
444
445        // Drop on someone?
446        final int[] coordinates = mCoordinatesTemp;
447        DropTarget dropTarget = findDropTarget(x, y, coordinates);
448        mDragObject.x = coordinates[0];
449        mDragObject.y = coordinates[1];
450        if (dropTarget != null) {
451            DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject);
452            if (delegate != null) {
453                dropTarget = delegate;
454            }
455
456            if (mLastDropTarget != dropTarget) {
457                if (mLastDropTarget != null) {
458                    mLastDropTarget.onDragExit(mDragObject);
459                }
460                dropTarget.onDragEnter(mDragObject);
461            }
462            dropTarget.onDragOver(mDragObject);
463        } else {
464            if (mLastDropTarget != null) {
465                mLastDropTarget.onDragExit(mDragObject);
466            }
467        }
468        mLastDropTarget = dropTarget;
469
470        // Scroll, maybe, but not if we're in the delete region.
471        boolean inDeleteRegion = false;
472        if (mDeleteRegion != null) {
473            inDeleteRegion = mDeleteRegion.contains(x, y);
474        }
475
476        // After a scroll, the touch point will still be in the scroll region.
477        // Rather than scrolling immediately, require a bit of twiddling to scroll again
478        final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop();
479        mDistanceSinceScroll +=
480            Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2));
481        mLastTouch[0] = x;
482        mLastTouch[1] = y;
483
484        if (!inDeleteRegion && x < mScrollZone) {
485            if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) {
486                mScrollState = SCROLL_WAITING_IN_ZONE;
487                mScrollRunnable.setDirection(SCROLL_LEFT);
488                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
489                mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT);
490            }
491        } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) {
492            if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) {
493                mScrollState = SCROLL_WAITING_IN_ZONE;
494                mScrollRunnable.setDirection(SCROLL_RIGHT);
495                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
496                mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT);
497            }
498        } else {
499            if (mScrollState == SCROLL_WAITING_IN_ZONE) {
500                mScrollState = SCROLL_OUTSIDE_ZONE;
501                mScrollRunnable.setDirection(SCROLL_RIGHT);
502                mHandler.removeCallbacks(mScrollRunnable);
503                mDragScroller.onExitScrollArea();
504            }
505        }
506    }
507
508    /**
509     * Call this from a drag source view.
510     */
511    public boolean onTouchEvent(MotionEvent ev) {
512        if (!mDragging) {
513            return false;
514        }
515
516        final int action = ev.getAction();
517        final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY());
518        final int dragLayerX = dragLayerPos[0];
519        final int dragLayerY = dragLayerPos[1];
520
521        switch (action) {
522        case MotionEvent.ACTION_DOWN:
523            // Remember where the motion event started
524            mMotionDownX = dragLayerX;
525            mMotionDownY = dragLayerY;
526
527            if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) {
528                mScrollState = SCROLL_WAITING_IN_ZONE;
529                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
530            } else {
531                mScrollState = SCROLL_OUTSIDE_ZONE;
532            }
533            break;
534        case MotionEvent.ACTION_MOVE:
535            handleMoveEvent(dragLayerX, dragLayerY);
536            break;
537        case MotionEvent.ACTION_UP:
538            // Ensure that we've processed a move event at the current pointer location.
539            handleMoveEvent(dragLayerX, dragLayerY);
540
541            mHandler.removeCallbacks(mScrollRunnable);
542            if (mDragging) {
543                drop(dragLayerX, dragLayerY);
544            }
545            endDrag();
546            break;
547        case MotionEvent.ACTION_CANCEL:
548            cancelDrag();
549            break;
550        }
551
552        return true;
553    }
554
555    private void drop(float x, float y) {
556        final int[] coordinates = mCoordinatesTemp;
557        final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
558
559        mDragObject.x = coordinates[0];
560        mDragObject.y = coordinates[1];
561        boolean accepted = false;
562        if (dropTarget != null) {
563            mDragObject.dragComplete = true;
564            dropTarget.onDragExit(mDragObject);
565            if (dropTarget.acceptDrop(mDragObject)) {
566                dropTarget.onDrop(mDragObject);
567                accepted = true;
568            }
569        }
570        mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, accepted);
571    }
572
573    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
574        final Rect r = mRectTemp;
575
576        final ArrayList<DropTarget> dropTargets = mDropTargets;
577        final int count = dropTargets.size();
578        for (int i=count-1; i>=0; i--) {
579            DropTarget target = dropTargets.get(i);
580            if (!target.isDropEnabled())
581                continue;
582
583            target.getHitRect(r);
584
585            // Convert the hit rect to DragLayer coordinates
586            target.getLocationInDragLayer(dropCoordinates);
587            r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop());
588
589            mDragObject.x = x;
590            mDragObject.y = y;
591            if (r.contains(x, y)) {
592                DropTarget delegate = target.getDropTargetDelegate(mDragObject);
593                if (delegate != null) {
594                    target = delegate;
595                    target.getLocationInDragLayer(dropCoordinates);
596                }
597
598                // Make dropCoordinates relative to the DropTarget
599                dropCoordinates[0] = x - dropCoordinates[0];
600                dropCoordinates[1] = y - dropCoordinates[1];
601
602                return target;
603            }
604        }
605        return null;
606    }
607
608    public void setDragScoller(DragScroller scroller) {
609        mDragScroller = scroller;
610    }
611
612    public void setWindowToken(IBinder token) {
613        mWindowToken = token;
614    }
615
616    /**
617     * Sets the drag listner which will be notified when a drag starts or ends.
618     */
619    public void addDragListener(DragListener l) {
620        mListeners.add(l);
621    }
622
623    /**
624     * Remove a previously installed drag listener.
625     */
626    public void removeDragListener(DragListener l) {
627        mListeners.remove(l);
628    }
629
630    /**
631     * Add a DropTarget to the list of potential places to receive drop events.
632     */
633    public void addDropTarget(DropTarget target) {
634        mDropTargets.add(target);
635    }
636
637    /**
638     * Don't send drop events to <em>target</em> any more.
639     */
640    public void removeDropTarget(DropTarget target) {
641        mDropTargets.remove(target);
642    }
643
644    /**
645     * Set which view scrolls for touch events near the edge of the screen.
646     */
647    public void setScrollView(View v) {
648        mScrollView = v;
649    }
650
651    /**
652     * Specifies the delete region.  We won't scroll on touch events over the delete region.
653     *
654     * @param region The rectangle in DragLayer coordinates of the delete region.
655     */
656    void setDeleteRegion(RectF region) {
657        mDeleteRegion = region;
658    }
659
660    DragView getDragView() {
661        return mDragObject.dragView;
662    }
663
664    private class ScrollRunnable implements Runnable {
665        private int mDirection;
666
667        ScrollRunnable() {
668        }
669
670        public void run() {
671            if (mDragScroller != null) {
672                if (mDirection == SCROLL_LEFT) {
673                    mDragScroller.scrollLeft();
674                } else {
675                    mDragScroller.scrollRight();
676                }
677                mScrollState = SCROLL_OUTSIDE_ZONE;
678                mDistanceSinceScroll = 0;
679                mDragScroller.onExitScrollArea();
680            }
681        }
682
683        void setDirection(int direction) {
684            mDirection = direction;
685        }
686    }
687}
688