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.Point; 22 import android.graphics.Rect; 23 import android.os.Handler; 24 import android.os.IBinder; 25 import android.os.Vibrator; 26 import android.util.Log; 27 import android.view.KeyEvent; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.ViewConfiguration; 31 import android.view.inputmethod.InputMethodManager; 32 33 import com.android.launcher.R; 34 35 import java.util.ArrayList; 36 37 /** 38 * Class for initiating a drag within a view or across multiple views. 39 */ 40 public 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 DropTarget mLastDropTarget; 104 105 private InputMethodManager mInputMethodManager; 106 107 private int mLastTouch[] = new int[2]; 108 private int mDistanceSinceScroll = 0; 109 110 private int mTmpPoint[] = new int[2]; 111 private Rect mDragLayerRect = new Rect(); 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, null, 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, null, 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, 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, Point dragOffset, 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 = new DropTarget.DragObject(); 278 279 mDragObject.dragComplete = false; 280 mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); 281 mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); 282 mDragObject.dragSource = source; 283 mDragObject.dragInfo = dragInfo; 284 285 mVibrator.vibrate(VIBRATE_DURATION); 286 287 final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, 288 registrationY, 0, 0, b.getWidth(), b.getHeight()); 289 290 if (dragOffset != null) { 291 dragView.setDragVisualizeOffset(new Point(dragOffset)); 292 } 293 if (dragRegion != null) { 294 dragView.setDragRegion(new Rect(dragRegion)); 295 } 296 297 dragView.show(mMotionDownX, mMotionDownY); 298 handleMoveEvent(mMotionDownX, mMotionDownY); 299 } 300 301 /** 302 * Draw the view into a bitmap. 303 */ 304 Bitmap getViewBitmap(View v) { 305 v.clearFocus(); 306 v.setPressed(false); 307 308 boolean willNotCache = v.willNotCacheDrawing(); 309 v.setWillNotCacheDrawing(false); 310 311 // Reset the drawing cache background color to fully transparent 312 // for the duration of this operation 313 int color = v.getDrawingCacheBackgroundColor(); 314 v.setDrawingCacheBackgroundColor(0); 315 float alpha = v.getAlpha(); 316 v.setAlpha(1.0f); 317 318 if (color != 0) { 319 v.destroyDrawingCache(); 320 } 321 v.buildDrawingCache(); 322 Bitmap cacheBitmap = v.getDrawingCache(); 323 if (cacheBitmap == null) { 324 Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); 325 return null; 326 } 327 328 Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); 329 330 // Restore the view 331 v.destroyDrawingCache(); 332 v.setAlpha(alpha); 333 v.setWillNotCacheDrawing(willNotCache); 334 v.setDrawingCacheBackgroundColor(color); 335 336 return bitmap; 337 } 338 339 /** 340 * Call this from a drag source view like this: 341 * 342 * <pre> 343 * @Override 344 * public boolean dispatchKeyEvent(KeyEvent event) { 345 * return mDragController.dispatchKeyEvent(this, event) 346 * || super.dispatchKeyEvent(event); 347 * </pre> 348 */ 349 @SuppressWarnings({"UnusedDeclaration"}) 350 public boolean dispatchKeyEvent(KeyEvent event) { 351 return mDragging; 352 } 353 354 public boolean isDragging() { 355 return mDragging; 356 } 357 358 /** 359 * Stop dragging without dropping. 360 */ 361 public void cancelDrag() { 362 if (mDragging) { 363 if (mLastDropTarget != null) { 364 mLastDropTarget.onDragExit(mDragObject); 365 } 366 mDragObject.cancelled = true; 367 mDragObject.dragComplete = true; 368 mDragObject.dragSource.onDropCompleted(null, mDragObject, false); 369 } 370 endDrag(); 371 } 372 public void onAppsRemoved(ArrayList<ApplicationInfo> apps, Context context) { 373 // Cancel the current drag if we are removing an app that we are dragging 374 if (mDragObject != null) { 375 Object rawDragInfo = mDragObject.dragInfo; 376 if (rawDragInfo instanceof ShortcutInfo) { 377 ShortcutInfo dragInfo = (ShortcutInfo) rawDragInfo; 378 for (ApplicationInfo info : apps) { 379 if (dragInfo.intent.getComponent().equals(info.intent.getComponent())) { 380 cancelDrag(); 381 return; 382 } 383 } 384 } 385 } 386 } 387 388 private void endDrag() { 389 if (mDragging) { 390 mDragging = false; 391 for (DragListener listener : mListeners) { 392 listener.onDragEnd(); 393 } 394 if (mDragObject.dragView != null) { 395 mDragObject.dragView.remove(); 396 mDragObject.dragView = null; 397 } 398 } 399 } 400 401 /** 402 * Clamps the position to the drag layer bounds. 403 */ 404 private int[] getClampedDragLayerPos(float x, float y) { 405 mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect); 406 mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1)); 407 mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1)); 408 return mTmpPoint; 409 } 410 411 /** 412 * Call this from a drag source view. 413 */ 414 public boolean onInterceptTouchEvent(MotionEvent ev) { 415 if (false) { 416 Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" 417 + mDragging); 418 } 419 final int action = ev.getAction(); 420 421 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 422 final int dragLayerX = dragLayerPos[0]; 423 final int dragLayerY = dragLayerPos[1]; 424 425 switch (action) { 426 case MotionEvent.ACTION_MOVE: 427 break; 428 case MotionEvent.ACTION_DOWN: 429 // Remember location of down touch 430 mMotionDownX = dragLayerX; 431 mMotionDownY = dragLayerY; 432 mLastDropTarget = null; 433 break; 434 case MotionEvent.ACTION_UP: 435 if (mDragging) { 436 drop(dragLayerX, dragLayerY); 437 } 438 endDrag(); 439 break; 440 case MotionEvent.ACTION_CANCEL: 441 cancelDrag(); 442 break; 443 } 444 445 return mDragging; 446 } 447 448 /** 449 * Sets the view that should handle move events. 450 */ 451 void setMoveTarget(View view) { 452 mMoveTarget = view; 453 } 454 455 public boolean dispatchUnhandledMove(View focused, int direction) { 456 return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); 457 } 458 459 private void handleMoveEvent(int x, int y) { 460 mDragObject.dragView.move(x, y); 461 462 // Drop on someone? 463 final int[] coordinates = mCoordinatesTemp; 464 DropTarget dropTarget = findDropTarget(x, y, coordinates); 465 mDragObject.x = coordinates[0]; 466 mDragObject.y = coordinates[1]; 467 if (dropTarget != null) { 468 DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject); 469 if (delegate != null) { 470 dropTarget = delegate; 471 } 472 473 if (mLastDropTarget != dropTarget) { 474 if (mLastDropTarget != null) { 475 mLastDropTarget.onDragExit(mDragObject); 476 } 477 dropTarget.onDragEnter(mDragObject); 478 } 479 dropTarget.onDragOver(mDragObject); 480 } else { 481 if (mLastDropTarget != null) { 482 mLastDropTarget.onDragExit(mDragObject); 483 } 484 } 485 mLastDropTarget = dropTarget; 486 487 // After a scroll, the touch point will still be in the scroll region. 488 // Rather than scrolling immediately, require a bit of twiddling to scroll again 489 final int slop = ViewConfiguration.get(mLauncher).getScaledWindowTouchSlop(); 490 mDistanceSinceScroll += 491 Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); 492 mLastTouch[0] = x; 493 mLastTouch[1] = y; 494 495 if (x < mScrollZone) { 496 if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { 497 mScrollState = SCROLL_WAITING_IN_ZONE; 498 if (mDragScroller.onEnterScrollArea(x, y, SCROLL_LEFT)) { 499 mScrollRunnable.setDirection(SCROLL_LEFT); 500 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 501 } 502 } 503 } else if (x > mScrollView.getWidth() - mScrollZone) { 504 if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { 505 mScrollState = SCROLL_WAITING_IN_ZONE; 506 if (mDragScroller.onEnterScrollArea(x, y, SCROLL_RIGHT)) { 507 mScrollRunnable.setDirection(SCROLL_RIGHT); 508 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 509 } 510 } 511 } else { 512 if (mScrollState == SCROLL_WAITING_IN_ZONE) { 513 mScrollState = SCROLL_OUTSIDE_ZONE; 514 mScrollRunnable.setDirection(SCROLL_RIGHT); 515 mHandler.removeCallbacks(mScrollRunnable); 516 mDragScroller.onExitScrollArea(); 517 } 518 } 519 } 520 521 /** 522 * Call this from a drag source view. 523 */ 524 public boolean onTouchEvent(MotionEvent ev) { 525 if (!mDragging) { 526 return false; 527 } 528 529 final int action = ev.getAction(); 530 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 531 final int dragLayerX = dragLayerPos[0]; 532 final int dragLayerY = dragLayerPos[1]; 533 534 switch (action) { 535 case MotionEvent.ACTION_DOWN: 536 // Remember where the motion event started 537 mMotionDownX = dragLayerX; 538 mMotionDownY = dragLayerY; 539 540 if ((dragLayerX < mScrollZone) || (dragLayerX > mScrollView.getWidth() - mScrollZone)) { 541 mScrollState = SCROLL_WAITING_IN_ZONE; 542 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 543 } else { 544 mScrollState = SCROLL_OUTSIDE_ZONE; 545 } 546 break; 547 case MotionEvent.ACTION_MOVE: 548 handleMoveEvent(dragLayerX, dragLayerY); 549 break; 550 case MotionEvent.ACTION_UP: 551 // Ensure that we've processed a move event at the current pointer location. 552 handleMoveEvent(dragLayerX, dragLayerY); 553 554 mHandler.removeCallbacks(mScrollRunnable); 555 if (mDragging) { 556 drop(dragLayerX, dragLayerY); 557 } 558 endDrag(); 559 break; 560 case MotionEvent.ACTION_CANCEL: 561 cancelDrag(); 562 break; 563 } 564 565 return true; 566 } 567 568 private void drop(float x, float y) { 569 final int[] coordinates = mCoordinatesTemp; 570 final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); 571 572 mDragObject.x = coordinates[0]; 573 mDragObject.y = coordinates[1]; 574 boolean accepted = false; 575 if (dropTarget != null) { 576 mDragObject.dragComplete = true; 577 dropTarget.onDragExit(mDragObject); 578 if (dropTarget.acceptDrop(mDragObject)) { 579 dropTarget.onDrop(mDragObject); 580 accepted = true; 581 } 582 } 583 mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, accepted); 584 } 585 586 private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 587 final Rect r = mRectTemp; 588 589 final ArrayList<DropTarget> dropTargets = mDropTargets; 590 final int count = dropTargets.size(); 591 for (int i=count-1; i>=0; i--) { 592 DropTarget target = dropTargets.get(i); 593 if (!target.isDropEnabled()) 594 continue; 595 596 target.getHitRect(r); 597 598 // Convert the hit rect to DragLayer coordinates 599 target.getLocationInDragLayer(dropCoordinates); 600 r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); 601 602 mDragObject.x = x; 603 mDragObject.y = y; 604 if (r.contains(x, y)) { 605 DropTarget delegate = target.getDropTargetDelegate(mDragObject); 606 if (delegate != null) { 607 target = delegate; 608 target.getLocationInDragLayer(dropCoordinates); 609 } 610 611 // Make dropCoordinates relative to the DropTarget 612 dropCoordinates[0] = x - dropCoordinates[0]; 613 dropCoordinates[1] = y - dropCoordinates[1]; 614 615 return target; 616 } 617 } 618 return null; 619 } 620 621 public void setDragScoller(DragScroller scroller) { 622 mDragScroller = scroller; 623 } 624 625 public void setWindowToken(IBinder token) { 626 mWindowToken = token; 627 } 628 629 /** 630 * Sets the drag listner which will be notified when a drag starts or ends. 631 */ 632 public void addDragListener(DragListener l) { 633 mListeners.add(l); 634 } 635 636 /** 637 * Remove a previously installed drag listener. 638 */ 639 public void removeDragListener(DragListener l) { 640 mListeners.remove(l); 641 } 642 643 /** 644 * Add a DropTarget to the list of potential places to receive drop events. 645 */ 646 public void addDropTarget(DropTarget target) { 647 mDropTargets.add(target); 648 } 649 650 /** 651 * Don't send drop events to <em>target</em> any more. 652 */ 653 public void removeDropTarget(DropTarget target) { 654 mDropTargets.remove(target); 655 } 656 657 /** 658 * Set which view scrolls for touch events near the edge of the screen. 659 */ 660 public void setScrollView(View v) { 661 mScrollView = v; 662 } 663 664 DragView getDragView() { 665 return mDragObject.dragView; 666 } 667 668 private class ScrollRunnable implements Runnable { 669 private int mDirection; 670 671 ScrollRunnable() { 672 } 673 674 public void run() { 675 if (mDragScroller != null) { 676 if (mDirection == SCROLL_LEFT) { 677 mDragScroller.scrollLeft(); 678 } else { 679 mDragScroller.scrollRight(); 680 } 681 mScrollState = SCROLL_OUTSIDE_ZONE; 682 mDistanceSinceScroll = 0; 683 mDragScroller.onExitScrollArea(); 684 } 685 } 686 687 void setDirection(int direction) { 688 mDirection = direction; 689 } 690 } 691 } 692