SwipeRefreshLayout.java revision 4ca82494575dc4927e7ff51291c3645025cc9414
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 // Maps to ProgressBar.Small style 64 public static final int SMALL = MaterialProgressDrawable.SMALL; 65 66 private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName(); 67 68 private static final int MAX_ALPHA = 255; 69 70 private static final int CIRCLE_DIAMETER = 40; 71 private static final int CIRCLE_DIAMETER_LARGE = 96; 72 private static final int CIRCLE_DIAMETER_SMALL = 16; 73 74 private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; 75 private static final int INVALID_POINTER = -1; 76 private static final float DRAG_RATE = .5f; 77 private static final int MAX_OVERSWIPE = 80; 78 79 // Default background for the progress spinner 80 private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; 81 private static final float MIN_CIRCLE_SCALE = .1f; 82 // Default offset in dips from the top of the view to where the progress spinner should stop 83 private static final int DEFAULT_CIRCLE_TARGET = 64; 84 85 private View mTarget; // the target of the gesture 86 private OnRefreshListener mListener; 87 private boolean mRefreshing = false; 88 private int mTouchSlop; 89 private float mDistanceToTriggerSync = -1; 90 private int mMediumAnimationDuration; 91 private int mShortAnimationDuration; 92 private int mCurrentTargetOffsetTop; 93 94 private float mInitialMotionY; 95 private boolean mIsBeingDragged; 96 private int mActivePointerId = INVALID_POINTER; 97 // Whether this item is scaled up rather than clipped 98 private boolean mScale; 99 100 // Target is returning to its start offset because it was cancelled or a 101 // refresh was triggered. 102 private boolean mReturningToStart; 103 private final DecelerateInterpolator mDecelerateInterpolator; 104 private static final int[] LAYOUT_ATTRS = new int[] { 105 android.R.attr.enabled 106 }; 107 108 private CircleImageView mCircleView; 109 110 protected int mFrom; 111 112 protected int mOriginalOffsetTop; 113 114 private MaterialProgressDrawable mProgress; 115 116 private Animation mScaleAnimation; 117 118 private Animation mScaleDownAnimation; 119 120 private float mLastY; 121 122 private float mTargetCirclePosition; 123 124 private boolean mNotify; 125 126 private int mCircleWidth; 127 128 private int mCircleHeight; 129 130 private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { 131 @Override 132 public void onAnimationStart(Animation animation) { 133 } 134 135 @Override 136 public void onAnimationRepeat(Animation animation) { 137 } 138 139 @Override 140 public void onAnimationEnd(Animation animation) { 141 if (mRefreshing) { 142 mProgress.showArrow(false); 143 mProgress.start(); 144 if (mNotify) { 145 if (mListener != null) { 146 mListener.onRefresh(); 147 } 148 } 149 } else { 150 mProgress.stop(); 151 mCircleView.setVisibility(View.GONE); 152 // Return the circle to its start position 153 if (mScale) { 154 setAnimationProgress(0 /* animation complete and view is hidden */); 155 } else { 156 setAnimationProgress(1 /* animation complete and view is showing */); 157 setTargetOffsetTopAndBottom(-mCircleHeight - mCurrentTargetOffsetTop, 158 true /* requires update */); 159 mCircleView.setVisibility(View.VISIBLE); 160 } 161 } 162 mCurrentTargetOffsetTop = mCircleView.getTop(); 163 } 164 }; 165 166 private void setColorViewAlpha(int targetAlpha) { 167 mCircleView.getBackground().setAlpha(targetAlpha); 168 mProgress.setAlpha(targetAlpha); 169 } 170 171 /** 172 * The refresh indicator starting and resting position is always positioned 173 * near the top of the refreshing content. This position is a consistent 174 * location, but can be adjusted in either direction based on whether or not 175 * there is a toolbar or actionbar present. 176 * 177 * @param scale Set to true if there is no view at a higher z-order than 178 * where the progress spinner is set to appear. 179 * @param start The offset in pixels from the top of this view at which the 180 * progress spinner should appear. 181 * @param end The offset in pixels from the top of this view at which the 182 * progress spinner should come to rest after a successful swipe 183 * gesture. 184 */ 185 public void setProgressViewOffset(boolean scale, int start, int end) { 186 mScale = scale; 187 mCircleView.setVisibility(View.GONE); 188 mOriginalOffsetTop = mCurrentTargetOffsetTop = start; 189 mDistanceToTriggerSync = (mTargetCirclePosition / DRAG_RATE); 190 mTargetCirclePosition = end; 191 mCircleView.invalidate(); 192 } 193 194 /** 195 * One of SMALL, DEFAULT, or LARGE. DEFAULT is ProgressBar default styling. 196 */ 197 public void setSize(int size) { 198 if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT 199 && size != MaterialProgressDrawable.SMALL) { 200 return; 201 } 202 final DisplayMetrics metrics = getResources().getDisplayMetrics(); 203 if (size == MaterialProgressDrawable.LARGE) { 204 mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); 205 } else if (size == MaterialProgressDrawable.SMALL) { 206 mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_SMALL * metrics.density); 207 } else { 208 mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 209 } 210 mProgress.updateSizes(size); 211 } 212 213 /** 214 * Simple constructor to use when creating a SwipeRefreshLayout from code. 215 * 216 * @param context 217 */ 218 public SwipeRefreshLayout(Context context) { 219 this(context, null); 220 } 221 222 /** 223 * Constructor that is called when inflating SwipeRefreshLayout from XML. 224 * 225 * @param context 226 * @param attrs 227 */ 228 public SwipeRefreshLayout(Context context, AttributeSet attrs) { 229 super(context, attrs); 230 231 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 232 233 mMediumAnimationDuration = getResources().getInteger( 234 android.R.integer.config_mediumAnimTime); 235 mShortAnimationDuration = getResources().getInteger( 236 android.R.integer.config_shortAnimTime); 237 238 setWillNotDraw(false); 239 mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); 240 241 final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 242 setEnabled(a.getBoolean(0, true)); 243 a.recycle(); 244 245 final DisplayMetrics metrics = getResources().getDisplayMetrics(); 246 mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); 247 mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); 248 249 mTargetCirclePosition = DEFAULT_CIRCLE_TARGET * metrics.density; 250 // Since the circle moves at 1/2 rate of drag, and we want the circle to 251 // end at 64, and its starts -Diameter offscreen, it needs to move overall this 252 mDistanceToTriggerSync = (mTargetCirclePosition / DRAG_RATE) 253 + (mCircleWidth / DRAG_RATE); 254 createProgressView(); 255 ViewCompat.setChildrenDrawingOrderEnabled(this, true); 256 } 257 258 protected int getChildDrawingOrder (int childCount, int i) { 259 if (getChildAt(i).equals(mCircleView)) { 260 return childCount - 1; 261 } 262 return i; 263 } 264 265 private void createProgressView() { 266 mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2); 267 mProgress = new MaterialProgressDrawable(getContext(), this); 268 mCircleView.setImageDrawable(mProgress); 269 addView(mCircleView); 270 mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleWidth; 271 } 272 273 /** 274 * Set the listener to be notified when a refresh is triggered via the swipe 275 * gesture. 276 */ 277 public void setOnRefreshListener(OnRefreshListener listener) { 278 mListener = listener; 279 } 280 281 /** 282 * Notify the widget that refresh state has changed. Do not call this when 283 * refresh is triggered by a swipe gesture. 284 * 285 * @param refreshing Whether or not the view should show refresh progress. 286 */ 287 public void setRefreshing(boolean refreshing) { 288 if (refreshing && mRefreshing != refreshing) { 289 // scale and show 290 mRefreshing = refreshing; 291 setTargetOffsetTopAndBottom((int) (mTargetCirclePosition - mCircleView.getTop()), 292 true /* requires update */); 293 mNotify = false; 294 startScaleUpAnimation(mRefreshListener); 295 } else { 296 setRefreshing(refreshing, false /* notify */); 297 } 298 } 299 300 private void startScaleUpAnimation(AnimationListener listener) { 301 mCircleView.setVisibility(View.VISIBLE); 302 mScaleAnimation = new Animation() { 303 @Override 304 public void applyTransformation(float interpolatedTime, Transformation t) { 305 setAnimationProgress(interpolatedTime); 306 } 307 }; 308 mScaleAnimation.setDuration(mMediumAnimationDuration); 309 if (listener != null) { 310 mCircleView.setAnimationListener(listener); 311 } 312 mCircleView.clearAnimation(); 313 mCircleView.startAnimation(mScaleAnimation); 314 } 315 316 /** 317 * Pre API 11, this does an alpha animation. 318 * @param progress 319 */ 320 private void setAnimationProgress(float progress) { 321 if (android.os.Build.VERSION.SDK_INT < 11) { 322 setColorViewAlpha((int) (progress * MAX_ALPHA)); 323 } else { 324 ViewCompat.setScaleX(mCircleView, progress); 325 ViewCompat.setScaleY(mCircleView, progress); 326 } 327 } 328 329 private void setRefreshing(boolean refreshing, final boolean notify) { 330 if (mRefreshing != refreshing) { 331 mNotify = notify; 332 ensureTarget(); 333 mRefreshing = refreshing; 334 if (mRefreshing) { 335 animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); 336 } else { 337 startScaleDownAnimation(mRefreshListener); 338 } 339 } 340 } 341 342 private void startScaleDownAnimation(Animation.AnimationListener listener) { 343 mScaleDownAnimation = new Animation() { 344 @Override 345 public void applyTransformation(float interpolatedTime, Transformation t) { 346 setAnimationProgress(1 - interpolatedTime); 347 } 348 }; 349 mScaleDownAnimation.setDuration(mShortAnimationDuration); 350 if (listener != null) { 351 mCircleView.setAnimationListener(listener); 352 } 353 mCircleView.clearAnimation(); 354 mCircleView.startAnimation(mScaleDownAnimation); 355 } 356 357 public void setProgressBackgroundColor(int colorRes) { 358 mCircleView.setBackgroundColor(colorRes); 359 } 360 361 /** 362 * @deprecated Use {@link #setColorSchemeResources(int...)} 363 */ 364 @Deprecated 365 public void setColorScheme(int... colors) { 366 setColorSchemeResources(colors); 367 } 368 369 /** 370 * Set the color resources used in the progress animation from color resources. 371 * The first color will also be the color of the bar that grows in response 372 * to a user swipe gesture. 373 * 374 * @param colorResIds 375 */ 376 public void setColorSchemeResources(int... colorResIds) { 377 final Resources res = getResources(); 378 int[] colorRes = new int[colorResIds.length]; 379 for (int i = 0; i < colorResIds.length; i++) { 380 colorRes[i] = res.getColor(colorResIds[i]); 381 } 382 setColorSchemeColors(colorRes); 383 } 384 385 /** 386 * Set the colors used in the progress animation. The first 387 * color will also be the color of the bar that grows in response to a user 388 * swipe gesture. 389 * 390 * @param colors 391 */ 392 public void setColorSchemeColors(int... colors) { 393 ensureTarget(); 394 mProgress.setColorSchemeColors(colors); 395 } 396 397 /** 398 * @return Whether the SwipeRefreshWidget is actively showing refresh 399 * progress. 400 */ 401 public boolean isRefreshing() { 402 return mRefreshing; 403 } 404 405 private void ensureTarget() { 406 // Don't bother getting the parent height if the parent hasn't been laid 407 // out yet. 408 if (mTarget == null) { 409 for (int i = 0; i < getChildCount(); i++) { 410 View child = getChildAt(i); 411 if (!child.equals(mCircleView)) { 412 mTarget = child; 413 break; 414 } 415 } 416 } 417 } 418 419 /** 420 * Set the distance to trigger a sync in dips 421 * 422 * @param distance 423 */ 424 public void setDistanceToTriggerSync(int distance) { 425 mDistanceToTriggerSync = distance; 426 } 427 428 @Override 429 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 430 final int width = getMeasuredWidth(); 431 final int height = getMeasuredHeight(); 432 if (getChildCount() == 0) { 433 return; 434 } 435 if (mTarget == null) { 436 ensureTarget(); 437 } 438 if (mTarget == null) { 439 return; 440 } 441 final View child = mTarget; 442 final int childLeft = getPaddingLeft(); 443 final int childTop = getPaddingTop(); 444 final int childWidth = width - getPaddingLeft() - getPaddingRight(); 445 final int childHeight = height - getPaddingTop() - getPaddingBottom(); 446 child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); 447 mCircleView.layout((width / 2 - mCircleWidth / 2), mCurrentTargetOffsetTop, 448 (width / 2 + mCircleWidth / 2), mCurrentTargetOffsetTop + mCircleHeight); 449 } 450 451 @Override 452 public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 453 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 454 if (mTarget == null) { 455 ensureTarget(); 456 } 457 if (mTarget == null) { 458 return; 459 } 460 mTarget.measure(MeasureSpec.makeMeasureSpec( 461 getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), 462 MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( 463 getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); 464 mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), 465 MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); 466 } 467 468 /** 469 * @return Whether it is possible for the child view of this layout to 470 * scroll up. Override this if the child view is a custom view. 471 */ 472 public boolean canChildScrollUp() { 473 if (android.os.Build.VERSION.SDK_INT < 14) { 474 if (mTarget instanceof AbsListView) { 475 final AbsListView absListView = (AbsListView) mTarget; 476 return absListView.getChildCount() > 0 477 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) 478 .getTop() < absListView.getPaddingTop()); 479 } else { 480 return mTarget.getScrollY() > 0; 481 } 482 } else { 483 return ViewCompat.canScrollVertically(mTarget, -1); 484 } 485 } 486 487 @Override 488 public boolean onInterceptTouchEvent(MotionEvent ev) { 489 ensureTarget(); 490 491 final int action = MotionEventCompat.getActionMasked(ev); 492 493 if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 494 mReturningToStart = false; 495 } 496 497 if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { 498 // Fail fast if we're not in a state where a swipe is possible 499 return false; 500 } 501 502 switch (action) { 503 case MotionEvent.ACTION_DOWN: 504 mLastY = mInitialMotionY = ev.getY(); 505 setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); 506 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 507 mIsBeingDragged = false; 508 break; 509 510 case MotionEvent.ACTION_MOVE: 511 if (mActivePointerId == INVALID_POINTER) { 512 Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); 513 return false; 514 } 515 516 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 517 if (pointerIndex < 0) { 518 Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); 519 return false; 520 } 521 522 final float y = MotionEventCompat.getY(ev, pointerIndex); 523 final float yDiff = y - mInitialMotionY; 524 if (yDiff > mTouchSlop) { 525 mIsBeingDragged = true; 526 } 527 break; 528 529 case MotionEventCompat.ACTION_POINTER_UP: 530 onSecondaryPointerUp(ev); 531 break; 532 533 case MotionEvent.ACTION_UP: 534 case MotionEvent.ACTION_CANCEL: 535 mIsBeingDragged = false; 536 mActivePointerId = INVALID_POINTER; 537 break; 538 } 539 540 return mIsBeingDragged; 541 } 542 543 @Override 544 public void requestDisallowInterceptTouchEvent(boolean b) { 545 // Nope. 546 } 547 548 @Override 549 public boolean onTouchEvent(MotionEvent ev) { 550 final int action = MotionEventCompat.getActionMasked(ev); 551 552 if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { 553 mReturningToStart = false; 554 } 555 556 if (!isEnabled() || mReturningToStart || canChildScrollUp()) { 557 // Fail fast if we're not in a state where a swipe is possible 558 return false; 559 } 560 561 switch (action) { 562 case MotionEvent.ACTION_DOWN: 563 mLastY = mInitialMotionY = ev.getY(); 564 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 565 mIsBeingDragged = false; 566 break; 567 568 case MotionEvent.ACTION_MOVE: 569 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 570 if (pointerIndex < 0) { 571 Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); 572 return false; 573 } 574 575 final float y = MotionEventCompat.getY(ev, pointerIndex); 576 final float yDiff = y - mInitialMotionY; 577 578 if (!mIsBeingDragged && yDiff > mTouchSlop) { 579 mIsBeingDragged = true; 580 } 581 582 if (mIsBeingDragged) { 583 // User velocity passed min velocity; trigger a refresh 584 if (yDiff > mDistanceToTriggerSync) { 585 // Continue updating circle, but at a reduced rate 586 float p = Math.max(0, 587 Math.min((yDiff - mDistanceToTriggerSync), MAX_OVERSWIPE) 588 / MAX_OVERSWIPE / 2); 589 double q = (p - Math.pow(p, 2)); 590 double extraDrag = (2 * MAX_OVERSWIPE * q); 591 if (mTargetCirclePosition + extraDrag 592 <= (mTargetCirclePosition + MAX_OVERSWIPE)) { 593 setTargetOffsetTopAndBottom( 594 (int) (mTargetCirclePosition + extraDrag 595 - mCurrentTargetOffsetTop), 596 true /* requires update */); 597 } 598 if (mScale || mCurrentTargetOffsetTop > -mCircleHeight / 2) { 599 // start showing the arrow 600 float pullPercent = yDiff / mDistanceToTriggerSync * DRAG_RATE; 601 mProgress.setProgressRotation((float) (Math.PI / 4 * pullPercent)); 602 mProgress.setArrowScale(1f); 603 } 604 } else { 605 if (mCircleView.getVisibility() != View.VISIBLE) { 606 mCircleView.setVisibility(View.VISIBLE); 607 } 608 mProgress.showArrow(true); 609 if (mScale) { 610 setAnimationProgress(yDiff / mDistanceToTriggerSync); 611 } 612 // Just track the user's movement 613 setTargetOffsetTopAndBottom((int) ((y - mLastY) * DRAG_RATE), 614 true /* requires update */); 615 if (mScale || mCurrentTargetOffsetTop > -mCircleHeight / 2) { 616 // start showing the arrow 617 float pullPercent = yDiff / mDistanceToTriggerSync; 618 float startTrim = (float) (.2 * Math.min(.75f, (pullPercent))); 619 mProgress.setStartEndTrim(startTrim, 620 Math.min(.75f, pullPercent)); 621 mProgress.setProgressRotation((float) (Math.PI / 4 * pullPercent)); 622 mProgress.setArrowScale(Math.min(1f, pullPercent)); 623 } 624 } 625 } 626 mLastY = y; 627 break; 628 629 case MotionEventCompat.ACTION_POINTER_DOWN: { 630 final int index = MotionEventCompat.getActionIndex(ev); 631 mActivePointerId = MotionEventCompat.getPointerId(ev, index); 632 break; 633 } 634 635 case MotionEventCompat.ACTION_POINTER_UP: 636 onSecondaryPointerUp(ev); 637 break; 638 639 case MotionEvent.ACTION_UP: 640 case MotionEvent.ACTION_CANCEL: 641 mIsBeingDragged = false; 642 if (mCurrentTargetOffsetTop >= mTargetCirclePosition) { 643 setRefreshing(true, true /* notify */); 644 mProgress.showArrow(false); 645 } else { 646 // cancel refresh 647 mRefreshing = false; 648 mProgress.setStartEndTrim(0f, 0f); 649 animateOffsetToStartPosition(mCurrentTargetOffsetTop, null); 650 mProgress.showArrow(false); 651 } 652 mActivePointerId = INVALID_POINTER; 653 return false; 654 } 655 656 return true; 657 } 658 659 private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { 660 mFrom = from; 661 mAnimateToCorrectPosition.reset(); 662 mAnimateToCorrectPosition.setDuration(mMediumAnimationDuration); 663 mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); 664 if (listener != null) { 665 mCircleView.setAnimationListener(listener); 666 } 667 mCircleView.clearAnimation(); 668 mCircleView.startAnimation(mAnimateToCorrectPosition); 669 } 670 671 private void animateOffsetToStartPosition(int from, AnimationListener listener) { 672 mFrom = from; 673 mAnimateToStartPosition.reset(); 674 mAnimateToStartPosition.setDuration(mMediumAnimationDuration); 675 mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); 676 if (listener != null) { 677 mCircleView.setAnimationListener(listener); 678 } 679 mCircleView.clearAnimation(); 680 mCircleView.startAnimation(mAnimateToStartPosition); 681 } 682 683 private final Animation mAnimateToCorrectPosition = new Animation() { 684 @Override 685 public void applyTransformation(float interpolatedTime, Transformation t) { 686 int targetTop = 0; 687 int endTarget = (int) (mTargetCirclePosition); 688 if (mFrom != endTarget) { 689 targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); 690 } 691 int offset = targetTop - mCircleView.getTop(); 692 setTargetOffsetTopAndBottom(offset, false /* requires update */); 693 } 694 }; 695 696 private final Animation mAnimateToStartPosition = new Animation() { 697 @Override 698 public void applyTransformation(float interpolatedTime, Transformation t) { 699 int targetTop = 0; 700 if (mFrom != mOriginalOffsetTop) { 701 targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); 702 } 703 int offset = targetTop - mCircleView.getTop(); 704 setTargetOffsetTopAndBottom(offset, false /* requires update */); 705 } 706 }; 707 708 private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { 709 mCircleView.bringToFront(); 710 mCircleView.offsetTopAndBottom(offset); 711 mCurrentTargetOffsetTop = mCircleView.getTop(); 712 if (requiresUpdate && android.os.Build.VERSION.SDK_INT <= 10) { 713 mCircleView.invalidate(); 714 } 715 } 716 717 private void onSecondaryPointerUp(MotionEvent ev) { 718 final int pointerIndex = MotionEventCompat.getActionIndex(ev); 719 final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); 720 if (pointerId == mActivePointerId) { 721 // This was our active pointer going up. Choose a new 722 // active pointer and adjust accordingly. 723 final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 724 mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); 725 } 726 } 727 728 /** 729 * Classes that wish to be notified when the swipe gesture correctly 730 * triggers a refresh should implement this interface. 731 */ 732 public interface OnRefreshListener { 733 public void onRefresh(); 734 } 735} 736