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