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