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