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