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.launcher3.dragndrop; 18 19import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY; 20import static com.android.launcher3.LauncherState.NORMAL; 21 22import android.content.ComponentName; 23import android.content.res.Resources; 24import android.graphics.Bitmap; 25import android.graphics.Point; 26import android.graphics.Rect; 27import android.os.IBinder; 28import android.view.DragEvent; 29import android.view.HapticFeedbackConstants; 30import android.view.KeyEvent; 31import android.view.MotionEvent; 32import android.view.View; 33 34import com.android.launcher3.DragSource; 35import com.android.launcher3.DropTarget; 36import com.android.launcher3.ItemInfo; 37import com.android.launcher3.Launcher; 38import com.android.launcher3.R; 39import com.android.launcher3.ShortcutInfo; 40import com.android.launcher3.accessibility.DragViewStateAnnouncer; 41import com.android.launcher3.util.ItemInfoMatcher; 42import com.android.launcher3.util.Thunk; 43import com.android.launcher3.util.TouchController; 44import com.android.launcher3.util.UiThreadHelper; 45 46import java.util.ArrayList; 47 48/** 49 * Class for initiating a drag within a view or across multiple views. 50 */ 51public class DragController implements DragDriver.EventListener, TouchController { 52 private static final boolean PROFILE_DRAWING_DURING_DRAG = false; 53 54 @Thunk Launcher mLauncher; 55 private FlingToDeleteHelper mFlingToDeleteHelper; 56 57 // temporaries to avoid gc thrash 58 private Rect mRectTemp = new Rect(); 59 private final int[] mCoordinatesTemp = new int[2]; 60 61 /** 62 * Drag driver for the current drag/drop operation, or null if there is no active DND operation. 63 * It's null during accessible drag operations. 64 */ 65 private DragDriver mDragDriver = null; 66 67 /** Options controlling the drag behavior. */ 68 private DragOptions mOptions; 69 70 /** X coordinate of the down event. */ 71 private int mMotionDownX; 72 73 /** Y coordinate of the down event. */ 74 private int mMotionDownY; 75 76 private DropTarget.DragObject mDragObject; 77 78 /** Who can receive drop events */ 79 private ArrayList<DropTarget> mDropTargets = new ArrayList<>(); 80 private ArrayList<DragListener> mListeners = new ArrayList<>(); 81 82 /** The window token used as the parent for the DragView. */ 83 private IBinder mWindowToken; 84 85 private View mMoveTarget; 86 87 private DropTarget mLastDropTarget; 88 89 @Thunk int mLastTouch[] = new int[2]; 90 @Thunk long mLastTouchUpTime = -1; 91 @Thunk int mDistanceSinceScroll = 0; 92 93 private int mTmpPoint[] = new int[2]; 94 private Rect mDragLayerRect = new Rect(); 95 96 private boolean mIsInPreDrag; 97 98 /** 99 * Interface to receive notifications when a drag starts or stops 100 */ 101 public interface DragListener { 102 /** 103 * A drag has begun 104 * 105 * @param dragObject The object being dragged 106 * @param options Options used to start the drag 107 */ 108 void onDragStart(DropTarget.DragObject dragObject, DragOptions options); 109 110 /** 111 * The drag has ended 112 */ 113 void onDragEnd(); 114 } 115 116 /** 117 * Used to create a new DragLayer from XML. 118 */ 119 public DragController(Launcher launcher) { 120 mLauncher = launcher; 121 mFlingToDeleteHelper = new FlingToDeleteHelper(launcher); 122 } 123 124 /** 125 * Starts a drag. 126 * When the drag is started, the UI automatically goes into spring loaded mode. On a successful 127 * drop, it is the responsibility of the {@link DropTarget} to exit out of the spring loaded 128 * mode. If the drop was cancelled for some reason, the UI will automatically exit out of this mode. 129 * 130 * @param b The bitmap to display as the drag image. It will be re-scaled to the 131 * enlarged size. 132 * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. 133 * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. 134 * @param source An object representing where the drag originated 135 * @param dragInfo The data associated with the object that is being dragged 136 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 137 * Makes dragging feel more precise, e.g. you can clip out a transparent border 138 */ 139 public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY, 140 DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion, 141 float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options) { 142 if (PROFILE_DRAWING_DURING_DRAG) { 143 android.os.Debug.startMethodTracing("Launcher"); 144 } 145 146 // Hide soft keyboard, if visible 147 UiThreadHelper.hideKeyboardAsync(mLauncher, mWindowToken); 148 149 mOptions = options; 150 if (mOptions.systemDndStartPoint != null) { 151 mMotionDownX = mOptions.systemDndStartPoint.x; 152 mMotionDownY = mOptions.systemDndStartPoint.y; 153 } 154 155 final int registrationX = mMotionDownX - dragLayerX; 156 final int registrationY = mMotionDownY - dragLayerY; 157 158 final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; 159 final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; 160 161 mLastDropTarget = null; 162 163 mDragObject = new DropTarget.DragObject(); 164 165 mIsInPreDrag = mOptions.preDragCondition != null 166 && !mOptions.preDragCondition.shouldStartDrag(0); 167 168 final Resources res = mLauncher.getResources(); 169 final float scaleDps = mIsInPreDrag 170 ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f; 171 final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, 172 registrationY, initialDragViewScale, dragViewScaleOnDrop, scaleDps); 173 dragView.setItemInfo(dragInfo); 174 mDragObject.dragComplete = false; 175 if (mOptions.isAccessibleDrag) { 176 // For an accessible drag, we assume the view is being dragged from the center. 177 mDragObject.xOffset = b.getWidth() / 2; 178 mDragObject.yOffset = b.getHeight() / 2; 179 mDragObject.accessibleDrag = true; 180 } else { 181 mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); 182 mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); 183 mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView); 184 185 mDragDriver = DragDriver.create(mLauncher, this, mDragObject, mOptions); 186 } 187 188 mDragObject.dragSource = source; 189 mDragObject.dragInfo = dragInfo; 190 mDragObject.originalDragInfo = new ItemInfo(); 191 mDragObject.originalDragInfo.copyFrom(dragInfo); 192 193 if (dragOffset != null) { 194 dragView.setDragVisualizeOffset(new Point(dragOffset)); 195 } 196 if (dragRegion != null) { 197 dragView.setDragRegion(new Rect(dragRegion)); 198 } 199 200 mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 201 dragView.show(mMotionDownX, mMotionDownY); 202 mDistanceSinceScroll = 0; 203 204 if (!mIsInPreDrag) { 205 callOnDragStart(); 206 } else if (mOptions.preDragCondition != null) { 207 mOptions.preDragCondition.onPreDragStart(mDragObject); 208 } 209 210 mLastTouch[0] = mMotionDownX; 211 mLastTouch[1] = mMotionDownY; 212 handleMoveEvent(mMotionDownX, mMotionDownY); 213 mLauncher.getUserEventDispatcher().resetActionDurationMillis(); 214 return dragView; 215 } 216 217 private void callOnDragStart() { 218 if (mOptions.preDragCondition != null) { 219 mOptions.preDragCondition.onPreDragEnd(mDragObject, true /* dragStarted*/); 220 } 221 mIsInPreDrag = false; 222 for (DragListener listener : new ArrayList<>(mListeners)) { 223 listener.onDragStart(mDragObject, mOptions); 224 } 225 } 226 227 /** 228 * Call this from a drag source view like this: 229 * 230 * <pre> 231 * @Override 232 * public boolean dispatchKeyEvent(KeyEvent event) { 233 * return mDragController.dispatchKeyEvent(this, event) 234 * || super.dispatchKeyEvent(event); 235 * </pre> 236 */ 237 public boolean dispatchKeyEvent(KeyEvent event) { 238 return mDragDriver != null; 239 } 240 241 public boolean isDragging() { 242 return mDragDriver != null || (mOptions != null && mOptions.isAccessibleDrag); 243 } 244 245 /** 246 * Stop dragging without dropping. 247 */ 248 public void cancelDrag() { 249 if (isDragging()) { 250 if (mLastDropTarget != null) { 251 mLastDropTarget.onDragExit(mDragObject); 252 } 253 mDragObject.deferDragViewCleanupPostAnimation = false; 254 mDragObject.cancelled = true; 255 mDragObject.dragComplete = true; 256 if (!mIsInPreDrag) { 257 dispatchDropComplete(null, false); 258 } 259 } 260 endDrag(); 261 } 262 263 private void dispatchDropComplete(View dropTarget, boolean accepted) { 264 if (!accepted) { 265 // If it was not accepted, cleanup the state. If it was accepted, it is the 266 // responsibility of the drop target to cleanup the state. 267 mLauncher.getStateManager().goToState(NORMAL, SPRING_LOADED_EXIT_DELAY); 268 mDragObject.deferDragViewCleanupPostAnimation = false; 269 } 270 271 mDragObject.dragSource.onDropCompleted(dropTarget, mDragObject, accepted); 272 } 273 274 public void onAppsRemoved(ItemInfoMatcher matcher) { 275 // Cancel the current drag if we are removing an app that we are dragging 276 if (mDragObject != null) { 277 ItemInfo dragInfo = mDragObject.dragInfo; 278 if (dragInfo instanceof ShortcutInfo) { 279 ComponentName cn = dragInfo.getTargetComponent(); 280 if (cn != null && matcher.matches(dragInfo, cn)) { 281 cancelDrag(); 282 } 283 } 284 } 285 } 286 287 private void endDrag() { 288 if (isDragging()) { 289 mDragDriver = null; 290 boolean isDeferred = false; 291 if (mDragObject.dragView != null) { 292 isDeferred = mDragObject.deferDragViewCleanupPostAnimation; 293 if (!isDeferred) { 294 mDragObject.dragView.remove(); 295 } else if (mIsInPreDrag) { 296 animateDragViewToOriginalPosition(null, null, -1); 297 } 298 mDragObject.dragView = null; 299 } 300 301 // Only end the drag if we are not deferred 302 if (!isDeferred) { 303 callOnDragEnd(); 304 } 305 } 306 307 mFlingToDeleteHelper.releaseVelocityTracker(); 308 } 309 310 public void animateDragViewToOriginalPosition(final Runnable onComplete, 311 final View originalIcon, int duration) { 312 Runnable onCompleteRunnable = new Runnable() { 313 @Override 314 public void run() { 315 if (originalIcon != null) { 316 originalIcon.setVisibility(View.VISIBLE); 317 } 318 if (onComplete != null) { 319 onComplete.run(); 320 } 321 } 322 }; 323 mDragObject.dragView.animateTo(mMotionDownX, mMotionDownY, onCompleteRunnable, duration); 324 } 325 326 private void callOnDragEnd() { 327 if (mIsInPreDrag && mOptions.preDragCondition != null) { 328 mOptions.preDragCondition.onPreDragEnd(mDragObject, false /* dragStarted*/); 329 } 330 mIsInPreDrag = false; 331 mOptions = null; 332 for (DragListener listener : new ArrayList<>(mListeners)) { 333 listener.onDragEnd(); 334 } 335 } 336 337 /** 338 * This only gets called as a result of drag view cleanup being deferred in endDrag(); 339 */ 340 void onDeferredEndDrag(DragView dragView) { 341 dragView.remove(); 342 343 if (mDragObject.deferDragViewCleanupPostAnimation) { 344 // If we skipped calling onDragEnd() before, do it now 345 callOnDragEnd(); 346 } 347 } 348 349 /** 350 * Clamps the position to the drag layer bounds. 351 */ 352 private int[] getClampedDragLayerPos(float x, float y) { 353 mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect); 354 mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1)); 355 mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1)); 356 return mTmpPoint; 357 } 358 359 public long getLastGestureUpTime() { 360 if (mDragDriver != null) { 361 return System.currentTimeMillis(); 362 } else { 363 return mLastTouchUpTime; 364 } 365 } 366 367 public void resetLastGestureUpTime() { 368 mLastTouchUpTime = -1; 369 } 370 371 @Override 372 public void onDriverDragMove(float x, float y) { 373 final int[] dragLayerPos = getClampedDragLayerPos(x, y); 374 375 handleMoveEvent(dragLayerPos[0], dragLayerPos[1]); 376 } 377 378 @Override 379 public void onDriverDragExitWindow() { 380 if (mLastDropTarget != null) { 381 mLastDropTarget.onDragExit(mDragObject); 382 mLastDropTarget = null; 383 } 384 } 385 386 @Override 387 public void onDriverDragEnd(float x, float y) { 388 DropTarget dropTarget; 389 Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject); 390 if (flingAnimation != null) { 391 dropTarget = mFlingToDeleteHelper.getDropTarget(); 392 } else { 393 dropTarget = findDropTarget((int) x, (int) y, mCoordinatesTemp); 394 } 395 396 drop(dropTarget, flingAnimation); 397 398 endDrag(); 399 } 400 401 @Override 402 public void onDriverDragCancel() { 403 cancelDrag(); 404 } 405 406 /** 407 * Call this from a drag source view. 408 */ 409 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 410 if (mOptions != null && mOptions.isAccessibleDrag) { 411 return false; 412 } 413 414 // Update the velocity tracker 415 mFlingToDeleteHelper.recordMotionEvent(ev); 416 417 final int action = ev.getAction(); 418 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 419 final int dragLayerX = dragLayerPos[0]; 420 final int dragLayerY = dragLayerPos[1]; 421 422 switch (action) { 423 case MotionEvent.ACTION_DOWN: 424 // Remember location of down touch 425 mMotionDownX = dragLayerX; 426 mMotionDownY = dragLayerY; 427 break; 428 case MotionEvent.ACTION_UP: 429 mLastTouchUpTime = System.currentTimeMillis(); 430 break; 431 } 432 433 return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev); 434 } 435 436 /** 437 * Call this from a drag source view. 438 */ 439 public boolean onDragEvent(long dragStartTime, DragEvent event) { 440 mFlingToDeleteHelper.recordDragEvent(dragStartTime, event); 441 return mDragDriver != null && mDragDriver.onDragEvent(event); 442 } 443 444 /** 445 * Call this from a drag view. 446 */ 447 public void onDragViewAnimationEnd() { 448 if (mDragDriver != null) { 449 mDragDriver.onDragViewAnimationEnd(); 450 } 451 } 452 453 /** 454 * Sets the view that should handle move events. 455 */ 456 public void setMoveTarget(View view) { 457 mMoveTarget = view; 458 } 459 460 public boolean dispatchUnhandledMove(View focused, int direction) { 461 return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); 462 } 463 464 private void handleMoveEvent(int x, int y) { 465 mDragObject.dragView.move(x, y); 466 467 // Drop on someone? 468 final int[] coordinates = mCoordinatesTemp; 469 DropTarget dropTarget = findDropTarget(x, y, coordinates); 470 mDragObject.x = coordinates[0]; 471 mDragObject.y = coordinates[1]; 472 checkTouchMove(dropTarget); 473 474 // Check if we are hovering over the scroll areas 475 mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y); 476 mLastTouch[0] = x; 477 mLastTouch[1] = y; 478 479 if (mIsInPreDrag && mOptions.preDragCondition != null 480 && mOptions.preDragCondition.shouldStartDrag(mDistanceSinceScroll)) { 481 callOnDragStart(); 482 } 483 } 484 485 public float getDistanceDragged() { 486 return mDistanceSinceScroll; 487 } 488 489 public void forceTouchMove() { 490 int[] dummyCoordinates = mCoordinatesTemp; 491 DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates); 492 mDragObject.x = dummyCoordinates[0]; 493 mDragObject.y = dummyCoordinates[1]; 494 checkTouchMove(dropTarget); 495 } 496 497 private void checkTouchMove(DropTarget dropTarget) { 498 if (dropTarget != null) { 499 if (mLastDropTarget != dropTarget) { 500 if (mLastDropTarget != null) { 501 mLastDropTarget.onDragExit(mDragObject); 502 } 503 dropTarget.onDragEnter(mDragObject); 504 } 505 dropTarget.onDragOver(mDragObject); 506 } else { 507 if (mLastDropTarget != null) { 508 mLastDropTarget.onDragExit(mDragObject); 509 } 510 } 511 mLastDropTarget = dropTarget; 512 } 513 514 /** 515 * Call this from a drag source view. 516 */ 517 public boolean onControllerTouchEvent(MotionEvent ev) { 518 if (mDragDriver == null || mOptions == null || mOptions.isAccessibleDrag) { 519 return false; 520 } 521 522 // Update the velocity tracker 523 mFlingToDeleteHelper.recordMotionEvent(ev); 524 525 final int action = ev.getAction(); 526 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 527 final int dragLayerX = dragLayerPos[0]; 528 final int dragLayerY = dragLayerPos[1]; 529 530 switch (action) { 531 case MotionEvent.ACTION_DOWN: 532 // Remember where the motion event started 533 mMotionDownX = dragLayerX; 534 mMotionDownY = dragLayerY; 535 break; 536 } 537 538 return mDragDriver.onTouchEvent(ev); 539 } 540 541 /** 542 * Since accessible drag and drop won't cause the same sequence of touch events, we manually 543 * inject the appropriate state. 544 */ 545 public void prepareAccessibleDrag(int x, int y) { 546 mMotionDownX = x; 547 mMotionDownY = y; 548 } 549 550 /** 551 * As above, since accessible drag and drop won't cause the same sequence of touch events, 552 * we manually ensure appropriate drag and drop events get emulated for accessible drag. 553 */ 554 public void completeAccessibleDrag(int[] location) { 555 final int[] coordinates = mCoordinatesTemp; 556 557 // We make sure that we prime the target for drop. 558 DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates); 559 mDragObject.x = coordinates[0]; 560 mDragObject.y = coordinates[1]; 561 checkTouchMove(dropTarget); 562 563 dropTarget.prepareAccessibilityDrop(); 564 // Perform the drop 565 drop(dropTarget, null); 566 endDrag(); 567 } 568 569 private void drop(DropTarget dropTarget, Runnable flingAnimation) { 570 final int[] coordinates = mCoordinatesTemp; 571 mDragObject.x = coordinates[0]; 572 mDragObject.y = coordinates[1]; 573 574 // Move dragging to the final target. 575 if (dropTarget != mLastDropTarget) { 576 if (mLastDropTarget != null) { 577 mLastDropTarget.onDragExit(mDragObject); 578 } 579 mLastDropTarget = dropTarget; 580 if (dropTarget != null) { 581 dropTarget.onDragEnter(mDragObject); 582 } 583 } 584 585 mDragObject.dragComplete = true; 586 if (mIsInPreDrag) { 587 if (dropTarget != null) { 588 dropTarget.onDragExit(mDragObject); 589 } 590 return; 591 } 592 593 // Drop onto the target. 594 boolean accepted = false; 595 if (dropTarget != null) { 596 dropTarget.onDragExit(mDragObject); 597 if (dropTarget.acceptDrop(mDragObject)) { 598 if (flingAnimation != null) { 599 flingAnimation.run(); 600 } else { 601 dropTarget.onDrop(mDragObject, mOptions); 602 } 603 accepted = true; 604 } 605 } 606 final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null; 607 mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView); 608 dispatchDropComplete(dropTargetAsView, accepted); 609 } 610 611 private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 612 mDragObject.x = x; 613 mDragObject.y = y; 614 615 final Rect r = mRectTemp; 616 final ArrayList<DropTarget> dropTargets = mDropTargets; 617 final int count = dropTargets.size(); 618 for (int i = count - 1; i >= 0; i--) { 619 DropTarget target = dropTargets.get(i); 620 if (!target.isDropEnabled()) 621 continue; 622 623 target.getHitRectRelativeToDragLayer(r); 624 if (r.contains(x, y)) { 625 dropCoordinates[0] = x; 626 dropCoordinates[1] = y; 627 mLauncher.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates); 628 return target; 629 } 630 } 631 // Pass all unhandled drag to workspace. Workspace finds the correct 632 // cell layout to drop to in the existing drag/drop logic. 633 dropCoordinates[0] = x; 634 dropCoordinates[1] = y; 635 mLauncher.getDragLayer().mapCoordInSelfToDescendant(mLauncher.getWorkspace(), 636 dropCoordinates); 637 return mLauncher.getWorkspace(); 638 } 639 640 public void setWindowToken(IBinder token) { 641 mWindowToken = token; 642 } 643 644 /** 645 * Sets the drag listener which will be notified when a drag starts or ends. 646 */ 647 public void addDragListener(DragListener l) { 648 mListeners.add(l); 649 } 650 651 /** 652 * Remove a previously installed drag listener. 653 */ 654 public void removeDragListener(DragListener l) { 655 mListeners.remove(l); 656 } 657 658 /** 659 * Add a DropTarget to the list of potential places to receive drop events. 660 */ 661 public void addDropTarget(DropTarget target) { 662 mDropTargets.add(target); 663 } 664 665 /** 666 * Don't send drop events to <em>target</em> any more. 667 */ 668 public void removeDropTarget(DropTarget target) { 669 mDropTargets.remove(target); 670 } 671 672} 673