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.launcher2; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Rect; 22 import android.graphics.RectF; 23 import android.os.IBinder; 24 import android.os.Handler; 25 import android.os.Vibrator; 26 import android.util.DisplayMetrics; 27 import android.util.Log; 28 import android.view.View; 29 import android.view.KeyEvent; 30 import android.view.MotionEvent; 31 import android.view.WindowManager; 32 import android.view.inputmethod.InputMethodManager; 33 34 import java.util.ArrayList; 35 36 /** 37 * Class for initiating a drag within a view or across multiple views. 38 */ 39 public 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 DragListener mListener; 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 eneded 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 mOriginator = v; 163 164 Bitmap b = getViewBitmap(v); 165 166 if (b == null) { 167 // out of memory? 168 return; 169 } 170 171 int[] loc = mCoordinatesTemp; 172 v.getLocationOnScreen(loc); 173 int screenX = loc[0]; 174 int screenY = loc[1]; 175 176 startDrag(b, screenX, screenY, 0, 0, b.getWidth(), b.getHeight(), 177 source, dragInfo, dragAction); 178 179 b.recycle(); 180 181 if (dragAction == DRAG_ACTION_MOVE) { 182 v.setVisibility(View.GONE); 183 } 184 } 185 186 /** 187 * Starts a drag. 188 * 189 * @param b The bitmap to display as the drag image. It will be re-scaled to the 190 * enlarged size. 191 * @param screenX The x position on screen of the left-top of the bitmap. 192 * @param screenY The y position on screen of the left-top of the bitmap. 193 * @param textureLeft The left edge of the region inside b to use. 194 * @param textureTop The top edge of the region inside b to use. 195 * @param textureWidth The width of the region inside b to use. 196 * @param textureHeight The height of the region inside b to use. 197 * @param source An object representing where the drag originated 198 * @param dragInfo The data associated with the object that is being dragged 199 * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or 200 * {@link #DRAG_ACTION_COPY} 201 */ 202 public void startDrag(Bitmap b, int screenX, int screenY, 203 int textureLeft, int textureTop, int textureWidth, int textureHeight, 204 DragSource source, Object dragInfo, int dragAction) { 205 if (PROFILE_DRAWING_DURING_DRAG) { 206 android.os.Debug.startMethodTracing("Launcher"); 207 } 208 209 // Hide soft keyboard, if visible 210 if (mInputMethodManager == null) { 211 mInputMethodManager = (InputMethodManager) 212 mContext.getSystemService(Context.INPUT_METHOD_SERVICE); 213 } 214 mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); 215 216 if (mListener != null) { 217 mListener.onDragStart(source, dragInfo, dragAction); 218 } 219 220 int registrationX = ((int)mMotionDownX) - screenX; 221 int registrationY = ((int)mMotionDownY) - screenY; 222 223 mTouchOffsetX = mMotionDownX - screenX; 224 mTouchOffsetY = mMotionDownY - screenY; 225 226 mDragging = true; 227 mDragSource = source; 228 mDragInfo = dragInfo; 229 230 mVibrator.vibrate(VIBRATE_DURATION); 231 232 DragView dragView = mDragView = new DragView(mContext, b, registrationX, registrationY, 233 textureLeft, textureTop, textureWidth, textureHeight); 234 dragView.show(mWindowToken, (int)mMotionDownX, (int)mMotionDownY); 235 } 236 237 /** 238 * Draw the view into a bitmap. 239 */ 240 private Bitmap getViewBitmap(View v) { 241 v.clearFocus(); 242 v.setPressed(false); 243 244 boolean willNotCache = v.willNotCacheDrawing(); 245 v.setWillNotCacheDrawing(false); 246 247 // Reset the drawing cache background color to fully transparent 248 // for the duration of this operation 249 int color = v.getDrawingCacheBackgroundColor(); 250 v.setDrawingCacheBackgroundColor(0); 251 252 if (color != 0) { 253 v.destroyDrawingCache(); 254 } 255 v.buildDrawingCache(); 256 Bitmap cacheBitmap = v.getDrawingCache(); 257 if (cacheBitmap == null) { 258 Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); 259 return null; 260 } 261 262 Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); 263 264 // Restore the view 265 v.destroyDrawingCache(); 266 v.setWillNotCacheDrawing(willNotCache); 267 v.setDrawingCacheBackgroundColor(color); 268 269 return bitmap; 270 } 271 272 /** 273 * Call this from a drag source view like this: 274 * 275 * <pre> 276 * @Override 277 * public boolean dispatchKeyEvent(KeyEvent event) { 278 * return mDragController.dispatchKeyEvent(this, event) 279 * || super.dispatchKeyEvent(event); 280 * </pre> 281 */ 282 @SuppressWarnings({"UnusedDeclaration"}) 283 public boolean dispatchKeyEvent(KeyEvent event) { 284 return mDragging; 285 } 286 287 /** 288 * Stop dragging without dropping. 289 */ 290 public void cancelDrag() { 291 endDrag(); 292 } 293 294 private void endDrag() { 295 if (mDragging) { 296 mDragging = false; 297 if (mOriginator != null) { 298 mOriginator.setVisibility(View.VISIBLE); 299 } 300 if (mListener != null) { 301 mListener.onDragEnd(); 302 } 303 if (mDragView != null) { 304 mDragView.remove(); 305 mDragView = null; 306 } 307 } 308 } 309 310 /** 311 * Call this from a drag source view. 312 */ 313 public boolean onInterceptTouchEvent(MotionEvent ev) { 314 if (false) { 315 Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" 316 + mDragging); 317 } 318 final int action = ev.getAction(); 319 320 if (action == MotionEvent.ACTION_DOWN) { 321 recordScreenSize(); 322 } 323 324 final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels); 325 final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels); 326 327 switch (action) { 328 case MotionEvent.ACTION_MOVE: 329 break; 330 331 case MotionEvent.ACTION_DOWN: 332 // Remember location of down touch 333 mMotionDownX = screenX; 334 mMotionDownY = screenY; 335 mLastDropTarget = null; 336 break; 337 338 case MotionEvent.ACTION_CANCEL: 339 case MotionEvent.ACTION_UP: 340 if (mDragging) { 341 drop(screenX, screenY); 342 } 343 endDrag(); 344 break; 345 } 346 347 return mDragging; 348 } 349 350 /** 351 * Sets the view that should handle move events. 352 */ 353 void setMoveTarget(View view) { 354 mMoveTarget = view; 355 } 356 357 public boolean dispatchUnhandledMove(View focused, int direction) { 358 return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); 359 } 360 361 /** 362 * Call this from a drag source view. 363 */ 364 public boolean onTouchEvent(MotionEvent ev) { 365 View scrollView = mScrollView; 366 367 if (!mDragging) { 368 return false; 369 } 370 371 final int action = ev.getAction(); 372 final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels); 373 final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels); 374 375 switch (action) { 376 case MotionEvent.ACTION_DOWN: 377 // Remember where the motion event started 378 mMotionDownX = screenX; 379 mMotionDownY = screenY; 380 381 if ((screenX < SCROLL_ZONE) || (screenX > scrollView.getWidth() - SCROLL_ZONE)) { 382 mScrollState = SCROLL_WAITING_IN_ZONE; 383 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 384 } else { 385 mScrollState = SCROLL_OUTSIDE_ZONE; 386 } 387 388 break; 389 case MotionEvent.ACTION_MOVE: 390 // Update the drag view. Don't use the clamped pos here so the dragging looks 391 // like it goes off screen a little, intead of bumping up against the edge. 392 mDragView.move((int)ev.getRawX(), (int)ev.getRawY()); 393 394 // Drop on someone? 395 final int[] coordinates = mCoordinatesTemp; 396 DropTarget dropTarget = findDropTarget(screenX, screenY, coordinates); 397 if (dropTarget != null) { 398 if (mLastDropTarget == dropTarget) { 399 dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], 400 (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); 401 } else { 402 if (mLastDropTarget != null) { 403 mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], 404 (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); 405 } 406 dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], 407 (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); 408 } 409 } else { 410 if (mLastDropTarget != null) { 411 mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], 412 (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); 413 } 414 } 415 mLastDropTarget = dropTarget; 416 417 // Scroll, maybe, but not if we're in the delete region. 418 boolean inDeleteRegion = false; 419 if (mDeleteRegion != null) { 420 inDeleteRegion = mDeleteRegion.contains(screenX, screenY); 421 } 422 if (!inDeleteRegion && screenX < SCROLL_ZONE) { 423 if (mScrollState == SCROLL_OUTSIDE_ZONE) { 424 mScrollState = SCROLL_WAITING_IN_ZONE; 425 mScrollRunnable.setDirection(SCROLL_LEFT); 426 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 427 } 428 } else if (!inDeleteRegion && screenX > scrollView.getWidth() - SCROLL_ZONE) { 429 if (mScrollState == SCROLL_OUTSIDE_ZONE) { 430 mScrollState = SCROLL_WAITING_IN_ZONE; 431 mScrollRunnable.setDirection(SCROLL_RIGHT); 432 mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); 433 } 434 } else { 435 if (mScrollState == SCROLL_WAITING_IN_ZONE) { 436 mScrollState = SCROLL_OUTSIDE_ZONE; 437 mScrollRunnable.setDirection(SCROLL_RIGHT); 438 mHandler.removeCallbacks(mScrollRunnable); 439 } 440 } 441 442 break; 443 case MotionEvent.ACTION_UP: 444 mHandler.removeCallbacks(mScrollRunnable); 445 if (mDragging) { 446 drop(screenX, screenY); 447 } 448 endDrag(); 449 450 break; 451 case MotionEvent.ACTION_CANCEL: 452 cancelDrag(); 453 } 454 455 return true; 456 } 457 458 private boolean drop(float x, float y) { 459 final int[] coordinates = mCoordinatesTemp; 460 DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); 461 462 if (dropTarget != null) { 463 dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], 464 (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); 465 if (dropTarget.acceptDrop(mDragSource, coordinates[0], coordinates[1], 466 (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo)) { 467 dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1], 468 (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); 469 mDragSource.onDropCompleted((View) dropTarget, true); 470 return true; 471 } else { 472 mDragSource.onDropCompleted((View) dropTarget, false); 473 return true; 474 } 475 } 476 return false; 477 } 478 479 private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { 480 final Rect r = mRectTemp; 481 482 final ArrayList<DropTarget> dropTargets = mDropTargets; 483 final int count = dropTargets.size(); 484 for (int i=count-1; i>=0; i--) { 485 final DropTarget target = dropTargets.get(i); 486 target.getHitRect(r); 487 target.getLocationOnScreen(dropCoordinates); 488 r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); 489 if (r.contains(x, y)) { 490 dropCoordinates[0] = x - dropCoordinates[0]; 491 dropCoordinates[1] = y - dropCoordinates[1]; 492 return target; 493 } 494 } 495 return null; 496 } 497 498 /** 499 * Get the screen size so we can clamp events to the screen size so even if 500 * you drag off the edge of the screen, we find something. 501 */ 502 private void recordScreenSize() { 503 ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) 504 .getDefaultDisplay().getMetrics(mDisplayMetrics); 505 } 506 507 /** 508 * Clamp val to be >= min and < max. 509 */ 510 private static int clamp(int val, int min, int max) { 511 if (val < min) { 512 return min; 513 } else if (val >= max) { 514 return max - 1; 515 } else { 516 return val; 517 } 518 } 519 520 public void setDragScoller(DragScroller scroller) { 521 mDragScroller = scroller; 522 } 523 524 public void setWindowToken(IBinder token) { 525 mWindowToken = token; 526 } 527 528 /** 529 * Sets the drag listner which will be notified when a drag starts or ends. 530 */ 531 public void setDragListener(DragListener l) { 532 mListener = l; 533 } 534 535 /** 536 * Remove a previously installed drag listener. 537 */ 538 public void removeDragListener(DragListener l) { 539 mListener = null; 540 } 541 542 /** 543 * Add a DropTarget to the list of potential places to receive drop events. 544 */ 545 public void addDropTarget(DropTarget target) { 546 mDropTargets.add(target); 547 } 548 549 /** 550 * Don't send drop events to <em>target</em> any more. 551 */ 552 public void removeDropTarget(DropTarget target) { 553 mDropTargets.remove(target); 554 } 555 556 /** 557 * Set which view scrolls for touch events near the edge of the screen. 558 */ 559 public void setScrollView(View v) { 560 mScrollView = v; 561 } 562 563 /** 564 * Specifies the delete region. We won't scroll on touch events over the delete region. 565 * 566 * @param region The rectangle in screen coordinates of the delete region. 567 */ 568 void setDeleteRegion(RectF region) { 569 mDeleteRegion = region; 570 } 571 572 private class ScrollRunnable implements Runnable { 573 private int mDirection; 574 575 ScrollRunnable() { 576 } 577 578 public void run() { 579 if (mDragScroller != null) { 580 if (mDirection == SCROLL_LEFT) { 581 mDragScroller.scrollLeft(); 582 } else { 583 mDragScroller.scrollRight(); 584 } 585 mScrollState = SCROLL_OUTSIDE_ZONE; 586 } 587 } 588 589 void setDirection(int direction) { 590 mDirection = direction; 591 } 592 } 593 } 594