DragController.java revision c57b7a8233d32c6dd8de5057c570afe3f50e3ef2
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.dragComplete = true; 365 mDragObject.dragSource.onDropCompleted(null, mDragObject, false); 366 } 367 endDrag(); 368 } 369 370 private void endDrag() { 371 if (mDragging) { 372 mDragging = false; 373 for (DragListener listener : mListeners) { 374 listener.onDragEnd(); 375 } 376 if (mDragObject.dragView != null) { 377 mDragObject.dragView.remove(); 378 mDragObject.dragView = null; 379 } 380 } 381 } 382 383 /** 384 * Clamps the position to the drag layer bounds. 385 */ 386 private int[] getClampedDragLayerPos(float x, float y) { 387 mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect); 388 mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1)); 389 mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1)); 390 return mTmpPoint; 391 } 392 393 /** 394 * Call this from a drag source view. 395 */ 396 public boolean onInterceptTouchEvent(MotionEvent ev) { 397 if (false) { 398 Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" 399 + mDragging); 400 } 401 final int action = ev.getAction(); 402 403 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 404 final int dragLayerX = dragLayerPos[0]; 405 final int dragLayerY = dragLayerPos[1]; 406 407 switch (action) { 408 case MotionEvent.ACTION_MOVE: 409 break; 410 case MotionEvent.ACTION_DOWN: 411 // Remember location of down touch 412 mMotionDownX = dragLayerX; 413 mMotionDownY = dragLayerY; 414 mLastDropTarget = null; 415 break; 416 case MotionEvent.ACTION_UP: 417 if (mDragging) { 418 drop(dragLayerX, dragLayerY); 419 } 420 endDrag(); 421 break; 422 case MotionEvent.ACTION_CANCEL: 423 cancelDrag(); 424 break; 425 } 426 427 return mDragging; 428 } 429 430 /** 431 * Sets the view that should handle move events. 432 */ 433 void setMoveTarget(View view) { 434 mMoveTarget = view; 435 } 436 437 public boolean dispatchUnhandledMove(View focused, int direction) { 438 return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); 439 } 440 441 private void handleMoveEvent(int x, int y) { 442 mDragObject.dragView.move(x, y); 443 444 // Drop on someone? 445 final int[] coordinates = mCoordinatesTemp; 446 DropTarget dropTarget = findDropTarget(x, y, coordinates); 447 mDragObject.x = coordinates[0]; 448 mDragObject.y = coordinates[1]; 449 if (dropTarget != null) { 450 DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject); 451 if (delegate != null) { 452 dropTarget = delegate; 453 } 454 455 if (mLastDropTarget != dropTarget) { 456 if (mLastDropTarget != null) { 457 mLastDropTarget.onDragExit(mDragObject); 458 } 459 dropTarget.onDragEnter(mDragObject); 460 } 461 dropTarget.onDragOver(mDragObject); 462 } else { 463 if (mLastDropTarget != null) { 464 mLastDropTarget.onDragExit(mDragObject); 465 } 466 } 467 mLastDropTarget = dropTarget; 468 469 // Scroll, maybe, but not if we're in the delete region. 470 boolean inDeleteRegion = false; 471 if (mDeleteRegion != null) { 472 inDeleteRegion = mDeleteRegion.contains(x, y); 473 } 474 475 // After a scroll, the touch point will still be in the scroll region. 476 // Rather than scrolling immediately, require a bit of twiddling to scroll again 477 final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop(); 478 mDistanceSinceScroll += 479 Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); 480 mLastTouch[0] = x; 481 mLastTouch[1] = y; 482 483 if (!inDeleteRegion && x < mScrollZone) { 484 if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { 485 mScrollState = SCROLL_WAITING_IN_ZONE; 486 mScrollRunnable.setDirection(SCROLL_LEFT); 487 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 488 mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT); 489 } 490 } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) { 491 if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { 492 mScrollState = SCROLL_WAITING_IN_ZONE; 493 mScrollRunnable.setDirection(SCROLL_RIGHT); 494 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 495 mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT); 496 } 497 } else { 498 if (mScrollState == SCROLL_WAITING_IN_ZONE) { 499 mScrollState = SCROLL_OUTSIDE_ZONE; 500 mScrollRunnable.setDirection(SCROLL_RIGHT); 501 mHandler.removeCallbacks(mScrollRunnable); 502 mDragScroller.onExitScrollArea(); 503 } 504 } 505 } 506 507 /** 508 * Call this from a drag source view. 509 */ 510 public boolean onTouchEvent(MotionEvent ev) { 511 if (!mDragging) { 512 return false; 513 } 514 515 final int action = ev.getAction(); 516 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 517 final int dragLayerX = dragLayerPos[0]; 518 final int dragLayerY = dragLayerPos[1]; 519 520 switch (action) { 521 case MotionEvent.ACTION_DOWN: 522 // Remember where the motion event started 523 mMotionDownX = dragLayerX; 524 mMotionDownY = dragLayerY; 525 526 if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) { 527 mScrollState = SCROLL_WAITING_IN_ZONE; 528 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 529 } else { 530 mScrollState = SCROLL_OUTSIDE_ZONE; 531 } 532 break; 533 case MotionEvent.ACTION_MOVE: 534 handleMoveEvent(dragLayerX, dragLayerY); 535 break; 536 case MotionEvent.ACTION_UP: 537 // Ensure that we've processed a move event at the current pointer location. 538 handleMoveEvent(dragLayerX, dragLayerY); 539 540 mHandler.removeCallbacks(mScrollRunnable); 541 if (mDragging) { 542 drop(dragLayerX, dragLayerY); 543 } 544 endDrag(); 545 break; 546 case MotionEvent.ACTION_CANCEL: 547 cancelDrag(); 548 break; 549 } 550 551 return true; 552 } 553 554 private void drop(float x, float y) { 555 final int[] coordinates = mCoordinatesTemp; 556 final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); 557 558 mDragObject.x = coordinates[0]; 559 mDragObject.y = coordinates[1]; 560 boolean accepted = false; 561 if (dropTarget != null) { 562 mDragObject.dragComplete = true; 563 dropTarget.onDragExit(mDragObject); 564 if (dropTarget.acceptDrop(mDragObject)) { 565 dropTarget.onDrop(mDragObject); 566 accepted = true; 567 } 568 } 569 mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, accepted); 570 } 571 572 private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 573 final Rect r = mRectTemp; 574 575 final ArrayList<DropTarget> dropTargets = mDropTargets; 576 final int count = dropTargets.size(); 577 for (int i=count-1; i>=0; i--) { 578 DropTarget target = dropTargets.get(i); 579 if (!target.isDropEnabled()) 580 continue; 581 582 target.getHitRect(r); 583 584 // Convert the hit rect to DragLayer coordinates 585 target.getLocationInDragLayer(dropCoordinates); 586 r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); 587 588 mDragObject.x = x; 589 mDragObject.y = y; 590 if (r.contains(x, y)) { 591 DropTarget delegate = target.getDropTargetDelegate(mDragObject); 592 if (delegate != null) { 593 target = delegate; 594 target.getLocationInDragLayer(dropCoordinates); 595 } 596 597 // Make dropCoordinates relative to the DropTarget 598 dropCoordinates[0] = x - dropCoordinates[0]; 599 dropCoordinates[1] = y - dropCoordinates[1]; 600 601 return target; 602 } 603 } 604 return null; 605 } 606 607 public void setDragScoller(DragScroller scroller) { 608 mDragScroller = scroller; 609 } 610 611 public void setWindowToken(IBinder token) { 612 mWindowToken = token; 613 } 614 615 /** 616 * Sets the drag listner which will be notified when a drag starts or ends. 617 */ 618 public void addDragListener(DragListener l) { 619 mListeners.add(l); 620 } 621 622 /** 623 * Remove a previously installed drag listener. 624 */ 625 public void removeDragListener(DragListener l) { 626 mListeners.remove(l); 627 } 628 629 /** 630 * Add a DropTarget to the list of potential places to receive drop events. 631 */ 632 public void addDropTarget(DropTarget target) { 633 mDropTargets.add(target); 634 } 635 636 /** 637 * Don't send drop events to <em>target</em> any more. 638 */ 639 public void removeDropTarget(DropTarget target) { 640 mDropTargets.remove(target); 641 } 642 643 /** 644 * Set which view scrolls for touch events near the edge of the screen. 645 */ 646 public void setScrollView(View v) { 647 mScrollView = v; 648 } 649 650 /** 651 * Specifies the delete region. We won't scroll on touch events over the delete region. 652 * 653 * @param region The rectangle in DragLayer coordinates of the delete region. 654 */ 655 void setDeleteRegion(RectF region) { 656 mDeleteRegion = region; 657 } 658 659 DragView getDragView() { 660 return mDragObject.dragView; 661 } 662 663 private class ScrollRunnable implements Runnable { 664 private int mDirection; 665 666 ScrollRunnable() { 667 } 668 669 public void run() { 670 if (mDragScroller != null) { 671 if (mDirection == SCROLL_LEFT) { 672 mDragScroller.scrollLeft(); 673 } else { 674 mDragScroller.scrollRight(); 675 } 676 mScrollState = SCROLL_OUTSIDE_ZONE; 677 mDistanceSinceScroll = 0; 678 mDragScroller.onExitScrollArea(); 679 } 680 } 681 682 void setDirection(int direction) { 683 mDirection = direction; 684 } 685 } 686} 687