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