ViewDragHelper.java revision 1fbad11af8f178d9fcee85dabe7cd8f24d2bc9a2
1/* 2 * Copyright (C) 2013 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 18package android.support.v4.widget; 19 20import android.content.Context; 21import android.support.v4.view.MotionEventCompat; 22import android.support.v4.view.VelocityTrackerCompat; 23import android.support.v4.view.ViewCompat; 24import android.view.MotionEvent; 25import android.view.VelocityTracker; 26import android.view.View; 27import android.view.ViewConfiguration; 28import android.view.ViewGroup; 29import android.view.animation.Interpolator; 30 31import java.util.Arrays; 32 33/** 34 * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number 35 * of useful operations and state tracking for allowing a user to drag and reposition 36 * views within their parent ViewGroup. 37 */ 38public class ViewDragHelper { 39 private static final String TAG = "ViewDragHelper"; 40 41 /** 42 * A null/invalid pointer ID. 43 */ 44 public static final int INVALID_POINTER = -1; 45 46 /** 47 * A view is not currently being dragged or animating as a result of a fling/snap. 48 */ 49 public static final int STATE_IDLE = 0; 50 51 /** 52 * A view is currently being dragged. The position is currently changing as a result 53 * of user input or simulated user input. 54 */ 55 public static final int STATE_DRAGGING = 1; 56 57 /** 58 * A view is currently settling into place as a result of a fling or 59 * predefined non-interactive motion. 60 */ 61 public static final int STATE_SETTLING = 2; 62 63 /** 64 * Edge flag indicating that the left edge should be affected. 65 */ 66 public static final int EDGE_LEFT = 1 << 0; 67 68 /** 69 * Edge flag indicating that the right edge should be affected. 70 */ 71 public static final int EDGE_RIGHT = 1 << 1; 72 73 /** 74 * Edge flag indicating that the top edge should be affected. 75 */ 76 public static final int EDGE_TOP = 1 << 2; 77 78 /** 79 * Edge flag indicating that the bottom edge should be affected. 80 */ 81 public static final int EDGE_BOTTOM = 1 << 3; 82 83 private static final int EDGE_SIZE = 16; // dp 84 85 private static final int BASE_SETTLE_DURATION = 256; // ms 86 private static final int MAX_SETTLE_DURATION = 600; // ms 87 88 // Current drag state; idle, dragging or settling 89 private int mDragState; 90 91 // Distance to travel before a drag may begin 92 private int mTouchSlop; 93 94 // Last known position/pointer tracking 95 private int mActivePointerId = INVALID_POINTER; 96 private float[] mInitialMotionX; 97 private float[] mInitialMotionY; 98 private float[] mLastMotionX; 99 private float[] mLastMotionY; 100 private int[] mInitialEdgesTouched; 101 private int[] mEdgeDragsInProgress; 102 103 private VelocityTracker mVelocityTracker; 104 private float mMaxVelocity; 105 private float mMinVelocity; 106 107 private int mEdgeSize; 108 private int mTrackingEdges; 109 110 private ScrollerCompat mScroller; 111 112 private final Callback mCallback; 113 114 private View mCapturedView; 115 private boolean mReleaseInProgress; 116 117 private final ViewGroup mParentView; 118 119 /** 120 * A Callback is used as a communication channel with the ViewDragHelper back to the 121 * parent view using it. <code>on*</code>methods are invoked on siginficant events and several 122 * accessor methods are expected to provide the ViewDragHelper with more information 123 * about the state of the parent view upon request. The callback also makes decisions 124 * governing the range and draggability of child views. 125 */ 126 public static abstract class Callback { 127 /** 128 * Called when the drag state changes. See the <code>STATE_*</code> constants 129 * for more information. 130 * 131 * @param state The new drag state 132 * 133 * @see #STATE_IDLE 134 * @see #STATE_DRAGGING 135 * @see #STATE_SETTLING 136 */ 137 public void onViewDragStateChanged(int state) {} 138 139 /** 140 * Called when the captured view's position changes as the result of a drag or settle. 141 * 142 * @param changedView View whose position changed 143 * @param left New X coordinate of the left edge of the view 144 * @param top New Y coordinate of the top edge of the view 145 * @param dx Change in X position from the last call 146 * @param dy Change in Y position from the last call 147 */ 148 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {} 149 150 /** 151 * Called when a child view is captured for dragging or settling. The ID of the pointer 152 * currently dragging the captured view is supplied. If activePointerId is 153 * identified as {@link #INVALID_POINTER} the capture is programmatic instead of 154 * pointer-initiated. 155 * 156 * @param capturedChild Child view that was captured 157 * @param activePointerId Pointer id tracking the child capture 158 */ 159 public void onViewCaptured(View capturedChild, int activePointerId) {} 160 161 /** 162 * Called when the child view is no longer being actively dragged. 163 * The fling velocity is also supplied, if relevant. The velocity values may 164 * be clamped to system minimums or maximums. 165 * 166 * <p>Calling code may decide to fling or otherwise release the view to let it 167 * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)} 168 * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes 169 * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} 170 * and the view capture will not fully end until it comes to a complete stop. 171 * If neither of these methods is invoked before <code>onViewReleased</code> returns, 172 * the view will stop in place and the ViewDragHelper will return to 173 * {@link #STATE_IDLE}.</p> 174 * 175 * @param releasedChild The captured child view now being released 176 * @param xvel X velocity of the pointer as it left the screen in pixels per second. 177 * @param yvel Y velocity of the pointer as it left the screen in pixels per second. 178 */ 179 public void onViewReleased(View releasedChild, float xvel, float yvel) {} 180 181 /** 182 * Called when one of the subscribed edges in the parent view has been touched 183 * by the user while no child view is currently captured. 184 * 185 * @param edgeFlags A combination of edge flags describing the edge(s) currently touched 186 * @param pointerId ID of the pointer touching the described edge(s) 187 * @see #EDGE_LEFT 188 * @see #EDGE_TOP 189 * @see #EDGE_RIGHT 190 * @see #EDGE_BOTTOM 191 */ 192 public void onEdgeTouched(int edgeFlags, int pointerId) {} 193 194 /** 195 * Called when the user has started a deliberate drag away from one 196 * of the subscribed edges in the parent view while no child view is currently captured. 197 * 198 * @param edgeFlags A combination of edge flags describing the edge(s) dragged 199 * @param pointerId ID of the pointer touching the described edge(s) 200 * @see #EDGE_LEFT 201 * @see #EDGE_TOP 202 * @see #EDGE_RIGHT 203 * @see #EDGE_BOTTOM 204 */ 205 public void onEdgeDragStarted(int edgeFlags, int pointerId) {} 206 207 /** 208 * Called to determine the Z-order of child views. 209 * 210 * @param index the ordered position to query for 211 * @return index of the view that should be ordered at position <code>index</code> 212 */ 213 public int getOrderedChildIndex(int index) { 214 return index; 215 } 216 217 /** 218 * Return the magnitude of a draggable child view's horizontal range of motion in pixels. 219 * This method should return 0 for views that cannot move horizontally. 220 * 221 * @param child Child view to check 222 * @return range of horizontal motion in pixels 223 */ 224 public int getViewHorizontalDragRange(View child) { 225 return 0; 226 } 227 228 /** 229 * Return the magnitude of a draggable child view's vertical range of motion in pixels. 230 * This method should return 0 for views that cannot move vertically. 231 * 232 * @param child Child view to check 233 * @return range of vertical motion in pixels 234 */ 235 public int getViewVerticalDragRange(View child) { 236 return 0; 237 } 238 239 /** 240 * Called when the user's input indicates that they want to capture the given child view 241 * with the pointer indicated by pointerId. The callback should return true if the user 242 * is permitted to drag the given view with the indicated pointer. 243 * 244 * <p>ViewDragHelper may call this method multiple times for the same view even if 245 * the view is already captured; this indicates that a new pointer is trying to take 246 * control of the view.</p> 247 * 248 * <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} 249 * will follow if the capture is successful.</p> 250 * 251 * @param child Child the user is attempting to capture 252 * @param pointerId ID of the pointer attempting the capture 253 * @return true if capture should be allowed, false otherwise 254 */ 255 public abstract boolean tryCaptureView(View child, int pointerId); 256 257 /** 258 * Restrict the motion of the dragged child view along the horizontal axis. 259 * The default implementation does not allow horizontal motion; the extending 260 * class must override this method and provide the desired clamping. 261 * 262 * 263 * @param child Child view being dragged 264 * @param left Attempted motion along the X axis 265 * @param dx Proposed change in position for left 266 * @return The new clamped position for left 267 */ 268 public int clampViewPositionHorizontal(View child, int left, int dx) { 269 return 0; 270 } 271 272 /** 273 * Restrict the motion of the dragged child view along the vertical axis. 274 * The default implementation does not allow vertical motion; the extending 275 * class must override this method and provide the desired clamping. 276 * 277 * 278 * @param child Child view being dragged 279 * @param top Attempted motion along the Y axis 280 * @param dy Proposed change in position for top 281 * @return The new clamped position for top 282 */ 283 public int clampViewPositionVertical(View child, int top, int dy) { 284 return 0; 285 } 286 } 287 288 /** 289 * Interpolator defining the animation curve for mScroller 290 */ 291 private static final Interpolator sInterpolator = new Interpolator() { 292 public float getInterpolation(float t) { 293 t -= 1.0f; 294 return t * t * t * t * t + 1.0f; 295 } 296 }; 297 298 private final Runnable mSetIdleRunnable = new Runnable() { 299 public void run() { 300 setDragState(STATE_IDLE); 301 } 302 }; 303 304 /** 305 * Factory method to create a new ViewDragHelper. 306 * 307 * @param forParent Parent view to monitor 308 * @param cb Callback to provide information and receive events 309 * @return a new ViewDragHelper instance 310 */ 311 public static ViewDragHelper create(ViewGroup forParent, Callback cb) { 312 return new ViewDragHelper(forParent.getContext(), forParent, cb); 313 } 314 315 /** 316 * Factory method to create a new ViewDragHelper. 317 * 318 * @param forParent Parent view to monitor 319 * @param sensitivity Multiplier for how sensitive the helper should be about detecting 320 * the start of a drag. Larger values are more sensitive. 1.0f is normal. 321 * @param cb Callback to provide information and receive events 322 * @return a new ViewDragHelper instance 323 */ 324 public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { 325 final ViewDragHelper helper = create(forParent, cb); 326 helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); 327 return helper; 328 } 329 330 /** 331 * Apps should use ViewDragHelper.create() to get a new instance. 332 * This will allow VDH to use internal compatibility implementations for different 333 * platform versions. 334 * 335 * @param context Context to initialize config-dependent params from 336 * @param forParent Parent view to monitor 337 */ 338 private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { 339 if (forParent == null) { 340 throw new IllegalArgumentException("Parent view may not be null"); 341 } 342 if (cb == null) { 343 throw new IllegalArgumentException("Callback may not be null"); 344 } 345 346 mParentView = forParent; 347 mCallback = cb; 348 349 final ViewConfiguration vc = ViewConfiguration.get(context); 350 final float density = context.getResources().getDisplayMetrics().density; 351 mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); 352 353 mTouchSlop = vc.getScaledTouchSlop(); 354 mMaxVelocity = vc.getScaledMaximumFlingVelocity(); 355 mMinVelocity = vc.getScaledMinimumFlingVelocity(); 356 mScroller = ScrollerCompat.create(context, sInterpolator); 357 } 358 359 /** 360 * Retrieve the current drag state of this helper. This will return one of 361 * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. 362 * @return The current drag state 363 */ 364 public int getViewDragState() { 365 return mDragState; 366 } 367 368 /** 369 * Enable edge tracking for the selected edges of the parent view. 370 * The callback's {@link Callback#onEdgeTouched(int, int)} and 371 * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked 372 * for edges for which edge tracking has been enabled. 373 * 374 * @param edgeFlags Combination of edge flags describing the edges to watch 375 * @see #EDGE_LEFT 376 * @see #EDGE_TOP 377 * @see #EDGE_RIGHT 378 * @see #EDGE_BOTTOM 379 */ 380 public void setEdgeTrackingEnabled(int edgeFlags) { 381 mTrackingEdges = edgeFlags; 382 } 383 384 /** 385 * Capture a specific child view for dragging within the parent. The callback will be notified 386 * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to 387 * capture this view. 388 * 389 * @param childView Child view to capture 390 * @param activePointerId ID of the pointer that is dragging the captured child view 391 */ 392 public void captureChildView(View childView, int activePointerId) { 393 if (childView.getParent() != mParentView) { 394 throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + 395 "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); 396 } 397 398 mCapturedView = childView; 399 mActivePointerId = activePointerId; 400 mCallback.onViewCaptured(childView, activePointerId); 401 setDragState(STATE_DRAGGING); 402 } 403 404 /** 405 * @return The currently captured view, or null if no view has been captured. 406 */ 407 public View getCapturedView() { 408 return mCapturedView; 409 } 410 411 /** 412 * @return The ID of the pointer currently dragging the captured view, 413 * or {@link #INVALID_POINTER}. 414 */ 415 public int getActivePointerId() { 416 return mActivePointerId; 417 } 418 419 /** 420 * @return The minimum distance in pixels that the user must travel to initiate a drag 421 */ 422 public int getTouchSlop() { 423 return mTouchSlop; 424 } 425 426 /** 427 * The result of a call to this method is equivalent to 428 * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. 429 */ 430 public void cancel() { 431 mActivePointerId = INVALID_POINTER; 432 clearMotionHistory(); 433 434 if (mVelocityTracker != null) { 435 mVelocityTracker.recycle(); 436 mVelocityTracker = null; 437 } 438 } 439 440 /** 441 * {@link #cancel()}, but also abort all motion in progress and snap to the end of any 442 * animation. 443 */ 444 public void abort() { 445 cancel(); 446 if (mDragState == STATE_SETTLING) { 447 final int oldX = mScroller.getCurrX(); 448 final int oldY = mScroller.getCurrY(); 449 mScroller.abortAnimation(); 450 final int newX = mScroller.getCurrX(); 451 final int newY = mScroller.getCurrY(); 452 mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); 453 } 454 setDragState(STATE_IDLE); 455 } 456 457 /** 458 * Animate the view <code>child</code> to the given (left, top) position. 459 * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} 460 * on each subsequent frame to continue the motion until it returns false. If this method 461 * returns false there is no further work to do to complete the movement. 462 * 463 * <p>This operation does not count as a capture event, though {@link #getCapturedView()} 464 * will still report the sliding view while the slide is in progress.</p> 465 * 466 * @param child Child view to capture and animate 467 * @param finalLeft Final left position of child 468 * @param finalTop Final top position of child 469 * @return true if animation should continue through {@link #continueSettling(boolean)} calls 470 */ 471 public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { 472 mCapturedView = child; 473 mActivePointerId = INVALID_POINTER; 474 475 return forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); 476 } 477 478 /** 479 * Settle the captured view at the given (left, top) position. 480 * The appropriate velocity from prior motion will be taken into account. 481 * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} 482 * on each subsequent frame to continue the motion until it returns false. If this method 483 * returns false there is no further work to do to complete the movement. 484 * 485 * @param finalLeft Settled left edge position for the captured view 486 * @param finalTop Settled top edge position for the captured view 487 * @return true if animation should continue through {@link #continueSettling(boolean)} calls 488 */ 489 public boolean settleCapturedViewAt(int finalLeft, int finalTop) { 490 if (!mReleaseInProgress) { 491 throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + 492 "Callback#onViewReleased"); 493 } 494 495 return forceSettleCapturedViewAt(finalLeft, finalTop, 496 (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 497 (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); 498 } 499 500 /** 501 * Settle the captured view at the given (left, top) position. 502 * 503 * @param finalLeft Target left position for the captured view 504 * @param finalTop Target top position for the captured view 505 * @param xvel Horizontal velocity 506 * @param yvel Vertical velocity 507 * @return true if animation should continue through {@link #continueSettling(boolean)} calls 508 */ 509 private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { 510 final int startLeft = mCapturedView.getLeft(); 511 final int startTop = mCapturedView.getTop(); 512 final int dx = finalLeft - startLeft; 513 final int dy = finalTop - startTop; 514 515 if (dx == 0 && dy == 0) { 516 // Nothing to do. Send callbacks, be done. 517 mScroller.abortAnimation(); 518 setDragState(STATE_IDLE); 519 return false; 520 } 521 522 final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); 523 mScroller.startScroll(startLeft, startTop, dx, dy, duration); 524 525 setDragState(STATE_SETTLING); 526 return true; 527 } 528 529 private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { 530 xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); 531 yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); 532 final int absDx = Math.abs(dx); 533 final int absDy = Math.abs(dy); 534 final int absXVel = Math.abs(xvel); 535 final int absYVel = Math.abs(yvel); 536 final int addedVel = absXVel + absYVel; 537 final int addedDistance = absDx + absDy; 538 539 final float xweight = xvel != 0 ? (float) absXVel / addedVel : 540 (float) absDx / addedDistance; 541 final float yweight = yvel != 0 ? (float) absYVel / addedVel : 542 (float) absDy / addedDistance; 543 544 int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); 545 int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); 546 547 return (int) (xduration * xweight + yduration * yweight); 548 } 549 550 private int computeAxisDuration(int delta, int velocity, int motionRange) { 551 if (delta == 0) { 552 return 0; 553 } 554 555 final int width = mParentView.getWidth(); 556 final int halfWidth = width / 2; 557 final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); 558 final float distance = halfWidth + halfWidth * 559 distanceInfluenceForSnapDuration(distanceRatio); 560 561 int duration; 562 velocity = Math.abs(velocity); 563 if (velocity > 0) { 564 duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); 565 } else { 566 final float range = (float) Math.abs(delta) / motionRange; 567 duration = (int) ((range + 1) * BASE_SETTLE_DURATION); 568 } 569 return Math.min(duration, MAX_SETTLE_DURATION); 570 } 571 572 /** 573 * Clamp the magnitude of value for absMin and absMax. 574 * If the value is below the minimum, it will be clamped to zero. 575 * If the value is above the maximum, it will be clamped to the maximum. 576 * 577 * @param value Value to clamp 578 * @param absMin Absolute value of the minimum significant value to return 579 * @param absMax Absolute value of the maximum value to return 580 * @return The clamped value with the same sign as <code>value</code> 581 */ 582 private int clampMag(int value, int absMin, int absMax) { 583 final int absValue = Math.abs(value); 584 if (absValue < absMin) return 0; 585 if (absValue > absMax) return value > 0 ? absMax : -absMax; 586 return value; 587 } 588 589 private float distanceInfluenceForSnapDuration(float f) { 590 f -= 0.5f; // center the values about 0. 591 f *= 0.3f * Math.PI / 2.0f; 592 return (float) Math.sin(f); 593 } 594 595 /** 596 * Settle the captured view based on standard free-moving fling behavior. 597 * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame 598 * to continue the motion until it returns false. 599 * 600 * @param minLeft Minimum X position for the view's left edge 601 * @param minTop Minimum Y position for the view's top edge 602 * @param maxLeft Maximum X position for the view's left edge 603 * @param maxTop Maximum Y position for the view's top edge 604 */ 605 public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { 606 if (!mReleaseInProgress) { 607 throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + 608 "Callback#onViewReleased"); 609 } 610 611 mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), 612 (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 613 (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), 614 minLeft, maxLeft, minTop, maxTop); 615 616 setDragState(STATE_SETTLING); 617 } 618 619 /** 620 * Move the captured settling view by the appropriate amount for the current time. 621 * If <code>continueSettling</code> returns true, the caller should call it again 622 * on the next frame to continue. 623 * 624 * @param deferCallbacks true if state callbacks should be deferred via posted message. 625 * Set this to true if you are calling this method from 626 * {@link android.view.View#computeScroll()} or similar methods 627 * invoked as part of layout or drawing. 628 * @return true if settle is still in progress 629 */ 630 public boolean continueSettling(boolean deferCallbacks) { 631 if (mDragState == STATE_SETTLING) { 632 boolean keepGoing = mScroller.computeScrollOffset(); 633 final int x = mScroller.getCurrX(); 634 final int y = mScroller.getCurrY(); 635 final int dx = x - mCapturedView.getLeft(); 636 final int dy = y - mCapturedView.getTop(); 637 638 if (dx != 0) { 639 mCapturedView.offsetLeftAndRight(dx); 640 } 641 if (dy != 0) { 642 mCapturedView.offsetTopAndBottom(dy); 643 } 644 645 if (dx != 0 || dy != 0) { 646 mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); 647 } 648 649 if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { 650 // Close enough. The interpolator/scroller might think we're still moving 651 // but the user sure doesn't. 652 mScroller.abortAnimation(); 653 keepGoing = mScroller.isFinished(); 654 } 655 656 if (!keepGoing) { 657 if (deferCallbacks) { 658 mParentView.post(mSetIdleRunnable); 659 } else { 660 setDragState(STATE_IDLE); 661 } 662 } 663 } 664 665 return mDragState == STATE_SETTLING; 666 } 667 668 /** 669 * Like all callback events this must happen on the UI thread, but release 670 * involves some extra semantics. During a release (mReleaseInProgress) 671 * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)} 672 * or {@link #flingCapturedView(int, int, int, int)}. 673 */ 674 private void dispatchViewReleased(float xvel, float yvel) { 675 mReleaseInProgress = true; 676 mCallback.onViewReleased(mCapturedView, xvel, yvel); 677 mReleaseInProgress = false; 678 679 if (mDragState == STATE_DRAGGING) { 680 // onViewReleased didn't call a method that would have changed this. Go idle. 681 setDragState(STATE_IDLE); 682 } 683 } 684 685 private void clearMotionHistory() { 686 if (mInitialMotionX == null) { 687 return; 688 } 689 Arrays.fill(mInitialMotionX, 0); 690 Arrays.fill(mInitialMotionY, 0); 691 Arrays.fill(mLastMotionX, 0); 692 Arrays.fill(mLastMotionY, 0); 693 Arrays.fill(mInitialEdgesTouched, 0); 694 Arrays.fill(mEdgeDragsInProgress, 0); 695 } 696 697 private void clearMotionHistory(int pointerId) { 698 if (mInitialMotionX == null) { 699 return; 700 } 701 mInitialMotionX[pointerId] = 0; 702 mInitialMotionY[pointerId] = 0; 703 mLastMotionX[pointerId] = 0; 704 mLastMotionY[pointerId] = 0; 705 mInitialEdgesTouched[pointerId] = 0; 706 mEdgeDragsInProgress[pointerId] = 0; 707 } 708 709 private void ensureMotionHistorySizeForId(int pointerId) { 710 if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) { 711 float[] imx = new float[pointerId + 1]; 712 float[] imy = new float[pointerId + 1]; 713 float[] lmx = new float[pointerId + 1]; 714 float[] lmy = new float[pointerId + 1]; 715 int[] iit = new int[pointerId + 1]; 716 int[] edip = new int[pointerId + 1]; 717 718 if (mInitialMotionX != null) { 719 System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length); 720 System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length); 721 System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length); 722 System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length); 723 System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length); 724 System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length); 725 } 726 727 mInitialMotionX = imx; 728 mInitialMotionY = imy; 729 mLastMotionX = lmx; 730 mLastMotionY = lmy; 731 mInitialEdgesTouched = iit; 732 mEdgeDragsInProgress = edip; 733 } 734 } 735 736 private void saveInitialMotion(float x, float y, int pointerId) { 737 ensureMotionHistorySizeForId(pointerId); 738 mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; 739 mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; 740 mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y); 741 } 742 743 private void saveLastMotion(MotionEvent ev) { 744 final int pointerCount = MotionEventCompat.getPointerCount(ev); 745 for (int i = 0; i < pointerCount; i++) { 746 final int pointerId = MotionEventCompat.getPointerId(ev, i); 747 final float x = MotionEventCompat.getX(ev, i); 748 final float y = MotionEventCompat.getY(ev, i); 749 mLastMotionX[pointerId] = x; 750 mLastMotionY[pointerId] = y; 751 } 752 } 753 754 void setDragState(int state) { 755 if (mDragState != state) { 756 mDragState = state; 757 mCallback.onViewDragStateChanged(state); 758 if (state == STATE_IDLE) { 759 mCapturedView = null; 760 } 761 } 762 } 763 764 /** 765 * Attempt to capture the view with the given pointer ID. The callback will be involved. 766 * This will put us into the "dragging" state. If we've already captured this view with 767 * this pointer this method will immediately return true without consulting the callback. 768 * 769 * @param toCapture View to capture 770 * @param pointerId Pointer to capture with 771 * @return true if capture was successful 772 */ 773 boolean tryCaptureViewForDrag(View toCapture, int pointerId) { 774 if (toCapture == mCapturedView && mActivePointerId == pointerId) { 775 // Already done! 776 return true; 777 } 778 if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { 779 mActivePointerId = pointerId; 780 captureChildView(toCapture, pointerId); 781 return true; 782 } 783 return false; 784 } 785 786 /** 787 * Tests scrollability within child views of v given a delta of dx. 788 * 789 * @param v View to test for horizontal scrollability 790 * @param checkV Whether the view v passed should itself be checked for scrollability (true), 791 * or just its children (false). 792 * @param dx Delta scrolled in pixels along the X axis 793 * @param dy Delta scrolled in pixels along the Y axis 794 * @param x X coordinate of the active touch point 795 * @param y Y coordinate of the active touch point 796 * @return true if child views of v can be scrolled by delta of dx. 797 */ 798 protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { 799 if (v instanceof ViewGroup) { 800 final ViewGroup group = (ViewGroup) v; 801 final int scrollX = v.getScrollX(); 802 final int scrollY = v.getScrollY(); 803 final int count = group.getChildCount(); 804 // Count backwards - let topmost views consume scroll distance first. 805 for (int i = count - 1; i >= 0; i--) { 806 // TODO: Add versioned support here for transformed views. 807 // This will not work for transformed views in Honeycomb+ 808 final View child = group.getChildAt(i); 809 if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && 810 y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && 811 canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), 812 y + scrollY - child.getTop())) { 813 return true; 814 } 815 } 816 } 817 818 return checkV && (ViewCompat.canScrollHorizontally(v, -dx) || 819 ViewCompat.canScrollVertically(v, -dy)); 820 } 821 822 /** 823 * Check if this event as provided to the parent view's onInterceptTouchEvent should 824 * cause the parent to intercept the touch event stream. 825 * 826 * @param ev MotionEvent provided to onInterceptTouchEvent 827 * @return true if the parent view should return true from onInterceptTouchEvent 828 */ 829 public boolean shouldInterceptTouchEvent(MotionEvent ev) { 830 final int action = MotionEventCompat.getActionMasked(ev); 831 final int actionIndex = MotionEventCompat.getActionIndex(ev); 832 833 if (action == MotionEvent.ACTION_DOWN) { 834 // Reset things for a new event stream, just in case we didn't get 835 // the whole previous stream. 836 cancel(); 837 } 838 839 if (mVelocityTracker == null) { 840 mVelocityTracker = VelocityTracker.obtain(); 841 } 842 mVelocityTracker.addMovement(ev); 843 844 switch (action) { 845 case MotionEvent.ACTION_DOWN: { 846 final float x = ev.getX(); 847 final float y = ev.getY(); 848 final int pointerId = MotionEventCompat.getPointerId(ev, 0); 849 saveInitialMotion(x, y, pointerId); 850 851 final View toCapture = findTopChildUnder((int) x, (int) y); 852 853 // Catch a settling view if possible. 854 if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { 855 tryCaptureViewForDrag(toCapture, pointerId); 856 } 857 858 final int edgesTouched = mInitialEdgesTouched[pointerId]; 859 if ((edgesTouched & mTrackingEdges) != 0) { 860 mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 861 } 862 break; 863 } 864 865 case MotionEventCompat.ACTION_POINTER_DOWN: { 866 final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 867 final float x = MotionEventCompat.getX(ev, actionIndex); 868 final float y = MotionEventCompat.getY(ev, actionIndex); 869 870 saveInitialMotion(x, y, pointerId); 871 872 // A ViewDragHelper can only manipulate one view at a time. 873 if (mDragState == STATE_IDLE) { 874 final int edgesTouched = mInitialEdgesTouched[pointerId]; 875 if ((edgesTouched & mTrackingEdges) != 0) { 876 mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 877 } 878 } else if (mDragState == STATE_SETTLING) { 879 // Catch a settling view if possible. 880 final View toCapture = findTopChildUnder((int) x, (int) y); 881 if (toCapture == mCapturedView) { 882 tryCaptureViewForDrag(toCapture, pointerId); 883 } 884 } 885 break; 886 } 887 888 case MotionEvent.ACTION_MOVE: { 889 // First to cross a touch slop over a draggable view wins. Also report edge drags. 890 final int pointerCount = MotionEventCompat.getPointerCount(ev); 891 for (int i = 0; i < pointerCount; i++) { 892 final int pointerId = MotionEventCompat.getPointerId(ev, i); 893 final float x = MotionEventCompat.getX(ev, i); 894 final float y = MotionEventCompat.getY(ev, i); 895 final float dx = x - mInitialMotionX[pointerId]; 896 final float dy = y - mInitialMotionY[pointerId]; 897 898 reportNewEdgeDrags(dx, dy, pointerId); 899 if (mDragState == STATE_DRAGGING) { 900 // Callback might have started an edge drag 901 break; 902 } 903 904 final View toCapture = findTopChildUnder((int) x, (int) y); 905 if (slopCheck(toCapture, dx, dy) && 906 tryCaptureViewForDrag(toCapture, pointerId)) { 907 break; 908 } 909 } 910 saveLastMotion(ev); 911 break; 912 } 913 914 case MotionEventCompat.ACTION_POINTER_UP: { 915 final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 916 clearMotionHistory(pointerId); 917 break; 918 } 919 920 case MotionEvent.ACTION_UP: 921 case MotionEvent.ACTION_CANCEL: { 922 cancel(); 923 break; 924 } 925 } 926 927 return mDragState == STATE_DRAGGING; 928 } 929 930 /** 931 * Process a touch event received by the parent view. This method will dispatch callback events 932 * as needed before returning. The parent view's onTouchEvent implementation should call this. 933 * 934 * @param ev The touch event received by the parent view 935 */ 936 public void processTouchEvent(MotionEvent ev) { 937 final int action = MotionEventCompat.getActionMasked(ev); 938 final int actionIndex = MotionEventCompat.getActionIndex(ev); 939 940 if (action == MotionEvent.ACTION_DOWN) { 941 // Reset things for a new event stream, just in case we didn't get 942 // the whole previous stream. 943 cancel(); 944 } 945 946 if (mVelocityTracker == null) { 947 mVelocityTracker = VelocityTracker.obtain(); 948 } 949 mVelocityTracker.addMovement(ev); 950 951 switch (action) { 952 case MotionEvent.ACTION_DOWN: { 953 final float x = ev.getX(); 954 final float y = ev.getY(); 955 final int pointerId = MotionEventCompat.getPointerId(ev, 0); 956 final View toCapture = findTopChildUnder((int) x, (int) y); 957 958 saveInitialMotion(x, y, pointerId); 959 960 // Since the parent is already directly processing this touch event, 961 // there is no reason to delay for a slop before dragging. 962 // Start immediately if possible. 963 tryCaptureViewForDrag(toCapture, pointerId); 964 965 final int edgesTouched = mInitialEdgesTouched[pointerId]; 966 if ((edgesTouched & mTrackingEdges) != 0) { 967 mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 968 } 969 break; 970 } 971 972 case MotionEventCompat.ACTION_POINTER_DOWN: { 973 final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 974 final float x = MotionEventCompat.getX(ev, actionIndex); 975 final float y = MotionEventCompat.getY(ev, actionIndex); 976 977 saveInitialMotion(x, y, pointerId); 978 979 // A ViewDragHelper can only manipulate one view at a time. 980 if (mDragState == STATE_IDLE) { 981 // If we're idle we can do anything! Treat it like a normal down event. 982 983 final View toCapture = findTopChildUnder((int) x, (int) y); 984 tryCaptureViewForDrag(toCapture, pointerId); 985 986 final int edgesTouched = mInitialEdgesTouched[pointerId]; 987 if ((edgesTouched & mTrackingEdges) != 0) { 988 mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); 989 } 990 } else if (isCapturedViewUnder((int) x, (int) y)) { 991 // We're still tracking a captured view. If the same view is under this 992 // point, we'll swap to controlling it with this pointer instead. 993 // (This will still work if we're "catching" a settling view.) 994 995 tryCaptureViewForDrag(mCapturedView, pointerId); 996 } 997 break; 998 } 999 1000 case MotionEvent.ACTION_MOVE: { 1001 if (mDragState == STATE_DRAGGING) { 1002 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 1003 final float x = MotionEventCompat.getX(ev, index); 1004 final float y = MotionEventCompat.getY(ev, index); 1005 final int idx = (int) (x - mLastMotionX[mActivePointerId]); 1006 final int idy = (int) (y - mLastMotionY[mActivePointerId]); 1007 final int ix = (int) x; 1008 final int iy = (int) y; 1009 1010 dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); 1011 1012 saveLastMotion(ev); 1013 1014 // Record what we lost from truncating to int for view coordinates. 1015 mLastMotionX[mActivePointerId] += x - ix; 1016 mLastMotionY[mActivePointerId] += y - iy; 1017 } else { 1018 // Check to see if any pointer is now over a draggable view. 1019 final int pointerCount = MotionEventCompat.getPointerCount(ev); 1020 for (int i = 0; i < pointerCount; i++) { 1021 final int pointerId = MotionEventCompat.getPointerId(ev, i); 1022 final float x = MotionEventCompat.getX(ev, i); 1023 final float y = MotionEventCompat.getY(ev, i); 1024 final float dx = x - mInitialMotionX[pointerId]; 1025 final float dy = y - mInitialMotionY[pointerId]; 1026 1027 reportNewEdgeDrags(dx, dy, pointerId); 1028 if (mDragState == STATE_DRAGGING) { 1029 // Callback might have started an edge drag. 1030 break; 1031 } 1032 1033 final View toCapture = findTopChildUnder((int) x, (int) y); 1034 if (slopCheck(toCapture, dx, dy) && 1035 tryCaptureViewForDrag(toCapture, pointerId)) { 1036 break; 1037 } 1038 } 1039 saveLastMotion(ev); 1040 } 1041 break; 1042 } 1043 1044 case MotionEventCompat.ACTION_POINTER_UP: { 1045 final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); 1046 if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { 1047 // Try to find another pointer that's still holding on to the captured view. 1048 int newActivePointer = INVALID_POINTER; 1049 final int pointerCount = MotionEventCompat.getPointerCount(ev); 1050 for (int i = 0; i < pointerCount; i++) { 1051 final int id = MotionEventCompat.getPointerId(ev, i); 1052 if (id == mActivePointerId) { 1053 // This one's going away, skip. 1054 continue; 1055 } 1056 1057 final float x = MotionEventCompat.getX(ev, i); 1058 final float y = MotionEventCompat.getY(ev, i); 1059 if (findTopChildUnder((int) x, (int) y) == mCapturedView && 1060 tryCaptureViewForDrag(mCapturedView, id)) { 1061 newActivePointer = mActivePointerId; 1062 break; 1063 } 1064 } 1065 1066 if (newActivePointer == INVALID_POINTER) { 1067 // We didn't find another pointer still touching the view, release it. 1068 releaseViewForPointerUp(); 1069 } 1070 } 1071 clearMotionHistory(pointerId); 1072 break; 1073 } 1074 1075 case MotionEvent.ACTION_UP: { 1076 if (mDragState == STATE_DRAGGING) { 1077 releaseViewForPointerUp(); 1078 } 1079 cancel(); 1080 break; 1081 } 1082 1083 case MotionEvent.ACTION_CANCEL: { 1084 if (mDragState == STATE_DRAGGING) { 1085 dispatchViewReleased(0, 0); 1086 } 1087 cancel(); 1088 break; 1089 } 1090 } 1091 } 1092 1093 private void reportNewEdgeDrags(float dx, float dy, int pointerId) { 1094 int dragsStarted = 0; 1095 if (checkNewEdgeDrag(dx, pointerId, EDGE_LEFT)) { 1096 dragsStarted |= EDGE_LEFT; 1097 } 1098 if (checkNewEdgeDrag(dy, pointerId, EDGE_TOP)) { 1099 dragsStarted |= EDGE_TOP; 1100 } 1101 if (checkNewEdgeDrag(dx, pointerId, EDGE_RIGHT)) { 1102 dragsStarted |= EDGE_RIGHT; 1103 } 1104 if (checkNewEdgeDrag(dy, pointerId, EDGE_BOTTOM)) { 1105 dragsStarted |= EDGE_BOTTOM; 1106 } 1107 1108 if (dragsStarted != 0) { 1109 mEdgeDragsInProgress[pointerId] |= dragsStarted; 1110 mCallback.onEdgeDragStarted(dragsStarted, pointerId); 1111 } 1112 } 1113 1114 private boolean checkNewEdgeDrag(float delta, int pointerId, int edge) { 1115 return (mTrackingEdges & edge) == edge && 1116 (mInitialEdgesTouched[pointerId] & edge) == edge && 1117 Math.abs(delta) > mTouchSlop && 1118 (mEdgeDragsInProgress[pointerId] & edge) == 0; 1119 } 1120 1121 /** 1122 * Check if we've crossed a reasonable touch slop for the given child view. 1123 * If the child cannot be dragged along the horizontal or vertical axis, motion 1124 * along that axis will not count toward the slop check. 1125 * 1126 * @param child Child to check 1127 * @param dx Motion since initial position along X axis 1128 * @param dy Motion since initial position along Y axis 1129 * @return true if the touch slop has been crossed 1130 */ 1131 private boolean slopCheck(View child, float dx, float dy) { 1132 final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; 1133 final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; 1134 1135 if (checkHorizontal && checkVertical) { 1136 return dx * dx + dy * dy > mTouchSlop * mTouchSlop; 1137 } else if (checkHorizontal) { 1138 return Math.abs(dx) > mTouchSlop; 1139 } else if (checkVertical) { 1140 return Math.abs(dy) > mTouchSlop; 1141 } 1142 return false; 1143 } 1144 1145 private void releaseViewForPointerUp() { 1146 mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); 1147 dispatchViewReleased(VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), 1148 VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); 1149 } 1150 1151 private void dragTo(int left, int top, int dx, int dy) { 1152 int clampedX = left; 1153 int clampedY = top; 1154 final int oldLeft = mCapturedView.getLeft(); 1155 final int oldTop = mCapturedView.getTop(); 1156 if (dx != 0) { 1157 clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); 1158 mCapturedView.offsetLeftAndRight(clampedX - oldLeft); 1159 } 1160 if (dy != 0) { 1161 clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); 1162 mCapturedView.offsetTopAndBottom(clampedY - oldTop); 1163 } 1164 1165 if (dx != 0 || dy != 0) { 1166 final int clampedDx = clampedX - oldLeft; 1167 final int clampedDy = clampedY - oldTop; 1168 mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, 1169 clampedDx, clampedDy); 1170 } 1171 } 1172 1173 /** 1174 * Determine if the currently captured view is under the given point in the 1175 * parent view's coordinate system. If there is no captured view this method 1176 * will return false. 1177 * 1178 * @param x X position to test in the parent's coordinate system 1179 * @param y Y position to test in the parent's coordinate system 1180 * @return true if the captured view is under the given point, false otherwise 1181 */ 1182 public boolean isCapturedViewUnder(int x, int y) { 1183 return isViewUnder(mCapturedView, x, y); 1184 } 1185 1186 /** 1187 * Determine if the supplied view is under the given point in the 1188 * parent view's coordinate system. 1189 * 1190 * @param view Child view of the parent to hit test 1191 * @param x X position to test in the parent's coordinate system 1192 * @param y Y position to test in the parent's coordinate system 1193 * @return true if the supplied view is under the given point, false otherwise 1194 */ 1195 public boolean isViewUnder(View view, int x, int y) { 1196 if (view == null) { 1197 return false; 1198 } 1199 return x >= view.getLeft() && 1200 x < view.getRight() && 1201 y >= view.getTop() && 1202 y > view.getBottom(); 1203 } 1204 1205 /** 1206 * Find the topmost child under the given point within the parent view's coordinate system. 1207 * The child order is determined using {@link Callback#getOrderedChildIndex(int)}. 1208 * 1209 * @param x X position to test in the parent's coordinate system 1210 * @param y Y position to test in the parent's coordinate system 1211 * @return The topmost child view under (x, y) or null if none found. 1212 */ 1213 public View findTopChildUnder(int x, int y) { 1214 final int childCount = mParentView.getChildCount(); 1215 for (int i = childCount - 1; i >= 0; i--) { 1216 final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); 1217 if (x >= child.getLeft() && x < child.getRight() && 1218 y >= child.getTop() && y < child.getBottom()) { 1219 return child; 1220 } 1221 } 1222 return null; 1223 } 1224 1225 private int getEdgesTouched(int x, int y) { 1226 int result = 0; 1227 1228 if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT; 1229 if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP; 1230 if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT; 1231 if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM; 1232 1233 return result; 1234 } 1235} 1236