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