DragController.java revision bfbfd26c627a18f8e1e3e6d0e53e78feab360203
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.launcher2; 18 19import android.content.Context; 20import android.graphics.Bitmap; 21import android.graphics.Rect; 22import android.graphics.RectF; 23import android.os.Handler; 24import android.os.IBinder; 25import android.os.Vibrator; 26import android.util.DisplayMetrics; 27import android.util.Log; 28import android.view.KeyEvent; 29import android.view.MotionEvent; 30import android.view.View; 31import android.view.ViewConfiguration; 32import android.view.WindowManager; 33import android.view.inputmethod.InputMethodManager; 34 35import com.android.launcher.R; 36 37import java.util.ArrayList; 38 39/** 40 * Class for initiating a drag within a view or across multiple views. 41 */ 42public class DragController { 43 @SuppressWarnings({"UnusedDeclaration"}) 44 private static final String TAG = "Launcher.DragController"; 45 46 /** Indicates the drag is a move. */ 47 public static int DRAG_ACTION_MOVE = 0; 48 49 /** Indicates the drag is a copy. */ 50 public static int DRAG_ACTION_COPY = 1; 51 52 private static final int SCROLL_DELAY = 600; 53 private static final int VIBRATE_DURATION = 35; 54 55 private static final boolean PROFILE_DRAWING_DURING_DRAG = false; 56 57 private static final int SCROLL_OUTSIDE_ZONE = 0; 58 private static final int SCROLL_WAITING_IN_ZONE = 1; 59 60 static final int SCROLL_NONE = -1; 61 static final int SCROLL_LEFT = 0; 62 static final int SCROLL_RIGHT = 1; 63 64 private Context mContext; 65 private Handler mHandler; 66 private final Vibrator mVibrator = new Vibrator(); 67 68 // temporaries to avoid gc thrash 69 private Rect mRectTemp = new Rect(); 70 private final int[] mCoordinatesTemp = new int[2]; 71 72 /** Whether or not we're dragging. */ 73 private boolean mDragging; 74 75 /** X coordinate of the down event. */ 76 private int mMotionDownX; 77 78 /** Y coordinate of the down event. */ 79 private int mMotionDownY; 80 81 /** Info about the screen for clamping. */ 82 private DisplayMetrics mDisplayMetrics = new DisplayMetrics(); 83 84 /** Original view that is being dragged. */ 85 private View mOriginator; 86 87 /** the area at the edge of the screen that makes the workspace go left 88 * or right while you're dragging. 89 */ 90 private int mScrollZone; 91 92 private DropTarget.DragObject mDragObject = new DropTarget.DragObject(); 93 94 /** Who can receive drop events */ 95 private ArrayList<DropTarget> mDropTargets = new ArrayList<DropTarget>(); 96 97 private ArrayList<DragListener> mListeners = new ArrayList<DragListener>(); 98 99 /** The window token used as the parent for the DragView. */ 100 private IBinder mWindowToken; 101 102 /** The view that will be scrolled when dragging to the left and right edges of the screen. */ 103 private View mScrollView; 104 105 private View mMoveTarget; 106 107 private DragScroller mDragScroller; 108 private int mScrollState = SCROLL_OUTSIDE_ZONE; 109 private ScrollRunnable mScrollRunnable = new ScrollRunnable(); 110 111 private RectF mDeleteRegion; 112 private DropTarget mLastDropTarget; 113 114 private InputMethodManager mInputMethodManager; 115 116 private int mLastTouch[] = new int[2]; 117 private int mDistanceSinceScroll = 0; 118 119 /** 120 * Interface to receive notifications when a drag starts or stops 121 */ 122 interface DragListener { 123 124 /** 125 * A drag has begun 126 * 127 * @param source An object representing where the drag originated 128 * @param info The data associated with the object that is being dragged 129 * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE} 130 * or {@link DragController#DRAG_ACTION_COPY} 131 */ 132 void onDragStart(DragSource source, Object info, int dragAction); 133 134 /** 135 * The drag has ended 136 */ 137 void onDragEnd(); 138 } 139 140 /** 141 * Used to create a new DragLayer from XML. 142 * 143 * @param context The application's context. 144 */ 145 public DragController(Context context) { 146 mContext = context; 147 mHandler = new Handler(); 148 mScrollZone = context.getResources().getDimensionPixelSize(R.dimen.scroll_zone); 149 } 150 151 public boolean dragging() { 152 return mDragging; 153 } 154 155 /** 156 * Starts a drag. 157 * 158 * @param v The view that is being dragged 159 * @param source An object representing where the drag originated 160 * @param dragInfo The data associated with the object that is being dragged 161 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 162 * {@link #DRAG_ACTION_COPY} 163 */ 164 public void startDrag(View v, DragSource source, Object dragInfo, int dragAction) { 165 startDrag(v, source, dragInfo, dragAction, null); 166 } 167 168 /** 169 * Starts a drag. 170 * 171 * @param v The view that is being dragged 172 * @param source An object representing where the drag originated 173 * @param dragInfo The data associated with the object that is being dragged 174 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 175 * {@link #DRAG_ACTION_COPY} 176 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 177 * Makes dragging feel more precise, e.g. you can clip out a transparent border 178 */ 179 public void startDrag(View v, DragSource source, Object dragInfo, int dragAction, 180 Rect dragRegion) { 181 mOriginator = v; 182 183 Bitmap b = getViewBitmap(v); 184 185 if (b == null) { 186 // out of memory? 187 return; 188 } 189 190 int[] loc = mCoordinatesTemp; 191 v.getLocationOnScreen(loc); 192 int screenX = loc[0]; 193 int screenY = loc[1]; 194 195 startDrag(b, screenX, screenY, source, dragInfo, dragAction, dragRegion); 196 b.recycle(); 197 198 if (dragAction == DRAG_ACTION_MOVE) { 199 v.setVisibility(View.GONE); 200 } 201 } 202 203 /** 204 * Starts a drag. 205 * 206 * @param v The view that is being dragged 207 * @param bmp The bitmap that represents the view being dragged 208 * @param source An object representing where the drag originated 209 * @param dragInfo The data associated with the object that is being dragged 210 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 211 * {@link #DRAG_ACTION_COPY} 212 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 213 * Makes dragging feel more precise, e.g. you can clip out a transparent border 214 */ 215 public void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction, 216 Rect dragRegion) { 217 mOriginator = v; 218 219 int[] loc = mCoordinatesTemp; 220 v.getLocationOnScreen(loc); 221 int screenX = loc[0]; 222 int screenY = loc[1]; 223 224 startDrag(bmp, screenX, screenY, source, dragInfo, dragAction, dragRegion); 225 226 if (dragAction == DRAG_ACTION_MOVE) { 227 v.setVisibility(View.GONE); 228 } 229 } 230 231 /** 232 * Starts a drag. 233 * 234 * @param b The bitmap to display as the drag image. It will be re-scaled to the 235 * enlarged size. 236 * @param screenX The x position on screen of the left-top of the bitmap. 237 * @param screenY The y position on screen of the left-top of the bitmap. 238 * @param source An object representing where the drag originated 239 * @param dragInfo The data associated with the object that is being dragged 240 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 241 * {@link #DRAG_ACTION_COPY} 242 */ 243 public void startDrag(Bitmap b, int screenX, int screenY, 244 DragSource source, Object dragInfo, int dragAction) { 245 startDrag(b, screenX, screenY, source, dragInfo, dragAction, null); 246 } 247 248 /** 249 * Starts a drag. 250 * 251 * @param b The bitmap to display as the drag image. It will be re-scaled to the 252 * enlarged size. 253 * @param screenX The x position on screen of the left-top of the bitmap. 254 * @param screenY The y position on screen of the left-top of the bitmap. 255 * @param source An object representing where the drag originated 256 * @param dragInfo The data associated with the object that is being dragged 257 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 258 * {@link #DRAG_ACTION_COPY} 259 * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. 260 * Makes dragging feel more precise, e.g. you can clip out a transparent border 261 */ 262 public void startDrag(Bitmap b, int screenX, int screenY, 263 DragSource source, Object dragInfo, int dragAction, Rect dragRegion) { 264 if (PROFILE_DRAWING_DURING_DRAG) { 265 android.os.Debug.startMethodTracing("Launcher"); 266 } 267 268 // Hide soft keyboard, if visible 269 if (mInputMethodManager == null) { 270 mInputMethodManager = (InputMethodManager) 271 mContext.getSystemService(Context.INPUT_METHOD_SERVICE); 272 } 273 mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); 274 275 for (DragListener listener : mListeners) { 276 listener.onDragStart(source, dragInfo, dragAction); 277 } 278 279 final int registrationX = ((int)mMotionDownX) - screenX; 280 final int registrationY = ((int)mMotionDownY) - screenY; 281 282 final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; 283 final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; 284 285 mDragging = true; 286 287 mDragObject.dragComplete = false; 288 mDragObject.xOffset = mMotionDownX - (screenX + dragRegionLeft); 289 mDragObject.yOffset = mMotionDownY - (screenY + dragRegionTop); 290 mDragObject.dragSource = source; 291 mDragObject.dragInfo = dragInfo; 292 293 mVibrator.vibrate(VIBRATE_DURATION); 294 295 final DragView dragView = mDragObject.dragView = new DragView(mContext, b, registrationX, 296 registrationY, 0, 0, b.getWidth(), b.getHeight()); 297 298 final DragSource dragSource = source; 299 dragView.setOnDrawRunnable(new Runnable() { 300 public void run() { 301 dragSource.onDragViewVisible(); 302 }; 303 }); 304 305 if (dragRegion != null) { 306 dragView.setDragRegion(new Rect(dragRegion)); 307 } 308 309 dragView.show(mWindowToken, (int)mMotionDownX, (int)mMotionDownY); 310 311 handleMoveEvent((int) mMotionDownX, (int) mMotionDownY); 312 } 313 314 /** 315 * Draw the view into a bitmap. 316 */ 317 Bitmap getViewBitmap(View v) { 318 v.clearFocus(); 319 v.setPressed(false); 320 321 boolean willNotCache = v.willNotCacheDrawing(); 322 v.setWillNotCacheDrawing(false); 323 324 // Reset the drawing cache background color to fully transparent 325 // for the duration of this operation 326 int color = v.getDrawingCacheBackgroundColor(); 327 v.setDrawingCacheBackgroundColor(0); 328 float alpha = v.getAlpha(); 329 v.setAlpha(1.0f); 330 331 if (color != 0) { 332 v.destroyDrawingCache(); 333 } 334 v.buildDrawingCache(); 335 Bitmap cacheBitmap = v.getDrawingCache(); 336 if (cacheBitmap == null) { 337 Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); 338 return null; 339 } 340 341 Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); 342 343 // Restore the view 344 v.destroyDrawingCache(); 345 v.setAlpha(alpha); 346 v.setWillNotCacheDrawing(willNotCache); 347 v.setDrawingCacheBackgroundColor(color); 348 349 return bitmap; 350 } 351 352 /** 353 * Call this from a drag source view like this: 354 * 355 * <pre> 356 * @Override 357 * public boolean dispatchKeyEvent(KeyEvent event) { 358 * return mDragController.dispatchKeyEvent(this, event) 359 * || super.dispatchKeyEvent(event); 360 * </pre> 361 */ 362 @SuppressWarnings({"UnusedDeclaration"}) 363 public boolean dispatchKeyEvent(KeyEvent event) { 364 return mDragging; 365 } 366 367 public boolean isDragging() { 368 return mDragging; 369 } 370 371 /** 372 * Stop dragging without dropping. 373 */ 374 public void cancelDrag() { 375 if (mDragging) { 376 // Should we also be calling onDragExit() here? 377 mDragObject.dragComplete = true; 378 mDragObject.dragSource.onDropCompleted(null, mDragObject, false); 379 } 380 endDrag(); 381 } 382 383 private void endDrag() { 384 if (mDragging) { 385 mDragging = false; 386 if (mOriginator != null) { 387 mOriginator.setVisibility(View.VISIBLE); 388 } 389 for (DragListener listener : mListeners) { 390 listener.onDragEnd(); 391 } 392 if (mDragObject.dragView != null) { 393 mDragObject.dragView.remove(); 394 mDragObject.dragView = null; 395 } 396 } 397 } 398 399 /** 400 * Call this from a drag source view. 401 */ 402 public boolean onInterceptTouchEvent(MotionEvent ev) { 403 if (false) { 404 Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" 405 + mDragging); 406 } 407 final int action = ev.getAction(); 408 409 if (action == MotionEvent.ACTION_DOWN) { 410 recordScreenSize(); 411 } 412 413 final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels); 414 final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels); 415 416 switch (action) { 417 case MotionEvent.ACTION_MOVE: 418 break; 419 case MotionEvent.ACTION_DOWN: 420 // Remember location of down touch 421 mMotionDownX = screenX; 422 mMotionDownY = screenY; 423 mLastDropTarget = null; 424 break; 425 case MotionEvent.ACTION_UP: 426 if (mDragging) { 427 drop(screenX, screenY); 428 } 429 endDrag(); 430 break; 431 case MotionEvent.ACTION_CANCEL: 432 cancelDrag(); 433 break; 434 } 435 436 return mDragging; 437 } 438 439 /** 440 * Sets the view that should handle move events. 441 */ 442 void setMoveTarget(View view) { 443 mMoveTarget = view; 444 } 445 446 public boolean dispatchUnhandledMove(View focused, int direction) { 447 return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); 448 } 449 450 private void handleMoveEvent(int x, int y) { 451 mDragObject.dragView.move(x, y); 452 453 // Drop on someone? 454 final int[] coordinates = mCoordinatesTemp; 455 DropTarget dropTarget = findDropTarget(x, y, coordinates); 456 mDragObject.x = coordinates[0]; 457 mDragObject.y = coordinates[1]; 458 if (dropTarget != null) { 459 DropTarget delegate = dropTarget.getDropTargetDelegate(mDragObject); 460 if (delegate != null) { 461 dropTarget = delegate; 462 } 463 464 if (mLastDropTarget != dropTarget) { 465 if (mLastDropTarget != null) { 466 mLastDropTarget.onDragExit(mDragObject); 467 } 468 dropTarget.onDragEnter(mDragObject); 469 } 470 dropTarget.onDragOver(mDragObject); 471 } else { 472 if (mLastDropTarget != null) { 473 mLastDropTarget.onDragExit(mDragObject); 474 } 475 } 476 mLastDropTarget = dropTarget; 477 478 // Scroll, maybe, but not if we're in the delete region. 479 boolean inDeleteRegion = false; 480 if (mDeleteRegion != null) { 481 inDeleteRegion = mDeleteRegion.contains(x, y); 482 } 483 484 // After a scroll, the touch point will still be in the scroll region. 485 // Rather than scrolling immediately, require a bit of twiddling to scroll again 486 final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop(); 487 mDistanceSinceScroll += 488 Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); 489 mLastTouch[0] = x; 490 mLastTouch[1] = y; 491 492 if (!inDeleteRegion && x < mScrollZone) { 493 if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { 494 mScrollState = SCROLL_WAITING_IN_ZONE; 495 mScrollRunnable.setDirection(SCROLL_LEFT); 496 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 497 mDragScroller.onEnterScrollArea(SCROLL_LEFT); 498 } 499 } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) { 500 if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { 501 mScrollState = SCROLL_WAITING_IN_ZONE; 502 mScrollRunnable.setDirection(SCROLL_RIGHT); 503 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 504 mDragScroller.onEnterScrollArea(SCROLL_RIGHT); 505 } 506 } else { 507 if (mScrollState == SCROLL_WAITING_IN_ZONE) { 508 mScrollState = SCROLL_OUTSIDE_ZONE; 509 mScrollRunnable.setDirection(SCROLL_RIGHT); 510 mHandler.removeCallbacks(mScrollRunnable); 511 mDragScroller.onExitScrollArea(); 512 } 513 } 514 } 515 516 /** 517 * Call this from a drag source view. 518 */ 519 public boolean onTouchEvent(MotionEvent ev) { 520 if (!mDragging) { 521 return false; 522 } 523 524 final int action = ev.getAction(); 525 final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels); 526 final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels); 527 528 switch (action) { 529 case MotionEvent.ACTION_DOWN: 530 // Remember where the motion event started 531 mMotionDownX = screenX; 532 mMotionDownY = screenY; 533 534 if ((screenX < mScrollZone) || (screenX > mScrollView.getWidth() - mScrollZone)) { 535 mScrollState = SCROLL_WAITING_IN_ZONE; 536 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 537 } else { 538 mScrollState = SCROLL_OUTSIDE_ZONE; 539 } 540 break; 541 case MotionEvent.ACTION_MOVE: 542 handleMoveEvent(screenX, screenY); 543 break; 544 case MotionEvent.ACTION_UP: 545 // Ensure that we've processed a move event at the current pointer location. 546 handleMoveEvent(screenX, screenY); 547 548 mHandler.removeCallbacks(mScrollRunnable); 549 if (mDragging) { 550 drop(screenX, screenY); 551 } 552 endDrag(); 553 break; 554 case MotionEvent.ACTION_CANCEL: 555 cancelDrag(); 556 break; 557 } 558 559 return true; 560 } 561 562 private void drop(float x, float y) { 563 final int[] coordinates = mCoordinatesTemp; 564 final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); 565 566 mDragObject.x = coordinates[0]; 567 mDragObject.y = coordinates[1]; 568 boolean accepted = false; 569 if (dropTarget != null) { 570 mDragObject.dragComplete = true; 571 dropTarget.onDragExit(mDragObject); 572 if (dropTarget.acceptDrop(mDragObject)) { 573 dropTarget.onDrop(mDragObject); 574 accepted = true; 575 } 576 } 577 mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, accepted); 578 } 579 580 private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 581 final Rect r = mRectTemp; 582 583 final ArrayList<DropTarget> dropTargets = mDropTargets; 584 final int count = dropTargets.size(); 585 for (int i=count-1; i>=0; i--) { 586 DropTarget target = dropTargets.get(i); 587 if (!target.isDropEnabled()) 588 continue; 589 590 target.getHitRect(r); 591 592 // Convert the hit rect to screen coordinates 593 target.getLocationOnScreen(dropCoordinates); 594 r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); 595 596 mDragObject.x = x; 597 mDragObject.y = y; 598 if (r.contains(x, y)) { 599 DropTarget delegate = target.getDropTargetDelegate(mDragObject); 600 if (delegate != null) { 601 target = delegate; 602 target.getLocationOnScreen(dropCoordinates); 603 } 604 605 // Make dropCoordinates relative to the DropTarget 606 dropCoordinates[0] = x - dropCoordinates[0]; 607 dropCoordinates[1] = y - dropCoordinates[1]; 608 609 return target; 610 } 611 } 612 return null; 613 } 614 615 /** 616 * Get the screen size so we can clamp events to the screen size so even if 617 * you drag off the edge of the screen, we find something. 618 */ 619 private void recordScreenSize() { 620 ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) 621 .getDefaultDisplay().getMetrics(mDisplayMetrics); 622 } 623 624 /** 625 * Clamp val to be >= min and < max. 626 */ 627 private static int clamp(int val, int min, int max) { 628 if (val < min) { 629 return min; 630 } else if (val >= max) { 631 return max - 1; 632 } else { 633 return val; 634 } 635 } 636 637 public void setDragScoller(DragScroller scroller) { 638 mDragScroller = scroller; 639 } 640 641 public void setWindowToken(IBinder token) { 642 mWindowToken = token; 643 } 644 645 /** 646 * Sets the drag listner which will be notified when a drag starts or ends. 647 */ 648 public void addDragListener(DragListener l) { 649 mListeners.add(l); 650 } 651 652 /** 653 * Remove a previously installed drag listener. 654 */ 655 public void removeDragListener(DragListener l) { 656 mListeners.remove(l); 657 } 658 659 /** 660 * Add a DropTarget to the list of potential places to receive drop events. 661 */ 662 public void addDropTarget(DropTarget target) { 663 mDropTargets.add(target); 664 } 665 666 /** 667 * Don't send drop events to <em>target</em> any more. 668 */ 669 public void removeDropTarget(DropTarget target) { 670 mDropTargets.remove(target); 671 } 672 673 /** 674 * Set which view scrolls for touch events near the edge of the screen. 675 */ 676 public void setScrollView(View v) { 677 mScrollView = v; 678 } 679 680 /** 681 * Specifies the delete region. We won't scroll on touch events over the delete region. 682 * 683 * @param region The rectangle in screen coordinates of the delete region. 684 */ 685 void setDeleteRegion(RectF region) { 686 mDeleteRegion = region; 687 } 688 689 DragView getDragView() { 690 return mDragObject.dragView; 691 } 692 693 private class ScrollRunnable implements Runnable { 694 private int mDirection; 695 696 ScrollRunnable() { 697 } 698 699 public void run() { 700 if (mDragScroller != null) { 701 if (mDirection == SCROLL_LEFT) { 702 mDragScroller.scrollLeft(); 703 } else { 704 mDragScroller.scrollRight(); 705 } 706 mScrollState = SCROLL_OUTSIDE_ZONE; 707 mDistanceSinceScroll = 0; 708 mDragScroller.onExitScrollArea(); 709 } 710 } 711 712 void setDirection(int direction) { 713 mDirection = direction; 714 } 715 } 716} 717