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