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