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