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