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