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