SwipeRefreshLayout.java revision 4e987010ab5116581826eb40aece85a46968cfa1
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 17package android.support.v4.widget; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.content.res.TypedArray; 22import android.support.v4.view.MotionEventCompat; 23import android.support.v4.view.ViewCompat; 24import android.util.AttributeSet; 25import android.util.DisplayMetrics; 26import android.util.Log; 27import android.view.MotionEvent; 28import android.view.View; 29import android.view.ViewConfiguration; 30import android.view.ViewGroup; 31import android.view.animation.Animation; 32import android.view.animation.Animation.AnimationListener; 33import android.view.animation.DecelerateInterpolator; 34import android.view.animation.Transformation; 35import android.widget.AbsListView; 36 37/** 38 * The SwipeRefreshLayout should be used whenever the user can refresh the 39 * contents of a view via a vertical swipe gesture. The activity that 40 * instantiates this view should add an OnRefreshListener to be notified 41 * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout 42 * will notify the listener each and every time the gesture is completed again; 43 * the listener is responsible for correctly determining when to actually 44 * initiate a refresh of its content. If the listener determines there should 45 * not be a refresh, it must call setRefreshing(false) to cancel any visual 46 * indication of a refresh. If an activity wishes to show just the progress 47 * animation, it should call setRefreshing(true). To disable the gesture and 48 * progress animation, call setEnabled(false) on the view. 49 * <p> 50 * This layout should be made the parent of the view that will be refreshed as a 51 * result of the gesture and can only support one direct child. This view will 52 * also be made the target of the gesture and will be forced to match both the 53 * width and the height supplied in this layout. The SwipeRefreshLayout does not 54 * provide accessibility events; instead, a menu item must be provided to allow 55 * refresh of the content wherever this gesture is used. 56 * </p> 57 */ 58public class SwipeRefreshLayout extends ViewGroup { 59 // Maps to ProgressBar.Large style 60 public static final int LARGE = MaterialProgressDrawable.LARGE; 61 // Maps to ProgressBar default style 62 public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; 63 64 private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName(); 65 66 private static final int MAX_ALPHA = 255; 67 private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); 68 69 private static final int CIRCLE_DIAMETER = 40; 70 private static final int CIRCLE_DIAMETER_LARGE = 56; 71 72 private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; 73 private static final int INVALID_POINTER = -1; 74 private static final float DRAG_RATE = .5f; 75 76 // Max amount of circle that can be filled by progress during swipe gesture, 77 // where 1.0 is a full circle 78 private static final float MAX_PROGRESS_ANGLE = .8f; 79 80 private static final int SCALE_DOWN_DURATION = 150; 81 82 private static final int ALPHA_ANIMATION_DURATION = 300; 83 84 private static final int ANIMATE_TO_TRIGGER_DURATION = 200; 85 86 private static final int ANIMATE_TO_START_DURATION = 200; 87 88 // Default background for the progress spinner 89 private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; 90 // Default offset in dips from the top of the view to where the progress spinner should stop 91 private static final int DEFAULT_CIRCLE_TARGET = 64; 92 93 private View mTarget; // the target of the gesture 94 private OnRefreshListener mListener; 95 private boolean mRefreshing = false; 96 private int mTouchSlop; 97 private float mTotalDragDistance = -1; 98 private int mMediumAnimationDuration; 99 private int mCurrentTargetOffsetTop; 100 // Whether or not the starting offset has been determined. 101 private boolean mOriginalOffsetCalculated = false; 102 103 private float mInitialMotionY; 104 private boolean mIsBeingDragged; 105 private int mActivePointerId = INVALID_POINTER; 106 // Whether this item is scaled up rather than clipped 107 private boolean mScale; 108 109 // Target is returning to its start offset because it was cancelled or a 110 // refresh was triggered. 111 private boolean mReturningToStart; 112 private final DecelerateInterpolator mDecelerateInterpolator; 113 private static final int[] LAYOUT_ATTRS = new int[] { 114 android.R.attr.enabled 115 }; 116 117 private CircleImageView mCircleView; 118 private int mCircleViewIndex = -1; 119 120 protected int mFrom; 121 122 private float mStartingScale; 123 124 protected int mOriginalOffsetTop; 125 126 private MaterialProgressDrawable mProgress; 127 128 private Animation mScaleAnimation; 129 130 private Animation mScaleDownAnimation; 131 132 private Animation mAlphaStartAnimation; 133 134 private Animation mAlphaMaxAnimation; 135 136 private Animation mScaleDownToStartAnimation; 137 138 private float mSpinnerFinalOffset; 139 140 private boolean mNotify; 141 142 private int mCircleWidth; 143 144 private int mCircleHeight; 145 146 // Whether the client has set a custom starting position; 147 private boolean mUsingCustomStart; 148 149 private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { 150 @Override 151 public void onAnimationStart(Animation animation) { 152 } 153 154 @Override 155 public void onAnimationRepeat(Animation animation) { 156 } 157 158 @Override 159 public void onAnimationEnd(Animation animation) { 160 if (mRefreshing) { 161 // Make sure the progress view is fully visible 162 mProgress.setAlpha(MAX_ALPHA); 163 mProgress.start(); 164 if (mNotify) { 165 if (mListener != null) { 166 mListener.onRefresh(); 167 } 168 } 169 } else { 170 mProgress.stop(); 171 mCircleView.setVisibility(View.GONE); 172 // Return the circle to its start position 173 if (mScale) { 174 setAnimationProgress(0 /* animation complete and view is hidden */); 175 } else { 176 setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, 177 true /* requires update */); 178 } 179 } 180 mCurrentTargetOffsetTop = mCircleView.getTop(); 181 } 182 }; 183 184 private void setColorViewAlpha(int targetAlpha) { 185 mCircleView.getBackground().setAlpha(targetAlpha); 186 mProgress.setAlpha(targetAlpha); 187 } 188 189 /** 190 * The refresh indicator starting and resting position is always positioned 191 * near the top of the refreshing content. This position is a consistent 192 * location, but can be adjusted in either direction based on whether or not 193 * there is a toolbar or actionbar present. 194 * 195 * @param scale Set to true if there is no view at a higher z-order than 196 * where the progress spinner is set to appear. 197 * @param start The offset in pixels from the top of this view at which the 198 * progress spinner should appear. 199 * @param end The offset in pixels from the top of this view at which the 200 * progress spinner should come to rest after a successful swipe 201 * gesture. 202 */ 203 public void setProgressViewOffset(boolean scale, int start, int end) { 204 mScale = scale; 205 mCircleView.setVisibility(View.GONE); 206 mOriginalOffsetTop = mCurrentTargetOffsetTop = start; 207 mSpinnerFinalOffset = end; 208 mUsingCustomStart = true; 209 mCircleView.invalidate(); 210 } 211 212 /** 213 * The refresh indicator resting position is always positioned near the top 214 * of the refreshing content. This position is a consistent location, but 215 * can be adjusted in either direction based on whether or not there is a 216 * toolbar or actionbar present. 217 * 218 * @param scale Set to true if there is no view at a higher z-order than 219 * where the progress spinner is set to appear. 220 * @param end The offset in pixels from the top of this view at which the 221 * progress spinner should come to rest after a successful swipe 222 * gesture. 223 */ 224 public void setProgressViewEndTarget(boolean scale, int end) { 225 mSpinnerFinalOffset = end; 226 mScale = scale; 227 mCircleView.invalidate(); 228 } 229 230 /** 231 * One of DEFAULT, or LARGE. 232 */ 233 public void setSize(int size) { 234 if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { 235 return; 236 } 237 final DisplayMetrics metrics = getResources().getDisplayMetrics(); 238 if (size == MaterialProgressDrawable.LARGE) { 239 mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); 240 } else { 241 mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 242 } 243 // force the bounds of the progress circle inside the circle view to 244 // update by setting it to null before updating its size and then 245 // re-setting it 246 mCircleView.setImageDrawable(null); 247 mProgress.updateSizes(size); 248 mCircleView.setImageDrawable(mProgress); 249 } 250 251 /** 252 * Simple constructor to use when creating a SwipeRefreshLayout from code. 253 * 254 * @param context 255 */ 256 public SwipeRefreshLayout(Context context) { 257 this(context, null); 258 } 259 260 /** 261 * Constructor that is called when inflating SwipeRefreshLayout from XML. 262 * 263 * @param context 264 * @param attrs 265 */ 266 public SwipeRefreshLayout(Context context, AttributeSet attrs) { 267 super(context, attrs); 268 269 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 270 271 mMediumAnimationDuration = getResources().getInteger( 272 android.R.integer.config_mediumAnimTime); 273 274 setWillNotDraw(false); 275 mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); 276 277 final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 278 setEnabled(a.getBoolean(0, true)); 279 a.recycle(); 280 281 final DisplayMetrics metrics = getResources().getDisplayMetrics(); 282 mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 283 mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); 284 285 createProgressView(); 286 ViewCompat.setChildrenDrawingOrderEnabled(this, true); 287 // the absolute offset has to take into account that the circle starts at an offset 288 mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; 289 mTotalDragDistance = mSpinnerFinalOffset; 290 } 291 292 protected int getChildDrawingOrder(int childCount, int i) { 293 if (mCircleViewIndex < 0) { 294 return i; 295 } else if (i == childCount - 1) { 296 // Draw the selected child last 297 return mCircleViewIndex; 298 } else if (i >= mCircleViewIndex) { 299 // Move the children after the selected child earlier one 300 return i + 1; 301 } else { 302 // Keep the children before the selected child the same 303 return i; 304 } 305 } 306 307 private void createProgressView() { 308 mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2); 309 mProgress = new MaterialProgressDrawable(getContext(), this); 310 mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); 311 mCircleView.setImageDrawable(mProgress); 312 mCircleView.setVisibility(View.GONE); 313 addView(mCircleView); 314 } 315 316 /** 317 * Set the listener to be notified when a refresh is triggered via the swipe 318 * gesture. 319 */ 320 public void setOnRefreshListener(OnRefreshListener listener) { 321 mListener = listener; 322 } 323 324 /** 325 * Pre API 11, alpha is used to make the progress circle appear instead of scale. 326 */ 327 private boolean isAlphaUsedForScale() { 328 return android.os.Build.VERSION.SDK_INT < 11; 329 } 330 331 /** 332 * Notify the widget that refresh state has changed. Do not call this when 333 * refresh is triggered by a swipe gesture. 334 * 335 * @param refreshing Whether or not the view should show refresh progress. 336 */ 337 public void setRefreshing(boolean refreshing) { 338 if (refreshing && mRefreshing != refreshing) { 339 // scale and show 340 mRefreshing = refreshing; 341 int endTarget = 0; 342 if (!mUsingCustomStart) { 343 endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); 344 } else { 345 endTarget = (int) mSpinnerFinalOffset; 346 } 347 setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, 348 true /* requires update */); 349 mNotify = false; 350 startScaleUpAnimation(mRefreshListener); 351 } else { 352 setRefreshing(refreshing, false /* notify */); 353 } 354 } 355 356 private void startScaleUpAnimation(AnimationListener listener) { 357 mCircleView.setVisibility(View.VISIBLE); 358 if (android.os.Build.VERSION.SDK_INT >= 11) { 359 // Pre API 11, alpha is used in place of scale up to show the 360 // progress circle appearing. 361 // Don't adjust the alpha during appearance otherwise. 362 mProgress.setAlpha(MAX_ALPHA); 363 } 364 mScaleAnimation = new Animation() { 365 @Override 366 public void applyTransformation(float interpolatedTime, Transformation t) { 367 setAnimationProgress(interpolatedTime); 368 } 369 }; 370 mScaleAnimation.setDuration(mMediumAnimationDuration); 371 if (listener != null) { 372 mCircleView.setAnimationListener(listener); 373 } 374 mCircleView.clearAnimation(); 375 mCircleView.startAnimation(mScaleAnimation); 376 } 377 378 /** 379 * Pre API 11, this does an alpha animation. 380 * @param progress 381 */ 382 private void setAnimationProgress(float progress) { 383 if (isAlphaUsedForScale()) { 384 setColorViewAlpha((int) (progress * MAX_ALPHA)); 385 } else { 386 ViewCompat.setScaleX(mCircleView, progress); 387 ViewCompat.setScaleY(mCircleView, progress); 388 } 389 } 390 391 private void setRefreshing(boolean refreshing, final boolean notify) { 392 if (mRefreshing != refreshing) { 393 mNotify = notify; 394 ensureTarget(); 395 mRefreshing = refreshing; 396 if (mRefreshing) { 397 animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); 398 } else { 399 startScaleDownAnimation(mRefreshListener); 400 } 401 } 402 } 403 404 private void startScaleDownAnimation(Animation.AnimationListener listener) { 405 mScaleDownAnimation = new Animation() { 406 @Override 407 public void applyTransformation(float interpolatedTime, Transformation t) { 408 setAnimationProgress(1 - interpolatedTime); 409 } 410 }; 411 mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); 412 mCircleView.setAnimationListener(listener); 413 mCircleView.clearAnimation(); 414 mCircleView.startAnimation(mScaleDownAnimation); 415 } 416 417 private void startProgressAlphaStartAnimation() { 418 mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); 419 } 420 421 private void startProgressAlphaMaxAnimation() { 422 mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); 423 } 424 425 private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { 426 // Pre API 11, alpha is used in place of scale. Don't also use it to 427 // show the trigger point. 428 if (mScale && isAlphaUsedForScale()) { 429 return null; 430 } 431 Animation alpha = new Animation() { 432 @Override 433 public void applyTransformation(float interpolatedTime, Transformation t) { 434 mProgress 435 .setAlpha((int) (startingAlpha+ ((endingAlpha - startingAlpha) 436 * interpolatedTime))); 437 } 438 }; 439 alpha.setDuration(ALPHA_ANIMATION_DURATION); 440 // Clear out the previous animation listeners. 441 mCircleView.setAnimationListener(null); 442 mCircleView.clearAnimation(); 443 mCircleView.startAnimation(alpha); 444 return alpha; 445 } 446 447 /** 448 * Set the background color of the progress spinner disc. 449 * 450 * @param colorRes Resource id of the color. 451 */ 452 public void setProgressBackgroundColor(int colorRes) { 453 mCircleView.setBackgroundColor(colorRes); 454 mProgress.setBackgroundColor(getResources().getColor(colorRes)); 455 } 456 457 /** 458 * @deprecated Use {@link #setColorSchemeResources(int...)} 459 */ 460 @Deprecated 461 public void setColorScheme(int... colors) { 462 setColorSchemeResources(colors); 463 } 464 465 /** 466 * Set the color resources used in the progress animation from color resources. 467 * The first color will also be the color of the bar that grows in response 468 * to a user swipe gesture. 469 * 470 * @param colorResIds 471 */ 472 public void setColorSchemeResources(int... colorResIds) { 473 final Resources res = getResources(); 474 int[] colorRes = new int[colorResIds.length]; 475 for (int i = 0; i < colorResIds.length; i++) { 476 colorRes[i] = res.getColor(colorResIds[i]); 477 } 478 setColorSchemeColors(colorRes); 479 } 480 481 /** 482 * Set the colors used in the progress animation. The first 483 * color will also be the color of the bar that grows in response to a user 484 * swipe gesture. 485 * 486 * @param colors 487 */ 488 public void setColorSchemeColors(int... colors) { 489 ensureTarget(); 490 mProgress.setColorSchemeColors(colors); 491 } 492 493 /** 494 * @return Whether the SwipeRefreshWidget is actively showing refresh 495 * progress. 496 */ 497 public boolean isRefreshing() { 498 return mRefreshing; 499 } 500 501 private void ensureTarget() { 502 // Don't bother getting the parent height if the parent hasn't been laid 503 // out yet. 504 if (mTarget == null) { 505 for (int i = 0; i < getChildCount(); i++) { 506 View child = getChildAt(i); 507 if (!child.equals(mCircleView)) { 508 mTarget = child; 509 break; 510 } 511 } 512 } 513 } 514 515 /** 516 * Set the distance to trigger a sync in dips 517 * 518 * @param distance 519 */ 520 public void setDistanceToTriggerSync(int distance) { 521 mTotalDragDistance = distance; 522 } 523 524 @Override 525 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 526 final int width = getMeasuredWidth(); 527 final int height = getMeasuredHeight(); 528 if (getChildCount() == 0) { 529 return; 530 } 531 if (mTarget == null) { 532 ensureTarget(); 533 } 534 if (mTarget == null) { 535 return; 536 } 537 final View child = mTarget; 538 final int childLeft = getPaddingLeft(); 539 final int childTop = getPaddingTop(); 540 final int childWidth = width - getPaddingLeft() - getPaddingRight(); 541 final int childHeight = height - getPaddingTop() - getPaddingBottom(); 542 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 543 int circleWidth = mCircleView.getMeasuredWidth(); 544 int circleHeight = mCircleView.getMeasuredHeight(); 545 mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, 546 (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); 547 } 548 549 @Override 550 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 551 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 552 if (mTarget == null) { 553 ensureTarget(); 554 } 555 if (mTarget == null) { 556 return; 557 } 558 mTarget.measure(MeasureSpec.makeMeasureSpec( 559 getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), 560 MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( 561 getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); 562 mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), 563 MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); 564 if (!mUsingCustomStart && !mOriginalOffsetCalculated) { 565 mOriginalOffsetCalculated = true; 566 mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); 567 } 568 mCircleViewIndex = -1; 569 // Get the index of the circleview. 570 for (int index = 0; index < getChildCount(); index++) { 571 if (getChildAt(index) == mCircleView) { 572 mCircleViewIndex = index; 573 break; 574 } 575 } 576 } 577 578 /** 579 * @return Whether it is possible for the child view of this layout to 580 * scroll up. Override this if the child view is a custom view. 581 */ 582 public boolean canChildScrollUp() { 583 if (android.os.Build.VERSION.SDK_INT < 14) { 584 if (mTarget instanceof AbsListView) { 585 final AbsListView absListView = (AbsListView) mTarget; 586 return absListView.getChildCount() > 0 587 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 588 .getTop() < absListView.getPaddingTop()); 589 } else { 590 return mTarget.getScrollY() > 0; 591 } 592 } else { 593 return ViewCompat.canScrollVertically(mTarget, -1); 594 } 595 } 596 597 @Override 598 public boolean onInterceptTouchEvent(MotionEvent ev) { 599 ensureTarget(); 600 601 final int action = MotionEventCompat.getActionMasked(ev); 602 603 if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 604 mReturningToStart = false; 605 } 606 607 if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { 608 // Fail fast if we're not in a state where a swipe is possible 609 return false; 610 } 611 612 switch (action) { 613 case MotionEvent.ACTION_DOWN: 614 setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); 615 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 616 mIsBeingDragged = false; 617 final float initialMotionY = getMotionEventY(ev, mActivePointerId); 618 if (initialMotionY == -1) { 619 return false; 620 } 621 mInitialMotionY = initialMotionY; 622 623 case MotionEvent.ACTION_MOVE: 624 if (mActivePointerId == INVALID_POINTER) { 625 Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); 626 return false; 627 } 628 629 final float y = getMotionEventY(ev, mActivePointerId); 630 if (y == -1) { 631 return false; 632 } 633 final float yDiff = y - mInitialMotionY; 634 if (yDiff > mTouchSlop && !mIsBeingDragged) { 635 mIsBeingDragged = true; 636 mProgress.setAlpha(STARTING_PROGRESS_ALPHA); 637 } 638 break; 639 640 case MotionEventCompat.ACTION_POINTER_UP: 641 onSecondaryPointerUp(ev); 642 break; 643 644 case MotionEvent.ACTION_UP: 645 case MotionEvent.ACTION_CANCEL: 646 mIsBeingDragged = false; 647 mActivePointerId = INVALID_POINTER; 648 break; 649 } 650 651 return mIsBeingDragged; 652 } 653 654 private float getMotionEventY(MotionEvent ev, int activePointerId) { 655 final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); 656 if (index < 0) { 657 return -1; 658 } 659 return MotionEventCompat.getY(ev, index); 660 } 661 662 @Override 663 public void requestDisallowInterceptTouchEvent(boolean b) { 664 // Nope. 665 } 666 667 private boolean isAnimationRunning(Animation animation) { 668 return animation != null && animation.hasStarted() && !animation.hasEnded(); 669 } 670 671 @Override 672 public boolean onTouchEvent(MotionEvent ev) { 673 final int action = MotionEventCompat.getActionMasked(ev); 674 675 if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 676 mReturningToStart = false; 677 } 678 679 if (!isEnabled() || mReturningToStart || canChildScrollUp()) { 680 // Fail fast if we're not in a state where a swipe is possible 681 return false; 682 } 683 684 switch (action) { 685 case MotionEvent.ACTION_DOWN: 686 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 687 mIsBeingDragged = false; 688 break; 689 690 case MotionEvent.ACTION_MOVE: { 691 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 692 if (pointerIndex < 0) { 693 Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); 694 return false; 695 } 696 697 final float y = MotionEventCompat.getY(ev, pointerIndex); 698 final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; 699 if (mIsBeingDragged) { 700 mProgress.showArrow(true); 701 float originalDragPercent = overscrollTop / mTotalDragDistance; 702 if (originalDragPercent < 0) { 703 return false; 704 } 705 float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); 706 float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; 707 float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; 708 float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset 709 - mOriginalOffsetTop : mSpinnerFinalOffset; 710 float tensionSlingshotPercent = Math.max(0, 711 Math.min(extraOS, slingshotDist * 2) / slingshotDist); 712 float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( 713 (tensionSlingshotPercent / 4), 2)) * 2f; 714 float extraMove = (slingshotDist) * tensionPercent * 2; 715 716 int targetY = mOriginalOffsetTop 717 + (int) ((slingshotDist * dragPercent) + extraMove); 718 // where 1.0f is a full circle 719 if (mCircleView.getVisibility() != View.VISIBLE) { 720 mCircleView.setVisibility(View.VISIBLE); 721 } 722 if (!mScale) { 723 ViewCompat.setScaleX(mCircleView, 1f); 724 ViewCompat.setScaleY(mCircleView, 1f); 725 } 726 if (overscrollTop < mTotalDragDistance) { 727 if (mScale) { 728 setAnimationProgress(overscrollTop / mTotalDragDistance); 729 } 730 if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA 731 && !isAnimationRunning(mAlphaStartAnimation)) { 732 // Animate the alpha 733 startProgressAlphaStartAnimation(); 734 } 735 float strokeStart = (float) (adjustedPercent * .8f); 736 mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); 737 mProgress.setArrowScale(Math.min(1f, adjustedPercent)); 738 } else { 739 if (mProgress.getAlpha() < MAX_ALPHA 740 && !isAnimationRunning(mAlphaMaxAnimation)) { 741 // Animate the alpha 742 startProgressAlphaMaxAnimation(); 743 } 744 } 745 float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; 746 mProgress.setProgressRotation(rotation); 747 setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, 748 true /* requires update */); 749 } 750 break; 751 } 752 case MotionEventCompat.ACTION_POINTER_DOWN: { 753 final int index = MotionEventCompat.getActionIndex(ev); 754 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 755 break; 756 } 757 758 case MotionEventCompat.ACTION_POINTER_UP: 759 onSecondaryPointerUp(ev); 760 break; 761 762 case MotionEvent.ACTION_UP: 763 case MotionEvent.ACTION_CANCEL: { 764 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 765 final float y = MotionEventCompat.getY(ev, pointerIndex); 766 final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; 767 mIsBeingDragged = false; 768 if (overscrollTop > mTotalDragDistance) { 769 setRefreshing(true, true /* notify */); 770 } else { 771 // cancel refresh 772 mRefreshing = false; 773 mProgress.setStartEndTrim(0f, 0f); 774 Animation.AnimationListener listener = null; 775 if (!mScale) { 776 listener = new Animation.AnimationListener() { 777 778 @Override 779 public void onAnimationStart(Animation animation) { 780 } 781 782 @Override 783 public void onAnimationEnd(Animation animation) { 784 if (!mScale) { 785 startScaleDownAnimation(null); 786 } 787 } 788 789 @Override 790 public void onAnimationRepeat(Animation animation) { 791 } 792 793 }; 794 } 795 animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); 796 mProgress.showArrow(false); 797 } 798 mActivePointerId = INVALID_POINTER; 799 return false; 800 } 801 } 802 803 return true; 804 } 805 806 private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { 807 mFrom = from; 808 mAnimateToCorrectPosition.reset(); 809 mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); 810 mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); 811 if (listener != null) { 812 mCircleView.setAnimationListener(listener); 813 } 814 mCircleView.clearAnimation(); 815 mCircleView.startAnimation(mAnimateToCorrectPosition); 816 } 817 818 private void animateOffsetToStartPosition(int from, AnimationListener listener) { 819 if (mScale) { 820 // Scale the item back down 821 startScaleDownReturnToStartAnimation(from, listener); 822 } else { 823 mFrom = from; 824 mAnimateToStartPosition.reset(); 825 mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); 826 mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); 827 if (listener != null) { 828 mCircleView.setAnimationListener(listener); 829 } 830 mCircleView.clearAnimation(); 831 mCircleView.startAnimation(mAnimateToStartPosition); 832 } 833 } 834 835 private final Animation mAnimateToCorrectPosition = new Animation() { 836 @Override 837 public void applyTransformation(float interpolatedTime, Transformation t) { 838 int targetTop = 0; 839 int endTarget = 0; 840 if (!mUsingCustomStart) { 841 endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); 842 } else { 843 endTarget = (int) mSpinnerFinalOffset; 844 } 845 targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); 846 int offset = targetTop - mCircleView.getTop(); 847 setTargetOffsetTopAndBottom(offset, false /* requires update */); 848 } 849 }; 850 851 private void moveToStart(float interpolatedTime) { 852 int targetTop = 0; 853 targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); 854 int offset = targetTop - mCircleView.getTop(); 855 setTargetOffsetTopAndBottom(offset, false /* requires update */); 856 } 857 858 private final Animation mAnimateToStartPosition = new Animation() { 859 @Override 860 public void applyTransformation(float interpolatedTime, Transformation t) { 861 moveToStart(interpolatedTime); 862 } 863 }; 864 865 private void startScaleDownReturnToStartAnimation(int from, 866 Animation.AnimationListener listener) { 867 mFrom = from; 868 if (isAlphaUsedForScale()) { 869 mStartingScale = mProgress.getAlpha(); 870 } else { 871 mStartingScale = ViewCompat.getScaleX(mCircleView); 872 } 873 mScaleDownToStartAnimation = new Animation() { 874 @Override 875 public void applyTransformation(float interpolatedTime, Transformation t) { 876 float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); 877 setAnimationProgress(targetScale); 878 moveToStart(interpolatedTime); 879 } 880 }; 881 mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); 882 if (listener != null) { 883 mCircleView.setAnimationListener(listener); 884 } 885 mCircleView.clearAnimation(); 886 mCircleView.startAnimation(mScaleDownToStartAnimation); 887 } 888 889 private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { 890 mCircleView.bringToFront(); 891 mCircleView.offsetTopAndBottom(offset); 892 mCurrentTargetOffsetTop = mCircleView.getTop(); 893 if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { 894 invalidate(); 895 } 896 } 897 898 private void onSecondaryPointerUp(MotionEvent ev) { 899 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 900 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 901 if (pointerId == mActivePointerId) { 902 // This was our active pointer going up. Choose a new 903 // active pointer and adjust accordingly. 904 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 905 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 906 } 907 } 908 909 /** 910 * Classes that wish to be notified when the swipe gesture correctly 911 * triggers a refresh should implement this interface. 912 */ 913 public interface OnRefreshListener { 914 public void onRefresh(); 915 } 916}