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