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