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 */ onDragStart(DragSource source, Object info, int dragAction)140 void onDragStart(DragSource source, Object info, int dragAction); 141 142 /** 143 * The drag has eneded 144 */ onDragEnd()145 void onDragEnd(); 146 } 147 148 /** 149 * Used to create a new DragLayer from XML. 150 * 151 * @param context The application's context. 152 */ DragController(Context context)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 */ startDrag(View v, DragSource source, Object dragInfo, int dragAction)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 */ startDrag(Bitmap b, int screenX, int screenY, int textureLeft, int textureTop, int textureWidth, int textureHeight, DragSource source, Object dragInfo, int dragAction)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 */ getViewBitmap(View v)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"}) dispatchKeyEvent(KeyEvent event)290 public boolean dispatchKeyEvent(KeyEvent event) { 291 return mDragging; 292 } 293 294 /** 295 * Stop dragging without dropping. 296 */ cancelDrag()297 public void cancelDrag() { 298 endDrag(); 299 } 300 endDrag()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 */ onInterceptTouchEvent(MotionEvent ev)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 */ setMoveTarget(View view)360 void setMoveTarget(View view) { 361 mMoveTarget = view; 362 } 363 dispatchUnhandledMove(View focused, int direction)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 */ onTouchEvent(MotionEvent ev)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 drop(float x, float y)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 findDropTarget(int x, int y, int[] dropCoordinates)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 */ recordScreenSize()511 private void recordScreenSize() { 512 ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) 513 .getDefaultDisplay().getMetrics(mDisplayMetrics); 514 } 515 516 /** 517 * Clamp val to be >= min and < max. 518 */ clamp(int val, int min, int max)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 setDragScoller(DragScroller scroller)529 public void setDragScoller(DragScroller scroller) { 530 mDragScroller = scroller; 531 } 532 setWindowToken(IBinder token)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 */ setDragListener(DragListener l)540 public void setDragListener(DragListener l) { 541 mListener = l; 542 } 543 544 /** 545 * Remove a previously installed drag listener. 546 */ removeDragListener(DragListener l)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 */ addDropTarget(DropTarget target)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 */ removeDropTarget(DropTarget target)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 */ setScrollView(View v)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 */ setDeleteRegion(RectF region)577 void setDeleteRegion(RectF region) { 578 mDeleteRegion = region; 579 } 580 581 private class ScrollRunnable implements Runnable { 582 private int mDirection; 583 ScrollRunnable()584 ScrollRunnable() { 585 } 586 run()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 setDirection(int direction)598 void setDirection(int direction) { 599 mDirection = direction; 600 } 601 } 602 } 603