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.Utilities.ATLEAST_Q; 20 21 import android.graphics.Point; 22 import android.graphics.Rect; 23 import android.graphics.drawable.Drawable; 24 import android.view.DragEvent; 25 import android.view.KeyEvent; 26 import android.view.MotionEvent; 27 import android.view.View; 28 29 import androidx.annotation.Nullable; 30 31 import com.android.app.animation.Interpolators; 32 import com.android.launcher3.DragSource; 33 import com.android.launcher3.DropTarget; 34 import com.android.launcher3.logging.InstanceId; 35 import com.android.launcher3.model.data.ItemInfo; 36 import com.android.launcher3.model.data.WorkspaceItemInfo; 37 import com.android.launcher3.util.TouchController; 38 import com.android.launcher3.views.ActivityContext; 39 40 import java.util.ArrayList; 41 import java.util.Optional; 42 import java.util.function.Predicate; 43 44 /** 45 * Class for initiating a drag within a view or across multiple views. 46 * @param <T> 47 */ 48 public abstract class DragController<T extends ActivityContext> 49 implements DragDriver.EventListener, TouchController { 50 51 /** 52 * When a drag is started from a deep press, you need to drag this much farther than normal to 53 * end a pre-drag. See {@link DragOptions.PreDragCondition#shouldStartDrag(double)}. 54 */ 55 private static final int DEEP_PRESS_DISTANCE_FACTOR = 3; 56 57 protected final T mActivity; 58 59 // temporaries to avoid gc thrash 60 private final Rect mRectTemp = new Rect(); 61 private final int[] mCoordinatesTemp = new int[2]; 62 63 /** 64 * Drag driver for the current drag/drop operation, or null if there is no active DND operation. 65 * It's null during accessible drag operations. 66 */ 67 protected DragDriver mDragDriver = null; 68 69 /** Options controlling the drag behavior. */ 70 protected DragOptions mOptions; 71 72 /** Coordinate for motion down event */ 73 protected final Point mMotionDown = new Point(); 74 /** Coordinate for last touch event **/ 75 protected final Point mLastTouch = new Point(); 76 77 protected final Point mTmpPoint = new Point(); 78 79 protected DropTarget.DragObject mDragObject; 80 81 /** Who can receive drop events */ 82 private final ArrayList<DropTarget> mDropTargets = new ArrayList<>(); 83 private final ArrayList<DragListener> mListeners = new ArrayList<>(); 84 85 protected DropTarget mLastDropTarget; 86 87 private int mLastTouchClassification; 88 protected int mDistanceSinceScroll = 0; 89 90 protected boolean mIsInPreDrag; 91 92 private final int DRAG_VIEW_SCALE_DURATION_MS = 500; 93 94 /** 95 * Interface to receive notifications when a drag starts or stops 96 */ 97 public interface DragListener { 98 /** 99 * A drag has begun 100 * 101 * @param dragObject The object being dragged 102 * @param options Options used to start the drag 103 */ onDragStart(DropTarget.DragObject dragObject, DragOptions options)104 void onDragStart(DropTarget.DragObject dragObject, DragOptions options); 105 106 /** 107 * The drag has ended 108 */ onDragEnd()109 void onDragEnd(); 110 } 111 112 /** 113 * Used to create a new DragLayer from XML. 114 */ DragController(T activity)115 public DragController(T activity) { 116 mActivity = activity; 117 } 118 119 /** 120 * Starts a drag. 121 * 122 * <p>When the drag is started, the UI automatically goes into spring loaded mode. On a 123 * successful drop, it is the responsibility of the {@link DropTarget} to exit out of the spring 124 * loaded mode. If the drop was cancelled for some reason, the UI will automatically exit out of 125 * this mode. 126 * 127 * @param drawable The drawable to be displayed in the drag view. It will be re-scaled to the 128 * enlarged size. 129 * @param originalView The source view (ie. icon, widget etc.) that is being dragged and which 130 * the DragView represents 131 * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. 132 * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. 133 * @param source An object representing where the drag originated 134 * @param dragInfo The data associated with the object that is being dragged 135 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 136 * Makes dragging feel more precise, e.g. you can clip out a transparent 137 * border 138 */ startDrag( Drawable drawable, DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Rect dragRegion, float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options)139 public DragView startDrag( 140 Drawable drawable, 141 DraggableView originalView, 142 int dragLayerX, 143 int dragLayerY, 144 DragSource source, 145 ItemInfo dragInfo, 146 Rect dragRegion, 147 float initialDragViewScale, 148 float dragViewScaleOnDrop, 149 DragOptions options) { 150 return startDrag(drawable, /* view= */ null, originalView, dragLayerX, dragLayerY, source, 151 dragInfo, dragRegion, initialDragViewScale, dragViewScaleOnDrop, options); 152 } 153 154 /** 155 * Starts a drag. 156 * 157 * <p>When the drag is started, the UI automatically goes into spring loaded mode. On a 158 * successful drop, it is the responsibility of the {@link DropTarget} to exit out of the spring 159 * loaded mode. If the drop was cancelled for some reason, the UI will automatically exit out of 160 * this mode. 161 * 162 * @param view The view to be displayed in the drag view. It will be re-scaled to the 163 * enlarged size. 164 * @param originalView The source view (ie. icon, widget etc.) that is being dragged and which 165 * the DragView represents 166 * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. 167 * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. 168 * @param source An object representing where the drag originated 169 * @param dragInfo The data associated with the object that is being dragged 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 172 * border 173 */ startDrag( View view, DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Rect dragRegion, float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options)174 public DragView startDrag( 175 View view, 176 DraggableView originalView, 177 int dragLayerX, 178 int dragLayerY, 179 DragSource source, 180 ItemInfo dragInfo, 181 Rect dragRegion, 182 float initialDragViewScale, 183 float dragViewScaleOnDrop, 184 DragOptions options) { 185 return startDrag(/* drawable= */ null, view, originalView, dragLayerX, dragLayerY, source, 186 dragInfo, dragRegion, initialDragViewScale, dragViewScaleOnDrop, options); 187 } 188 startDrag( @ullable Drawable drawable, @Nullable View view, DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Rect dragRegion, float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options)189 protected abstract DragView startDrag( 190 @Nullable Drawable drawable, 191 @Nullable View view, 192 DraggableView originalView, 193 int dragLayerX, 194 int dragLayerY, 195 DragSource source, 196 ItemInfo dragInfo, 197 Rect dragRegion, 198 float initialDragViewScale, 199 float dragViewScaleOnDrop, 200 DragOptions options); 201 callOnDragStart()202 protected void callOnDragStart() { 203 if (mOptions.preDragCondition != null) { 204 mOptions.preDragCondition.onPreDragEnd(mDragObject, true /* dragStarted*/); 205 } 206 mIsInPreDrag = false; 207 if (mOptions.preDragEndScale != 0) { 208 mDragObject.dragView 209 .animate() 210 .scaleX(mOptions.preDragEndScale) 211 .scaleY(mOptions.preDragEndScale) 212 .setInterpolator(Interpolators.EMPHASIZED) 213 .setDuration(DRAG_VIEW_SCALE_DURATION_MS) 214 .start(); 215 } 216 mDragObject.dragView.onDragStart(); 217 for (DragListener listener : new ArrayList<>(mListeners)) { 218 listener.onDragStart(mDragObject, mOptions); 219 } 220 } 221 getLogInstanceId()222 public Optional<InstanceId> getLogInstanceId() { 223 return Optional.ofNullable(mDragObject) 224 .map(dragObject -> dragObject.logInstanceId); 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 */ dispatchKeyEvent(KeyEvent event)237 public boolean dispatchKeyEvent(KeyEvent event) { 238 return mDragDriver != null; 239 } 240 isDragging()241 public boolean isDragging() { 242 return mDragDriver != null || (mOptions != null && mOptions.isAccessibleDrag); 243 } 244 245 /** 246 * Stop dragging without dropping. 247 */ cancelDrag()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 dispatchDropComplete(View dropTarget, boolean accepted)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 exitDrag(); 268 mDragObject.deferDragViewCleanupPostAnimation = false; 269 } 270 271 mDragObject.dragSource.onDropCompleted(dropTarget, mDragObject, accepted); 272 } 273 exitDrag()274 protected abstract void exitDrag(); 275 onAppsRemoved(Predicate<ItemInfo> matcher)276 public void onAppsRemoved(Predicate<ItemInfo> matcher) { 277 // Cancel the current drag if we are removing an app that we are dragging 278 if (mDragObject != null) { 279 ItemInfo dragInfo = mDragObject.dragInfo; 280 if (dragInfo instanceof WorkspaceItemInfo && matcher.test(dragInfo)) { 281 cancelDrag(); 282 } 283 } 284 } 285 endDrag()286 protected void endDrag() { 287 if (isDragging()) { 288 mDragDriver = null; 289 boolean isDeferred = false; 290 if (mDragObject.dragView != null) { 291 isDeferred = mDragObject.deferDragViewCleanupPostAnimation; 292 if (!isDeferred) { 293 mDragObject.dragView.remove(); 294 } else if (mIsInPreDrag) { 295 animateDragViewToOriginalPosition(null, null, -1); 296 } 297 mDragObject.dragView.clearAnimation(); 298 mDragObject.dragView = null; 299 } 300 // Only end the drag if we are not deferred 301 if (!isDeferred) { 302 callOnDragEnd(); 303 } 304 } 305 } 306 animateDragViewToOriginalPosition(final Runnable onComplete, final View originalIcon, int duration)307 public void animateDragViewToOriginalPosition(final Runnable onComplete, 308 final View originalIcon, int duration) { 309 Runnable onCompleteRunnable = new Runnable() { 310 @Override 311 public void run() { 312 if (originalIcon != null) { 313 originalIcon.setVisibility(View.VISIBLE); 314 } 315 if (onComplete != null) { 316 onComplete.run(); 317 } 318 } 319 }; 320 mDragObject.dragView.animateTo(mMotionDown.x, mMotionDown.y, onCompleteRunnable, duration); 321 } 322 callOnDragEnd()323 protected void callOnDragEnd() { 324 if (mIsInPreDrag && mOptions.preDragCondition != null) { 325 mOptions.preDragCondition.onPreDragEnd(mDragObject, false /* dragStarted*/); 326 } 327 mIsInPreDrag = false; 328 mOptions = null; 329 for (DragListener listener : new ArrayList<>(mListeners)) { 330 listener.onDragEnd(); 331 } 332 } 333 334 /** 335 * This only gets called as a result of drag view cleanup being deferred in endDrag(); 336 */ onDeferredEndDrag(DragView dragView)337 void onDeferredEndDrag(DragView dragView) { 338 dragView.remove(); 339 340 if (mDragObject.deferDragViewCleanupPostAnimation) { 341 // If we skipped calling onDragEnd() before, do it now 342 callOnDragEnd(); 343 } 344 } 345 346 /** 347 * Clamps the position to the drag layer bounds. 348 */ getClampedDragLayerPos(float x, float y)349 protected Point getClampedDragLayerPos(float x, float y) { 350 mActivity.getDragLayer().getLocalVisibleRect(mRectTemp); 351 mTmpPoint.x = (int) Math.max(mRectTemp.left, Math.min(x, mRectTemp.right - 1)); 352 mTmpPoint.y = (int) Math.max(mRectTemp.top, Math.min(y, mRectTemp.bottom - 1)); 353 return mTmpPoint; 354 } 355 356 @Override onDriverDragMove(float x, float y)357 public void onDriverDragMove(float x, float y) { 358 Point dragLayerPos = getClampedDragLayerPos(x, y); 359 handleMoveEvent(dragLayerPos.x, dragLayerPos.y); 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 if (!endWithFlingAnimation()) { 373 drop(findDropTarget((int) x, (int) y, mCoordinatesTemp), null); 374 } 375 endDrag(); 376 } 377 endWithFlingAnimation()378 protected boolean endWithFlingAnimation() { 379 return false; 380 } 381 382 @Override onDriverDragCancel()383 public void onDriverDragCancel() { 384 cancelDrag(); 385 } 386 387 /** 388 * Call this from a drag source view. 389 */ 390 @Override onControllerInterceptTouchEvent(MotionEvent ev)391 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 392 if (mOptions != null && mOptions.isAccessibleDrag) { 393 return false; 394 } 395 396 Point dragLayerPos = getClampedDragLayerPos(getX(ev), getY(ev)); 397 mLastTouch.set(dragLayerPos.x, dragLayerPos.y); 398 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 399 // Remember location of down touch 400 mMotionDown.set(dragLayerPos.x, dragLayerPos.y); 401 } 402 403 if (ATLEAST_Q) { 404 mLastTouchClassification = ev.getClassification(); 405 } 406 return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev); 407 } 408 getX(MotionEvent ev)409 protected float getX(MotionEvent ev) { 410 return ev.getX(); 411 } 412 getY(MotionEvent ev)413 protected float getY(MotionEvent ev) { 414 return ev.getY(); 415 } 416 417 /** 418 * Call this from a drag source view. 419 */ 420 @Override onControllerTouchEvent(MotionEvent ev)421 public boolean onControllerTouchEvent(MotionEvent ev) { 422 return mDragDriver != null && mDragDriver.onTouchEvent(ev); 423 } 424 425 /** 426 * Call this from a drag source view. 427 */ onDragEvent(DragEvent event)428 public boolean onDragEvent(DragEvent event) { 429 return mDragDriver != null && mDragDriver.onDragEvent(event); 430 } 431 handleMoveEvent(int x, int y)432 protected void handleMoveEvent(int x, int y) { 433 mDragObject.dragView.move(x, y); 434 435 // Drop on someone? 436 final int[] coordinates = mCoordinatesTemp; 437 DropTarget dropTarget = findDropTarget(x, y, coordinates); 438 mDragObject.x = coordinates[0]; 439 mDragObject.y = coordinates[1]; 440 checkTouchMove(dropTarget); 441 442 // Check if we are hovering over the scroll areas 443 mDistanceSinceScroll += Math.hypot(mLastTouch.x - x, mLastTouch.y - y); 444 mLastTouch.set(x, y); 445 446 int distanceDragged = mDistanceSinceScroll; 447 if (ATLEAST_Q && mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) { 448 distanceDragged /= DEEP_PRESS_DISTANCE_FACTOR; 449 } 450 if (mIsInPreDrag && mOptions.preDragCondition != null 451 && mOptions.preDragCondition.shouldStartDrag(distanceDragged)) { 452 callOnDragStart(); 453 } 454 } 455 getDistanceDragged()456 public float getDistanceDragged() { 457 return mDistanceSinceScroll; 458 } 459 forceTouchMove()460 public void forceTouchMove() { 461 int[] placeholderCoordinates = mCoordinatesTemp; 462 DropTarget dropTarget = findDropTarget(mLastTouch.x, mLastTouch.y, placeholderCoordinates); 463 mDragObject.x = placeholderCoordinates[0]; 464 mDragObject.y = placeholderCoordinates[1]; 465 checkTouchMove(dropTarget); 466 } 467 checkTouchMove(DropTarget dropTarget)468 private void checkTouchMove(DropTarget dropTarget) { 469 if (dropTarget != null) { 470 if (mLastDropTarget != dropTarget) { 471 if (mLastDropTarget != null) { 472 mLastDropTarget.onDragExit(mDragObject); 473 } 474 dropTarget.onDragEnter(mDragObject); 475 } 476 dropTarget.onDragOver(mDragObject); 477 } else { 478 if (mLastDropTarget != null) { 479 mLastDropTarget.onDragExit(mDragObject); 480 } 481 } 482 mLastDropTarget = dropTarget; 483 } 484 485 /** 486 * As above, since accessible drag and drop won't cause the same sequence of touch events, 487 * we manually ensure appropriate drag and drop events get emulated for accessible drag. 488 */ completeAccessibleDrag(int[] location)489 public void completeAccessibleDrag(int[] location) { 490 final int[] coordinates = mCoordinatesTemp; 491 492 // We make sure that we prime the target for drop. 493 DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates); 494 mDragObject.x = coordinates[0]; 495 mDragObject.y = coordinates[1]; 496 checkTouchMove(dropTarget); 497 498 dropTarget.prepareAccessibilityDrop(); 499 // Perform the drop 500 drop(dropTarget, null); 501 endDrag(); 502 } 503 drop(DropTarget dropTarget, Runnable flingAnimation)504 protected void drop(DropTarget dropTarget, Runnable flingAnimation) { 505 final int[] coordinates = mCoordinatesTemp; 506 mDragObject.x = coordinates[0]; 507 mDragObject.y = coordinates[1]; 508 509 // Move dragging to the final target. 510 if (dropTarget != mLastDropTarget) { 511 if (mLastDropTarget != null) { 512 mLastDropTarget.onDragExit(mDragObject); 513 } 514 mLastDropTarget = dropTarget; 515 if (dropTarget != null) { 516 dropTarget.onDragEnter(mDragObject); 517 } 518 } 519 520 mDragObject.dragComplete = true; 521 if (mIsInPreDrag) { 522 if (dropTarget != null) { 523 dropTarget.onDragExit(mDragObject); 524 } 525 return; 526 } 527 528 // Drop onto the target. 529 boolean accepted = false; 530 if (dropTarget != null) { 531 dropTarget.onDragExit(mDragObject); 532 if (dropTarget.acceptDrop(mDragObject)) { 533 if (flingAnimation != null) { 534 flingAnimation.run(); 535 } else { 536 dropTarget.onDrop(mDragObject, mOptions); 537 } 538 accepted = true; 539 } 540 } 541 final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null; 542 dispatchDropComplete(dropTargetAsView, accepted); 543 } 544 findDropTarget(int x, int y, int[] dropCoordinates)545 private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 546 mDragObject.x = x; 547 mDragObject.y = y; 548 549 final Rect r = mRectTemp; 550 final ArrayList<DropTarget> dropTargets = mDropTargets; 551 final int count = dropTargets.size(); 552 for (int i = count - 1; i >= 0; i--) { 553 DropTarget target = dropTargets.get(i); 554 if (!target.isDropEnabled()) 555 continue; 556 557 target.getHitRectRelativeToDragLayer(r); 558 if (r.contains(x, y)) { 559 dropCoordinates[0] = x; 560 dropCoordinates[1] = y; 561 mActivity.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates); 562 return target; 563 } 564 } 565 // Pass all unhandled drag to workspace. Workspace finds the correct 566 // cell layout to drop to in the existing drag/drop logic. 567 dropCoordinates[0] = x; 568 dropCoordinates[1] = y; 569 return getDefaultDropTarget(dropCoordinates); 570 } 571 getDefaultDropTarget(int[] dropCoordinates)572 protected abstract DropTarget getDefaultDropTarget(int[] dropCoordinates); 573 574 /** 575 * Sets the drag listener which will be notified when a drag starts or ends. 576 */ addDragListener(DragListener l)577 public void addDragListener(DragListener l) { 578 mListeners.add(l); 579 } 580 581 /** 582 * Remove a previously installed drag listener. 583 */ removeDragListener(DragListener l)584 public void removeDragListener(DragListener l) { 585 mListeners.remove(l); 586 } 587 588 /** 589 * Add a DropTarget to the list of potential places to receive drop events. 590 */ addDropTarget(DropTarget target)591 public void addDropTarget(DropTarget target) { 592 mDropTargets.add(target); 593 } 594 595 /** 596 * Don't send drop events to <em>target</em> any more. 597 */ removeDropTarget(DropTarget target)598 public void removeDropTarget(DropTarget target) { 599 mDropTargets.remove(target); 600 } 601 } 602