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 17package com.android.launcher3.dragndrop; 18 19import android.content.ComponentName; 20import android.content.res.Resources; 21import android.graphics.Bitmap; 22import android.graphics.Point; 23import android.graphics.Rect; 24import android.os.IBinder; 25import android.view.DragEvent; 26import android.view.HapticFeedbackConstants; 27import android.view.KeyEvent; 28import android.view.MotionEvent; 29import android.view.View; 30import android.view.inputmethod.InputMethodManager; 31 32import com.android.launcher3.DragSource; 33import com.android.launcher3.DropTarget; 34import com.android.launcher3.ItemInfo; 35import com.android.launcher3.Launcher; 36import com.android.launcher3.R; 37import com.android.launcher3.ShortcutInfo; 38import com.android.launcher3.accessibility.DragViewStateAnnouncer; 39import com.android.launcher3.util.ItemInfoMatcher; 40import com.android.launcher3.util.Thunk; 41import com.android.launcher3.util.TouchController; 42 43import java.util.ArrayList; 44 45/** 46 * Class for initiating a drag within a view or across multiple views. 47 */ 48public class DragController implements DragDriver.EventListener, TouchController { 49 private static final boolean PROFILE_DRAWING_DURING_DRAG = false; 50 51 @Thunk Launcher mLauncher; 52 private FlingToDeleteHelper mFlingToDeleteHelper; 53 54 // temporaries to avoid gc thrash 55 private Rect mRectTemp = new Rect(); 56 private final int[] mCoordinatesTemp = new int[2]; 57 58 /** 59 * Drag driver for the current drag/drop operation, or null if there is no active DND operation. 60 * It's null during accessible drag operations. 61 */ 62 private DragDriver mDragDriver = null; 63 64 /** Options controlling the drag behavior. */ 65 private DragOptions mOptions; 66 67 /** X coordinate of the down event. */ 68 private int mMotionDownX; 69 70 /** Y coordinate of the down event. */ 71 private int mMotionDownY; 72 73 private DropTarget.DragObject mDragObject; 74 75 /** Who can receive drop events */ 76 private ArrayList<DropTarget> mDropTargets = new ArrayList<>(); 77 private ArrayList<DragListener> mListeners = new ArrayList<>(); 78 79 /** The window token used as the parent for the DragView. */ 80 private IBinder mWindowToken; 81 82 private View mMoveTarget; 83 84 private DropTarget mLastDropTarget; 85 86 @Thunk int mLastTouch[] = new int[2]; 87 @Thunk long mLastTouchUpTime = -1; 88 @Thunk int mDistanceSinceScroll = 0; 89 90 private int mTmpPoint[] = new int[2]; 91 private Rect mDragLayerRect = new Rect(); 92 93 private boolean mIsInPreDrag; 94 95 /** 96 * Interface to receive notifications when a drag starts or stops 97 */ 98 public interface DragListener { 99 /** 100 * A drag has begun 101 * 102 * @param dragObject The object being dragged 103 * @param options Options used to start the drag 104 */ 105 void onDragStart(DropTarget.DragObject dragObject, DragOptions options); 106 107 /** 108 * The drag has ended 109 */ 110 void onDragEnd(); 111 } 112 113 /** 114 * Used to create a new DragLayer from XML. 115 */ 116 public DragController(Launcher launcher) { 117 mLauncher = launcher; 118 mFlingToDeleteHelper = new FlingToDeleteHelper(launcher); 119 } 120 121 /** 122 * Starts a drag. 123 * 124 * @param b The bitmap to display as the drag image. It will be re-scaled to the 125 * enlarged size. 126 * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. 127 * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. 128 * @param source An object representing where the drag originated 129 * @param dragInfo The data associated with the object that is being dragged 130 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 131 * Makes dragging feel more precise, e.g. you can clip out a transparent border 132 */ 133 public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY, 134 DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion, 135 float initialDragViewScale, DragOptions options) { 136 if (PROFILE_DRAWING_DURING_DRAG) { 137 android.os.Debug.startMethodTracing("Launcher"); 138 } 139 140 // Hide soft keyboard, if visible 141 mLauncher.getSystemService(InputMethodManager.class) 142 .hideSoftInputFromWindow(mWindowToken, 0); 143 144 mOptions = options; 145 if (mOptions.systemDndStartPoint != null) { 146 mMotionDownX = mOptions.systemDndStartPoint.x; 147 mMotionDownY = mOptions.systemDndStartPoint.y; 148 } 149 150 final int registrationX = mMotionDownX - dragLayerX; 151 final int registrationY = mMotionDownY - dragLayerY; 152 153 final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; 154 final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; 155 156 mLastDropTarget = null; 157 158 mDragObject = new DropTarget.DragObject(); 159 160 mIsInPreDrag = mOptions.preDragCondition != null 161 && !mOptions.preDragCondition.shouldStartDrag(0); 162 163 final Resources res = mLauncher.getResources(); 164 final float scaleDps = mIsInPreDrag 165 ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f; 166 final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, 167 registrationY, initialDragViewScale, scaleDps); 168 169 mDragObject.dragComplete = false; 170 if (mOptions.isAccessibleDrag) { 171 // For an accessible drag, we assume the view is being dragged from the center. 172 mDragObject.xOffset = b.getWidth() / 2; 173 mDragObject.yOffset = b.getHeight() / 2; 174 mDragObject.accessibleDrag = true; 175 } else { 176 mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); 177 mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); 178 mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView); 179 180 mDragDriver = DragDriver.create(mLauncher, this, mDragObject, mOptions); 181 } 182 183 mDragObject.dragSource = source; 184 mDragObject.dragInfo = dragInfo; 185 mDragObject.originalDragInfo = new ItemInfo(); 186 mDragObject.originalDragInfo.copyFrom(dragInfo); 187 188 if (dragOffset != null) { 189 dragView.setDragVisualizeOffset(new Point(dragOffset)); 190 } 191 if (dragRegion != null) { 192 dragView.setDragRegion(new Rect(dragRegion)); 193 } 194 195 mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 196 dragView.show(mMotionDownX, mMotionDownY); 197 mDistanceSinceScroll = 0; 198 199 if (!mIsInPreDrag) { 200 callOnDragStart(); 201 } else if (mOptions.preDragCondition != null) { 202 mOptions.preDragCondition.onPreDragStart(mDragObject); 203 } 204 205 mLastTouch[0] = mMotionDownX; 206 mLastTouch[1] = mMotionDownY; 207 handleMoveEvent(mMotionDownX, mMotionDownY); 208 mLauncher.getUserEventDispatcher().resetActionDurationMillis(); 209 return dragView; 210 } 211 212 private void callOnDragStart() { 213 for (DragListener listener : new ArrayList<>(mListeners)) { 214 listener.onDragStart(mDragObject, mOptions); 215 } 216 if (mOptions.preDragCondition != null) { 217 mOptions.preDragCondition.onPreDragEnd(mDragObject, true /* dragStarted*/); 218 } 219 mIsInPreDrag = false; 220 } 221 222 /** 223 * Call this from a drag source view like this: 224 * 225 * <pre> 226 * @Override 227 * public boolean dispatchKeyEvent(KeyEvent event) { 228 * return mDragController.dispatchKeyEvent(this, event) 229 * || super.dispatchKeyEvent(event); 230 * </pre> 231 */ 232 public boolean dispatchKeyEvent(KeyEvent event) { 233 return mDragDriver != null; 234 } 235 236 public boolean isDragging() { 237 return mDragDriver != null || (mOptions != null && mOptions.isAccessibleDrag); 238 } 239 240 /** 241 * Stop dragging without dropping. 242 */ 243 public void cancelDrag() { 244 if (isDragging()) { 245 if (mLastDropTarget != null) { 246 mLastDropTarget.onDragExit(mDragObject); 247 } 248 mDragObject.deferDragViewCleanupPostAnimation = false; 249 mDragObject.cancelled = true; 250 mDragObject.dragComplete = true; 251 if (!mIsInPreDrag) { 252 mDragObject.dragSource.onDropCompleted(null, mDragObject, false, false); 253 } 254 } 255 endDrag(); 256 } 257 258 public void onAppsRemoved(ItemInfoMatcher matcher) { 259 // Cancel the current drag if we are removing an app that we are dragging 260 if (mDragObject != null) { 261 ItemInfo dragInfo = mDragObject.dragInfo; 262 if (dragInfo instanceof ShortcutInfo) { 263 ComponentName cn = dragInfo.getTargetComponent(); 264 if (cn != null && matcher.matches(dragInfo, cn)) { 265 cancelDrag(); 266 } 267 } 268 } 269 } 270 271 private void endDrag() { 272 if (isDragging()) { 273 mDragDriver = null; 274 boolean isDeferred = false; 275 if (mDragObject.dragView != null) { 276 isDeferred = mDragObject.deferDragViewCleanupPostAnimation; 277 if (!isDeferred) { 278 mDragObject.dragView.remove(); 279 } else if (mIsInPreDrag) { 280 animateDragViewToOriginalPosition(null, null, -1); 281 } 282 mDragObject.dragView = null; 283 } 284 285 // Only end the drag if we are not deferred 286 if (!isDeferred) { 287 callOnDragEnd(); 288 } 289 } 290 291 mFlingToDeleteHelper.releaseVelocityTracker(); 292 } 293 294 public void animateDragViewToOriginalPosition(final Runnable onComplete, 295 final View originalIcon, int duration) { 296 Runnable onCompleteRunnable = new Runnable() { 297 @Override 298 public void run() { 299 if (originalIcon != null) { 300 originalIcon.setVisibility(View.VISIBLE); 301 } 302 if (onComplete != null) { 303 onComplete.run(); 304 } 305 } 306 }; 307 mDragObject.dragView.animateTo(mMotionDownX, mMotionDownY, onCompleteRunnable, duration); 308 } 309 310 private void callOnDragEnd() { 311 if (mIsInPreDrag && mOptions.preDragCondition != null) { 312 mOptions.preDragCondition.onPreDragEnd(mDragObject, false /* dragStarted*/); 313 } 314 mIsInPreDrag = false; 315 mOptions = null; 316 for (DragListener listener : new ArrayList<>(mListeners)) { 317 listener.onDragEnd(); 318 } 319 } 320 321 /** 322 * This only gets called as a result of drag view cleanup being deferred in endDrag(); 323 */ 324 void onDeferredEndDrag(DragView dragView) { 325 dragView.remove(); 326 327 if (mDragObject.deferDragViewCleanupPostAnimation) { 328 // If we skipped calling onDragEnd() before, do it now 329 callOnDragEnd(); 330 } 331 } 332 333 /** 334 * Clamps the position to the drag layer bounds. 335 */ 336 private int[] getClampedDragLayerPos(float x, float y) { 337 mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect); 338 mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1)); 339 mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1)); 340 return mTmpPoint; 341 } 342 343 public long getLastGestureUpTime() { 344 if (mDragDriver != null) { 345 return System.currentTimeMillis(); 346 } else { 347 return mLastTouchUpTime; 348 } 349 } 350 351 public void resetLastGestureUpTime() { 352 mLastTouchUpTime = -1; 353 } 354 355 @Override 356 public void onDriverDragMove(float x, float y) { 357 final int[] dragLayerPos = getClampedDragLayerPos(x, y); 358 359 handleMoveEvent(dragLayerPos[0], dragLayerPos[1]); 360 } 361 362 @Override 363 public void onDriverDragExitWindow() { 364 if (mLastDropTarget != null) { 365 mLastDropTarget.onDragExit(mDragObject); 366 mLastDropTarget = null; 367 } 368 } 369 370 @Override 371 public void onDriverDragEnd(float x, float y) { 372 DropTarget dropTarget; 373 Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject); 374 if (flingAnimation != null) { 375 dropTarget = mFlingToDeleteHelper.getDropTarget(); 376 } else { 377 dropTarget = findDropTarget((int) x, (int) y, mCoordinatesTemp); 378 } 379 380 drop(dropTarget, flingAnimation); 381 382 endDrag(); 383 } 384 385 @Override 386 public void onDriverDragCancel() { 387 cancelDrag(); 388 } 389 390 /** 391 * Call this from a drag source view. 392 */ 393 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 394 if (mOptions != null && mOptions.isAccessibleDrag) { 395 return false; 396 } 397 398 // Update the velocity tracker 399 mFlingToDeleteHelper.recordMotionEvent(ev); 400 401 final int action = ev.getAction(); 402 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 403 final int dragLayerX = dragLayerPos[0]; 404 final int dragLayerY = dragLayerPos[1]; 405 406 switch (action) { 407 case MotionEvent.ACTION_DOWN: 408 // Remember location of down touch 409 mMotionDownX = dragLayerX; 410 mMotionDownY = dragLayerY; 411 break; 412 case MotionEvent.ACTION_UP: 413 mLastTouchUpTime = System.currentTimeMillis(); 414 break; 415 } 416 417 return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev); 418 } 419 420 /** 421 * Call this from a drag source view. 422 */ 423 public boolean onDragEvent(long dragStartTime, DragEvent event) { 424 mFlingToDeleteHelper.recordDragEvent(dragStartTime, event); 425 return mDragDriver != null && mDragDriver.onDragEvent(event); 426 } 427 428 /** 429 * Call this from a drag view. 430 */ 431 public void onDragViewAnimationEnd() { 432 if (mDragDriver != null) { 433 mDragDriver.onDragViewAnimationEnd(); 434 } 435 } 436 437 /** 438 * Sets the view that should handle move events. 439 */ 440 public void setMoveTarget(View view) { 441 mMoveTarget = view; 442 } 443 444 public boolean dispatchUnhandledMove(View focused, int direction) { 445 return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); 446 } 447 448 private void handleMoveEvent(int x, int y) { 449 mDragObject.dragView.move(x, y); 450 451 // Drop on someone? 452 final int[] coordinates = mCoordinatesTemp; 453 DropTarget dropTarget = findDropTarget(x, y, coordinates); 454 mDragObject.x = coordinates[0]; 455 mDragObject.y = coordinates[1]; 456 checkTouchMove(dropTarget); 457 458 // Check if we are hovering over the scroll areas 459 mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y); 460 mLastTouch[0] = x; 461 mLastTouch[1] = y; 462 463 if (mIsInPreDrag && mOptions.preDragCondition != null 464 && mOptions.preDragCondition.shouldStartDrag(mDistanceSinceScroll)) { 465 callOnDragStart(); 466 } 467 } 468 469 public float getDistanceDragged() { 470 return mDistanceSinceScroll; 471 } 472 473 public void forceTouchMove() { 474 int[] dummyCoordinates = mCoordinatesTemp; 475 DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates); 476 mDragObject.x = dummyCoordinates[0]; 477 mDragObject.y = dummyCoordinates[1]; 478 checkTouchMove(dropTarget); 479 } 480 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 * Call this from a drag source view. 500 */ 501 public boolean onControllerTouchEvent(MotionEvent ev) { 502 if (mDragDriver == null || mOptions == null || mOptions.isAccessibleDrag) { 503 return false; 504 } 505 506 // Update the velocity tracker 507 mFlingToDeleteHelper.recordMotionEvent(ev); 508 509 final int action = ev.getAction(); 510 final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); 511 final int dragLayerX = dragLayerPos[0]; 512 final int dragLayerY = dragLayerPos[1]; 513 514 switch (action) { 515 case MotionEvent.ACTION_DOWN: 516 // Remember where the motion event started 517 mMotionDownX = dragLayerX; 518 mMotionDownY = dragLayerY; 519 break; 520 } 521 522 return mDragDriver.onTouchEvent(ev); 523 } 524 525 /** 526 * Since accessible drag and drop won't cause the same sequence of touch events, we manually 527 * inject the appropriate state. 528 */ 529 public void prepareAccessibleDrag(int x, int y) { 530 mMotionDownX = x; 531 mMotionDownY = y; 532 } 533 534 /** 535 * As above, since accessible drag and drop won't cause the same sequence of touch events, 536 * we manually ensure appropriate drag and drop events get emulated for accessible drag. 537 */ 538 public void completeAccessibleDrag(int[] location) { 539 final int[] coordinates = mCoordinatesTemp; 540 541 // We make sure that we prime the target for drop. 542 DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates); 543 mDragObject.x = coordinates[0]; 544 mDragObject.y = coordinates[1]; 545 checkTouchMove(dropTarget); 546 547 dropTarget.prepareAccessibilityDrop(); 548 // Perform the drop 549 drop(dropTarget, null); 550 endDrag(); 551 } 552 553 private void drop(DropTarget dropTarget, Runnable flingAnimation) { 554 final int[] coordinates = mCoordinatesTemp; 555 mDragObject.x = coordinates[0]; 556 mDragObject.y = coordinates[1]; 557 558 // Move dragging to the final target. 559 if (dropTarget != mLastDropTarget) { 560 if (mLastDropTarget != null) { 561 mLastDropTarget.onDragExit(mDragObject); 562 } 563 mLastDropTarget = dropTarget; 564 if (dropTarget != null) { 565 dropTarget.onDragEnter(mDragObject); 566 } 567 } 568 569 mDragObject.dragComplete = true; 570 571 // Drop onto the target. 572 boolean accepted = false; 573 if (dropTarget != null) { 574 dropTarget.onDragExit(mDragObject); 575 if (dropTarget.acceptDrop(mDragObject)) { 576 if (flingAnimation != null) { 577 flingAnimation.run(); 578 } else if (!mIsInPreDrag) { 579 dropTarget.onDrop(mDragObject); 580 } 581 accepted = true; 582 } 583 } 584 final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null; 585 if (!mIsInPreDrag) { 586 mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView); 587 mDragObject.dragSource.onDropCompleted( 588 dropTargetAsView, mDragObject, flingAnimation != null, accepted); 589 } 590 } 591 592 private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 593 final Rect r = mRectTemp; 594 595 final ArrayList<DropTarget> dropTargets = mDropTargets; 596 final int count = dropTargets.size(); 597 for (int i=count-1; i>=0; i--) { 598 DropTarget target = dropTargets.get(i); 599 if (!target.isDropEnabled()) 600 continue; 601 602 target.getHitRectRelativeToDragLayer(r); 603 604 mDragObject.x = x; 605 mDragObject.y = y; 606 if (r.contains(x, y)) { 607 608 dropCoordinates[0] = x; 609 dropCoordinates[1] = y; 610 mLauncher.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates); 611 612 return target; 613 } 614 } 615 return null; 616 } 617 618 public void setWindowToken(IBinder token) { 619 mWindowToken = token; 620 } 621 622 /** 623 * Sets the drag listener which will be notified when a drag starts or ends. 624 */ 625 public void addDragListener(DragListener l) { 626 mListeners.add(l); 627 } 628 629 /** 630 * Remove a previously installed drag listener. 631 */ 632 public void removeDragListener(DragListener l) { 633 mListeners.remove(l); 634 } 635 636 /** 637 * Add a DropTarget to the list of potential places to receive drop events. 638 */ 639 public void addDropTarget(DropTarget target) { 640 mDropTargets.add(target); 641 } 642 643 /** 644 * Don't send drop events to <em>target</em> any more. 645 */ 646 public void removeDropTarget(DropTarget target) { 647 mDropTargets.remove(target); 648 } 649 650} 651