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