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