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