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