DragController.java revision 36cc09b07b19198f4ea886583cef462ade27192c
1/* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.launcher2; 18 19import android.content.Context; 20import android.graphics.Bitmap; 21import android.graphics.Rect; 22import android.graphics.RectF; 23import android.os.Handler; 24import android.os.IBinder; 25import android.os.Vibrator; 26import android.util.Log; 27import android.view.KeyEvent; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.ViewConfiguration; 31import android.view.inputmethod.InputMethodManager; 32 33import com.android.launcher.R; 34 35import java.util.ArrayList; 36 37/** 38 * Class for initiating a drag within a view or across multiple views. 39 */ 40public class DragController { 41 @SuppressWarnings({"UnusedDeclaration"}) 42 private static final String TAG = "Launcher.DragController"; 43 44 /** Indicates the drag is a move. */ 45 public static int DRAG_ACTION_MOVE = 0; 46 47 /** Indicates the drag is a copy. */ 48 public static int DRAG_ACTION_COPY = 1; 49 50 private static final int SCROLL_DELAY = 600; 51 private static final int VIBRATE_DURATION = 35; 52 53 private static final boolean PROFILE_DRAWING_DURING_DRAG = false; 54 55 private static final int SCROLL_OUTSIDE_ZONE = 0; 56 private static final int SCROLL_WAITING_IN_ZONE = 1; 57 58 static final int SCROLL_NONE = -1; 59 static final int SCROLL_LEFT = 0; 60 static final int SCROLL_RIGHT = 1; 61 62 private Launcher mLauncher; 63 private Handler mHandler; 64 private final Vibrator mVibrator = new Vibrator(); 65 66 // temporaries to avoid gc thrash 67 private Rect mRectTemp = new Rect(); 68 private final int[] mCoordinatesTemp = new int[2]; 69 70 /** Whether or not we're dragging. */ 71 private boolean mDragging; 72 73 /** X coordinate of the down event. */ 74 private int mMotionDownX; 75 76 /** Y coordinate of the down event. */ 77 private int mMotionDownY; 78 79 /** the area at the edge of the screen that makes the workspace go left 80 * or right while you're dragging. 81 */ 82 private int mScrollZone; 83 84 private DropTarget.DragObject mDragObject; 85 86 /** Who can receive drop events */ 87 private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>(); 88 89 private ArrayList<DragListener> mListeners = new ArrayList<DragListener>(); 90 91 /** The window token used as the parent for the DragView. */ 92 private IBinder mWindowToken; 93 94 /** The view that will be scrolled when dragging to the left and right edges of the screen. */ 95 private View mScrollView; 96 97 private View mMoveTarget; 98 99 private DragScroller mDragScroller; 100 private int mScrollState = SCROLL_OUTSIDE_ZONE; 101 private ScrollRunnable mScrollRunnable = new ScrollRunnable(); 102 103 private RectF mDeleteRegion; 104 private DropTarget mLastDropTarget; 105 106 private InputMethodManager mInputMethodManager; 107 108 private int mLastTouch[] = new int[2]; 109 private int mDistanceSinceScroll = 0; 110 111 private int mTmpPoint[] = new int[2]; 112 private Rect mDragLayerRect = new Rect(); 113 114 /** 115 * Interface to receive notifications when a drag starts or stops 116 */ 117 interface DragListener { 118 119 /** 120 * A drag has begun 121 * 122 * @param source An object representing where the drag originated 123 * @param info The data associated with the object that is being dragged 124 * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE} 125 * or {@link DragController#DRAG_ACTION_COPY} 126 */ 127 void onDragStart(DragSource source, Object info, int dragAction); 128 129 /** 130 * The drag has ended 131 */ 132 void onDragEnd(); 133 } 134 135 /** 136 * Used to create a new DragLayer from XML. 137 * 138 * @param context The application's context. 139 */ 140 public DragController(Launcher launcher) { 141 mLauncher = launcher; 142 mHandler = new Handler(); 143 mScrollZone = launcher.getResources().getDimensionPixelSize(R.dimen.scroll_zone); 144 } 145 146 public boolean dragging() { 147 return mDragging; 148 } 149 150 /** 151 * Starts a drag. 152 * 153 * @param v The view that is being dragged 154 * @param source An object representing where the drag originated 155 * @param dragInfo The data associated with the object that is being dragged 156 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 157 * {@link #DRAG_ACTION_COPY} 158 */ 159 public void startDrag(View v, DragSource source, Object dragInfo, int dragAction) { 160 startDrag(v, source, dragInfo, dragAction, null); 161 } 162 163 /** 164 * Starts a drag. 165 * 166 * @param v The view that is being dragged 167 * @param source An object representing where the drag originated 168 * @param dragInfo The data associated with the object that is being dragged 169 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 170 * {@link #DRAG_ACTION_COPY} 171 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 172 * Makes dragging feel more precise, e.g. you can clip out a transparent border 173 */ 174 public void startDrag(View v, DragSource source, Object dragInfo, int dragAction, 175 Rect dragRegion) { 176 Bitmap b = getViewBitmap(v); 177 178 if (b == null) { 179 // out of memory? 180 return; 181 } 182 183 int[] loc = mCoordinatesTemp; 184 mLauncher.getDragLayer().getLocationInDragLayer(v, loc); 185 int dragLayerX = loc[0]; 186 int dragLayerY = loc[1]; 187 188 startDrag(b, dragLayerX, dragLayerY, source, dragInfo, dragAction, dragRegion); 189 b.recycle(); 190 191 if (dragAction == DRAG_ACTION_MOVE) { 192 v.setVisibility(View.GONE); 193 } 194 } 195 196 /** 197 * Starts a drag. 198 * 199 * @param v The view that is being dragged 200 * @param bmp The bitmap that represents the view being dragged 201 * @param source An object representing where the drag originated 202 * @param dragInfo The data associated with the object that is being dragged 203 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 204 * {@link #DRAG_ACTION_COPY} 205 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 206 * Makes dragging feel more precise, e.g. you can clip out a transparent border 207 */ 208 public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction, 209 Rect dragRegion) { 210 int[] loc = mCoordinatesTemp; 211 mLauncher.getDragLayer().getLocationInDragLayer(v, loc); 212 int dragLayerX = loc[0]; 213 int dragLayerY = loc[1]; 214 215 startDrag(bmp, dragLayerX, dragLayerY, source, dragInfo, dragAction, dragRegion); 216 217 if (dragAction == DRAG_ACTION_MOVE) { 218 v.setVisibility(View.GONE); 219 } 220 } 221 222 /** 223 * Starts a drag. 224 * 225 * @param b The bitmap to display as the drag image. It will be re-scaled to the 226 * enlarged size. 227 * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. 228 * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. 229 * @param source An object representing where the drag originated 230 * @param dragInfo The data associated with the object that is being dragged 231 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 232 * {@link #DRAG_ACTION_COPY} 233 */ 234 public void startDrag(Bitmap b, int dragLayerX, int dragLayerY, 235 DragSource source, Object dragInfo, int dragAction) { 236 startDrag(b, dragLayerX, dragLayerY, source, dragInfo, dragAction, null); 237 } 238 239 /** 240 * Starts a drag. 241 * 242 * @param b The bitmap to display as the drag image. It will be re-scaled to the 243 * enlarged size. 244 * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. 245 * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. 246 * @param source An object representing where the drag originated 247 * @param dragInfo The data associated with the object that is being dragged 248 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 249 * {@link #DRAG_ACTION_COPY} 250 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 251 * Makes dragging feel more precise, e.g. you can clip out a transparent border 252 */ 253 public void startDrag(Bitmap b, int dragLayerX, int dragLayerY, 254 DragSource source, Object dragInfo, int dragAction, Rect dragRegion) { 255 if (PROFILE_DRAWING_DURING_DRAG) { 256 android.os.Debug.startMethodTracing("Launcher"); 257 } 258 259 // Hide soft keyboard, if visible 260 if (mInputMethodManager == null) { 261 mInputMethodManager = (InputMethodManager) 262 mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE); 263 } 264 mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); 265 266 for (DragListener listener : mListeners) { 267 listener.onDragStart(source, dragInfo, dragAction); 268 } 269 270 final int registrationX = mMotionDownX - dragLayerX; 271 final int registrationY = mMotionDownY - dragLayerY; 272 273 final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; 274 final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; 275 276 mDragging = true; 277 278 mDragObject = new DropTarget.DragObject(); 279 280 mDragObject.dragComplete = false; 281 mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); 282 mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); 283 mDragObject.dragSource = source; 284 mDragObject.dragInfo = dragInfo; 285 286 mVibrator.vibrate(VIBRATE_DURATION); 287 288 final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, 289 registrationY, 0, 0, b.getWidth(), b.getHeight()); 290 291 if (dragRegion != null) { 292 dragView.setDragRegion(new Rect(dragRegion)); 293 } 294 295 dragView.show(mMotionDownX, mMotionDownY); 296 handleMoveEvent(mMotionDownX, mMotionDownY); 297 } 298 299 /** 300 * Draw the view into a bitmap. 301 */ 302 Bitmap getViewBitmap(View v) { 303 v.clearFocus(); 304 v.setPressed(false); 305 306 boolean willNotCache = v.willNotCacheDrawing(); 307 v.setWillNotCacheDrawing(false); 308 309 // Reset the drawing cache background color to fully transparent 310 // for the duration of this operation 311 int color = v.getDrawingCacheBackgroundColor(); 312 v.setDrawingCacheBackgroundColor(0); 313 float alpha = v.getAlpha(); 314 v.setAlpha(1.0f); 315 316 if (color != 0) { 317 v.destroyDrawingCache(); 318 } 319 v.buildDrawingCache(); 320 Bitmap cacheBitmap = v.getDrawingCache(); 321 if (cacheBitmap == null) { 322 Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); 323 return null; 324 } 325 326 Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); 327 328 // Restore the view 329 v.destroyDrawingCache(); 330 v.setAlpha(alpha); 331 v.setWillNotCacheDrawing(willNotCache); 332 v.setDrawingCacheBackgroundColor(color); 333 334 return bitmap; 335 } 336 337 /** 338 * Call this from a drag source view like this: 339 * 340 * <pre> 341 * @Override 342 * public boolean dispatchKeyEvent(KeyEvent event) { 343 * return mDragController.dispatchKeyEvent(this, event) 344 * || super.dispatchKeyEvent(event); 345 * </pre> 346 */ 347 @SuppressWarnings({"UnusedDeclaration"}) 348 public boolean dispatchKeyEvent(KeyEvent event) { 349 return mDragging; 350 } 351 352 public boolean isDragging() { 353 return mDragging; 354 } 355 356 /** 357 * Stop dragging without dropping. 358 */ 359 public void cancelDrag() { 360 if (mDragging) { 361 if (mLastDropTarget != null) { 362 mLastDropTarget.onDragExit(mDragObject); 363 } 364 mDragObject.cancelled = true; 365 mDragObject.dragComplete = true; 366 mDragObject.dragSource.onDropCompleted(null, mDragObject, false); 367 } 368 endDrag(); 369 } 370 371 private void endDrag() { 372 if (mDragging) { 373 mDragging = false; 374 for (DragListener listener : mListeners) { 375 listener.onDragEnd(); 376 } 377 if (mDragObject.dragView != null) { 378 mDragObject.dragView.remove(); 379 mDragObject.dragView = null; 380 } 381 } 382 } 383 384 /** 385 * Clamps the position to the drag layer bounds. 386 */ 387 private int[] getClampedDragLayerPos(float x, float y) { 388 mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect); 389 mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1)); 390 mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1)); 391 return mTmpPoint; 392 } 393 394 /** 395 * Call this from a drag source view. 396 */ 397 public boolean onInterceptTouchEvent(MotionEvent ev) { 398 if (false) { 399 Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" 400 + mDragging); 401 } 402 final int action = ev.getAction(); 403 404 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 405 final int dragLayerX = dragLayerPos[0]; 406 final int dragLayerY = dragLayerPos[1]; 407 408 switch (action) { 409 case MotionEvent.ACTION_MOVE: 410 break; 411 case MotionEvent.ACTION_DOWN: 412 // Remember location of down touch 413 mMotionDownX = dragLayerX; 414 mMotionDownY = dragLayerY; 415 mLastDropTarget = null; 416 break; 417 case MotionEvent.ACTION_UP: 418 if (mDragging) { 419 drop(dragLayerX, dragLayerY); 420 } 421 endDrag(); 422 break; 423 case MotionEvent.ACTION_CANCEL: 424 cancelDrag(); 425 break; 426 } 427 428 return mDragging; 429 } 430 431 /** 432 * Sets the view that should handle move events. 433 */ 434 void setMoveTarget(View view) { 435 mMoveTarget = view; 436 } 437 438 public boolean dispatchUnhandledMove(View focused, int direction) { 439 return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); 440 } 441 442 private void handleMoveEvent(int x, int y) { 443 mDragObject.dragView.move(x, y); 444 445 // Drop on someone? 446 final int[] coordinates = mCoordinatesTemp; 447 DropTarget dropTarget = findDropTarget(x, y, coordinates); 448 mDragObject.x = coordinates[0]; 449 mDragObject.y = coordinates[1]; 450 if (dropTarget != null) { 451 DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject); 452 if (delegate != null) { 453 dropTarget = delegate; 454 } 455 456 if (mLastDropTarget != dropTarget) { 457 if (mLastDropTarget != null) { 458 mLastDropTarget.onDragExit(mDragObject); 459 } 460 dropTarget.onDragEnter(mDragObject); 461 } 462 dropTarget.onDragOver(mDragObject); 463 } else { 464 if (mLastDropTarget != null) { 465 mLastDropTarget.onDragExit(mDragObject); 466 } 467 } 468 mLastDropTarget = dropTarget; 469 470 // Scroll, maybe, but not if we're in the delete region. 471 boolean inDeleteRegion = false; 472 if (mDeleteRegion != null) { 473 inDeleteRegion = mDeleteRegion.contains(x, y); 474 } 475 476 // After a scroll, the touch point will still be in the scroll region. 477 // Rather than scrolling immediately, require a bit of twiddling to scroll again 478 final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop(); 479 mDistanceSinceScroll += 480 Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); 481 mLastTouch[0] = x; 482 mLastTouch[1] = y; 483 484 if (!inDeleteRegion && x < mScrollZone) { 485 if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { 486 mScrollState = SCROLL_WAITING_IN_ZONE; 487 mScrollRunnable.setDirection(SCROLL_LEFT); 488 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 489 mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT); 490 } 491 } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) { 492 if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { 493 mScrollState = SCROLL_WAITING_IN_ZONE; 494 mScrollRunnable.setDirection(SCROLL_RIGHT); 495 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 496 mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT); 497 } 498 } else { 499 if (mScrollState == SCROLL_WAITING_IN_ZONE) { 500 mScrollState = SCROLL_OUTSIDE_ZONE; 501 mScrollRunnable.setDirection(SCROLL_RIGHT); 502 mHandler.removeCallbacks(mScrollRunnable); 503 mDragScroller.onExitScrollArea(); 504 } 505 } 506 } 507 508 /** 509 * Call this from a drag source view. 510 */ 511 public boolean onTouchEvent(MotionEvent ev) { 512 if (!mDragging) { 513 return false; 514 } 515 516 final int action = ev.getAction(); 517 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 518 final int dragLayerX = dragLayerPos[0]; 519 final int dragLayerY = dragLayerPos[1]; 520 521 switch (action) { 522 case MotionEvent.ACTION_DOWN: 523 // Remember where the motion event started 524 mMotionDownX = dragLayerX; 525 mMotionDownY = dragLayerY; 526 527 if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) { 528 mScrollState = SCROLL_WAITING_IN_ZONE; 529 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 530 } else { 531 mScrollState = SCROLL_OUTSIDE_ZONE; 532 } 533 break; 534 case MotionEvent.ACTION_MOVE: 535 handleMoveEvent(dragLayerX, dragLayerY); 536 break; 537 case MotionEvent.ACTION_UP: 538 // Ensure that we've processed a move event at the current pointer location. 539 handleMoveEvent(dragLayerX, dragLayerY); 540 541 mHandler.removeCallbacks(mScrollRunnable); 542 if (mDragging) { 543 drop(dragLayerX, dragLayerY); 544 } 545 endDrag(); 546 break; 547 case MotionEvent.ACTION_CANCEL: 548 cancelDrag(); 549 break; 550 } 551 552 return true; 553 } 554 555 private void drop(float x, float y) { 556 final int[] coordinates = mCoordinatesTemp; 557 final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); 558 559 mDragObject.x = coordinates[0]; 560 mDragObject.y = coordinates[1]; 561 boolean accepted = false; 562 if (dropTarget != null) { 563 mDragObject.dragComplete = true; 564 dropTarget.onDragExit(mDragObject); 565 if (dropTarget.acceptDrop(mDragObject)) { 566 dropTarget.onDrop(mDragObject); 567 accepted = true; 568 } 569 } 570 mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, accepted); 571 } 572 573 private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 574 final Rect r = mRectTemp; 575 576 final ArrayList<DropTarget> dropTargets = mDropTargets; 577 final int count = dropTargets.size(); 578 for (int i=count-1; i>=0; i--) { 579 DropTarget target = dropTargets.get(i); 580 if (!target.isDropEnabled()) 581 continue; 582 583 target.getHitRect(r); 584 585 // Convert the hit rect to DragLayer coordinates 586 target.getLocationInDragLayer(dropCoordinates); 587 r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); 588 589 mDragObject.x = x; 590 mDragObject.y = y; 591 if (r.contains(x, y)) { 592 DropTarget delegate = target.getDropTargetDelegate(mDragObject); 593 if (delegate != null) { 594 target = delegate; 595 target.getLocationInDragLayer(dropCoordinates); 596 } 597 598 // Make dropCoordinates relative to the DropTarget 599 dropCoordinates[0] = x - dropCoordinates[0]; 600 dropCoordinates[1] = y - dropCoordinates[1]; 601 602 return target; 603 } 604 } 605 return null; 606 } 607 608 public void setDragScoller(DragScroller scroller) { 609 mDragScroller = scroller; 610 } 611 612 public void setWindowToken(IBinder token) { 613 mWindowToken = token; 614 } 615 616 /** 617 * Sets the drag listner which will be notified when a drag starts or ends. 618 */ 619 public void addDragListener(DragListener l) { 620 mListeners.add(l); 621 } 622 623 /** 624 * Remove a previously installed drag listener. 625 */ 626 public void removeDragListener(DragListener l) { 627 mListeners.remove(l); 628 } 629 630 /** 631 * Add a DropTarget to the list of potential places to receive drop events. 632 */ 633 public void addDropTarget(DropTarget target) { 634 mDropTargets.add(target); 635 } 636 637 /** 638 * Don't send drop events to <em>target</em> any more. 639 */ 640 public void removeDropTarget(DropTarget target) { 641 mDropTargets.remove(target); 642 } 643 644 /** 645 * Set which view scrolls for touch events near the edge of the screen. 646 */ 647 public void setScrollView(View v) { 648 mScrollView = v; 649 } 650 651 /** 652 * Specifies the delete region. We won't scroll on touch events over the delete region. 653 * 654 * @param region The rectangle in DragLayer coordinates of the delete region. 655 */ 656 void setDeleteRegion(RectF region) { 657 mDeleteRegion = region; 658 } 659 660 DragView getDragView() { 661 return mDragObject.dragView; 662 } 663 664 private class ScrollRunnable implements Runnable { 665 private int mDirection; 666 667 ScrollRunnable() { 668 } 669 670 public void run() { 671 if (mDragScroller != null) { 672 if (mDirection == SCROLL_LEFT) { 673 mDragScroller.scrollLeft(); 674 } else { 675 mDragScroller.scrollRight(); 676 } 677 mScrollState = SCROLL_OUTSIDE_ZONE; 678 mDistanceSinceScroll = 0; 679 mDragScroller.onExitScrollArea(); 680 } 681 } 682 683 void setDirection(int direction) { 684 mDirection = direction; 685 } 686 } 687} 688