Home | History | Annotate | Download | only in launcher2
      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 
     17 package com.android.launcher2;
     18 
     19 import android.content.Context;
     20 import android.graphics.Bitmap;
     21 import android.graphics.Rect;
     22 import android.graphics.RectF;
     23 import android.os.IBinder;
     24 import android.os.Handler;
     25 import android.os.Vibrator;
     26 import android.util.DisplayMetrics;
     27 import android.util.Log;
     28 import android.view.View;
     29 import android.view.KeyEvent;
     30 import android.view.MotionEvent;
     31 import android.view.WindowManager;
     32 import android.view.inputmethod.InputMethodManager;
     33 
     34 import java.util.ArrayList;
     35 
     36 /**
     37  * Class for initiating a drag within a view or across multiple views.
     38  */
     39 public 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 DragListener mListener;
    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 eneded
    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         mOriginator = v;
    163 
    164         Bitmap b = getViewBitmap(v);
    165 
    166         if (b == null) {
    167             // out of memory?
    168             return;
    169         }
    170 
    171         int[] loc = mCoordinatesTemp;
    172         v.getLocationOnScreen(loc);
    173         int screenX = loc[0];
    174         int screenY = loc[1];
    175 
    176         startDrag(b, screenX, screenY, 0, 0, b.getWidth(), b.getHeight(),
    177                 source, dragInfo, dragAction);
    178 
    179         b.recycle();
    180 
    181         if (dragAction == DRAG_ACTION_MOVE) {
    182             v.setVisibility(View.GONE);
    183         }
    184     }
    185 
    186     /**
    187      * Starts a drag.
    188      *
    189      * @param b The bitmap to display as the drag image.  It will be re-scaled to the
    190      *          enlarged size.
    191      * @param screenX The x position on screen of the left-top of the bitmap.
    192      * @param screenY The y position on screen of the left-top of the bitmap.
    193      * @param textureLeft The left edge of the region inside b to use.
    194      * @param textureTop The top edge of the region inside b to use.
    195      * @param textureWidth The width of the region inside b to use.
    196      * @param textureHeight The height of the region inside b to use.
    197      * @param source An object representing where the drag originated
    198      * @param dragInfo The data associated with the object that is being dragged
    199      * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or
    200      *        {@link #DRAG_ACTION_COPY}
    201      */
    202     public void startDrag(Bitmap b, int screenX, int screenY,
    203             int textureLeft, int textureTop, int textureWidth, int textureHeight,
    204             DragSource source, Object dragInfo, int dragAction) {
    205         if (PROFILE_DRAWING_DURING_DRAG) {
    206             android.os.Debug.startMethodTracing("Launcher");
    207         }
    208 
    209         // Hide soft keyboard, if visible
    210         if (mInputMethodManager == null) {
    211             mInputMethodManager = (InputMethodManager)
    212                     mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
    213         }
    214         mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
    215 
    216         if (mListener != null) {
    217             mListener.onDragStart(source, dragInfo, dragAction);
    218         }
    219 
    220         int registrationX = ((int)mMotionDownX) - screenX;
    221         int registrationY = ((int)mMotionDownY) - screenY;
    222 
    223         mTouchOffsetX = mMotionDownX - screenX;
    224         mTouchOffsetY = mMotionDownY - screenY;
    225 
    226         mDragging = true;
    227         mDragSource = source;
    228         mDragInfo = dragInfo;
    229 
    230         mVibrator.vibrate(VIBRATE_DURATION);
    231 
    232         DragView dragView = mDragView = new DragView(mContext, b, registrationX, registrationY,
    233                 textureLeft, textureTop, textureWidth, textureHeight);
    234         dragView.show(mWindowToken, (int)mMotionDownX, (int)mMotionDownY);
    235     }
    236 
    237     /**
    238      * Draw the view into a bitmap.
    239      */
    240     private Bitmap getViewBitmap(View v) {
    241         v.clearFocus();
    242         v.setPressed(false);
    243 
    244         boolean willNotCache = v.willNotCacheDrawing();
    245         v.setWillNotCacheDrawing(false);
    246 
    247         // Reset the drawing cache background color to fully transparent
    248         // for the duration of this operation
    249         int color = v.getDrawingCacheBackgroundColor();
    250         v.setDrawingCacheBackgroundColor(0);
    251 
    252         if (color != 0) {
    253             v.destroyDrawingCache();
    254         }
    255         v.buildDrawingCache();
    256         Bitmap cacheBitmap = v.getDrawingCache();
    257         if (cacheBitmap == null) {
    258             Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException());
    259             return null;
    260         }
    261 
    262         Bitmap bitmap = Bitmap.createBitmap(cacheBitmap);
    263 
    264         // Restore the view
    265         v.destroyDrawingCache();
    266         v.setWillNotCacheDrawing(willNotCache);
    267         v.setDrawingCacheBackgroundColor(color);
    268 
    269         return bitmap;
    270     }
    271 
    272     /**
    273      * Call this from a drag source view like this:
    274      *
    275      * <pre>
    276      *  @Override
    277      *  public boolean dispatchKeyEvent(KeyEvent event) {
    278      *      return mDragController.dispatchKeyEvent(this, event)
    279      *              || super.dispatchKeyEvent(event);
    280      * </pre>
    281      */
    282     @SuppressWarnings({"UnusedDeclaration"})
    283     public boolean dispatchKeyEvent(KeyEvent event) {
    284         return mDragging;
    285     }
    286 
    287     /**
    288      * Stop dragging without dropping.
    289      */
    290     public void cancelDrag() {
    291         endDrag();
    292     }
    293 
    294     private void endDrag() {
    295         if (mDragging) {
    296             mDragging = false;
    297             if (mOriginator != null) {
    298                 mOriginator.setVisibility(View.VISIBLE);
    299             }
    300             if (mListener != null) {
    301                 mListener.onDragEnd();
    302             }
    303             if (mDragView != null) {
    304                 mDragView.remove();
    305                 mDragView = null;
    306             }
    307         }
    308     }
    309 
    310     /**
    311      * Call this from a drag source view.
    312      */
    313     public boolean onInterceptTouchEvent(MotionEvent ev) {
    314         if (false) {
    315             Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging="
    316                     + mDragging);
    317         }
    318         final int action = ev.getAction();
    319 
    320         if (action == MotionEvent.ACTION_DOWN) {
    321             recordScreenSize();
    322         }
    323 
    324         final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels);
    325         final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels);
    326 
    327         switch (action) {
    328             case MotionEvent.ACTION_MOVE:
    329                 break;
    330 
    331             case MotionEvent.ACTION_DOWN:
    332                 // Remember location of down touch
    333                 mMotionDownX = screenX;
    334                 mMotionDownY = screenY;
    335                 mLastDropTarget = null;
    336                 break;
    337 
    338             case MotionEvent.ACTION_CANCEL:
    339             case MotionEvent.ACTION_UP:
    340                 if (mDragging) {
    341                     drop(screenX, screenY);
    342                 }
    343                 endDrag();
    344                 break;
    345         }
    346 
    347         return mDragging;
    348     }
    349 
    350     /**
    351      * Sets the view that should handle move events.
    352      */
    353     void setMoveTarget(View view) {
    354         mMoveTarget = view;
    355     }
    356 
    357     public boolean dispatchUnhandledMove(View focused, int direction) {
    358         return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction);
    359     }
    360 
    361     /**
    362      * Call this from a drag source view.
    363      */
    364     public boolean onTouchEvent(MotionEvent ev) {
    365         View scrollView = mScrollView;
    366 
    367         if (!mDragging) {
    368             return false;
    369         }
    370 
    371         final int action = ev.getAction();
    372         final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels);
    373         final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels);
    374 
    375         switch (action) {
    376         case MotionEvent.ACTION_DOWN:
    377             // Remember where the motion event started
    378             mMotionDownX = screenX;
    379             mMotionDownY = screenY;
    380 
    381             if ((screenX < SCROLL_ZONE) || (screenX > scrollView.getWidth() - SCROLL_ZONE)) {
    382                 mScrollState = SCROLL_WAITING_IN_ZONE;
    383                 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
    384             } else {
    385                 mScrollState = SCROLL_OUTSIDE_ZONE;
    386             }
    387 
    388             break;
    389         case MotionEvent.ACTION_MOVE:
    390             // Update the drag view.  Don't use the clamped pos here so the dragging looks
    391             // like it goes off screen a little, intead of bumping up against the edge.
    392             mDragView.move((int)ev.getRawX(), (int)ev.getRawY());
    393 
    394             // Drop on someone?
    395             final int[] coordinates = mCoordinatesTemp;
    396             DropTarget dropTarget = findDropTarget(screenX, screenY, coordinates);
    397             if (dropTarget != null) {
    398                 if (mLastDropTarget == dropTarget) {
    399                     dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1],
    400                         (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
    401                 } else {
    402                     if (mLastDropTarget != null) {
    403                         mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1],
    404                             (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
    405                     }
    406                     dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1],
    407                         (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
    408                 }
    409             } else {
    410                 if (mLastDropTarget != null) {
    411                     mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1],
    412                         (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
    413                 }
    414             }
    415             mLastDropTarget = dropTarget;
    416 
    417             // Scroll, maybe, but not if we're in the delete region.
    418             boolean inDeleteRegion = false;
    419             if (mDeleteRegion != null) {
    420                 inDeleteRegion = mDeleteRegion.contains(screenX, screenY);
    421             }
    422             if (!inDeleteRegion && screenX < SCROLL_ZONE) {
    423                 if (mScrollState == SCROLL_OUTSIDE_ZONE) {
    424                     mScrollState = SCROLL_WAITING_IN_ZONE;
    425                     mScrollRunnable.setDirection(SCROLL_LEFT);
    426                     mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
    427                 }
    428             } else if (!inDeleteRegion && screenX > scrollView.getWidth() - SCROLL_ZONE) {
    429                 if (mScrollState == SCROLL_OUTSIDE_ZONE) {
    430                     mScrollState = SCROLL_WAITING_IN_ZONE;
    431                     mScrollRunnable.setDirection(SCROLL_RIGHT);
    432                     mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
    433                 }
    434             } else {
    435                 if (mScrollState == SCROLL_WAITING_IN_ZONE) {
    436                     mScrollState = SCROLL_OUTSIDE_ZONE;
    437                     mScrollRunnable.setDirection(SCROLL_RIGHT);
    438                     mHandler.removeCallbacks(mScrollRunnable);
    439                 }
    440             }
    441 
    442             break;
    443         case MotionEvent.ACTION_UP:
    444             mHandler.removeCallbacks(mScrollRunnable);
    445             if (mDragging) {
    446                 drop(screenX, screenY);
    447             }
    448             endDrag();
    449 
    450             break;
    451         case MotionEvent.ACTION_CANCEL:
    452             cancelDrag();
    453         }
    454 
    455         return true;
    456     }
    457 
    458     private boolean drop(float x, float y) {
    459         final int[] coordinates = mCoordinatesTemp;
    460         DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
    461 
    462         if (dropTarget != null) {
    463             dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1],
    464                     (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
    465             if (dropTarget.acceptDrop(mDragSource, coordinates[0], coordinates[1],
    466                     (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo)) {
    467                 dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1],
    468                         (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
    469                 mDragSource.onDropCompleted((View) dropTarget, true);
    470                 return true;
    471             } else {
    472                 mDragSource.onDropCompleted((View) dropTarget, false);
    473                 return true;
    474             }
    475         }
    476         return false;
    477     }
    478 
    479     private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
    480         final Rect r = mRectTemp;
    481 
    482         final ArrayList<DropTarget> dropTargets = mDropTargets;
    483         final int count = dropTargets.size();
    484         for (int i=count-1; i>=0; i--) {
    485             final DropTarget target = dropTargets.get(i);
    486             target.getHitRect(r);
    487             target.getLocationOnScreen(dropCoordinates);
    488             r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop());
    489             if (r.contains(x, y)) {
    490                 dropCoordinates[0] = x - dropCoordinates[0];
    491                 dropCoordinates[1] = y - dropCoordinates[1];
    492                 return target;
    493             }
    494         }
    495         return null;
    496     }
    497 
    498     /**
    499      * Get the screen size so we can clamp events to the screen size so even if
    500      * you drag off the edge of the screen, we find something.
    501      */
    502     private void recordScreenSize() {
    503         ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE))
    504                 .getDefaultDisplay().getMetrics(mDisplayMetrics);
    505     }
    506 
    507     /**
    508      * Clamp val to be &gt;= min and &lt; max.
    509      */
    510     private static int clamp(int val, int min, int max) {
    511         if (val < min) {
    512             return min;
    513         } else if (val >= max) {
    514             return max - 1;
    515         } else {
    516             return val;
    517         }
    518     }
    519 
    520     public void setDragScoller(DragScroller scroller) {
    521         mDragScroller = scroller;
    522     }
    523 
    524     public void setWindowToken(IBinder token) {
    525         mWindowToken = token;
    526     }
    527 
    528     /**
    529      * Sets the drag listner which will be notified when a drag starts or ends.
    530      */
    531     public void setDragListener(DragListener l) {
    532         mListener = l;
    533     }
    534 
    535     /**
    536      * Remove a previously installed drag listener.
    537      */
    538     public void removeDragListener(DragListener l) {
    539         mListener = null;
    540     }
    541 
    542     /**
    543      * Add a DropTarget to the list of potential places to receive drop events.
    544      */
    545     public void addDropTarget(DropTarget target) {
    546         mDropTargets.add(target);
    547     }
    548 
    549     /**
    550      * Don't send drop events to <em>target</em> any more.
    551      */
    552     public void removeDropTarget(DropTarget target) {
    553         mDropTargets.remove(target);
    554     }
    555 
    556     /**
    557      * Set which view scrolls for touch events near the edge of the screen.
    558      */
    559     public void setScrollView(View v) {
    560         mScrollView = v;
    561     }
    562 
    563     /**
    564      * Specifies the delete region.  We won't scroll on touch events over the delete region.
    565      *
    566      * @param region The rectangle in screen coordinates of the delete region.
    567      */
    568     void setDeleteRegion(RectF region) {
    569         mDeleteRegion = region;
    570     }
    571 
    572     private class ScrollRunnable implements Runnable {
    573         private int mDirection;
    574 
    575         ScrollRunnable() {
    576         }
    577 
    578         public void run() {
    579             if (mDragScroller != null) {
    580                 if (mDirection == SCROLL_LEFT) {
    581                     mDragScroller.scrollLeft();
    582                 } else {
    583                     mDragScroller.scrollRight();
    584                 }
    585                 mScrollState = SCROLL_OUTSIDE_ZONE;
    586             }
    587         }
    588 
    589         void setDirection(int direction) {
    590             mDirection = direction;
    591         }
    592     }
    593 }
    594