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