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