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