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