SwipeProgressBar.java revision cb084a5eea8cc641096fc288cb7156e0bb866d81
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.graphics.Canvas;
20import android.graphics.Paint;
21import android.graphics.Rect;
22import android.graphics.RectF;
23import android.support.v4.view.ViewCompat;
24import android.view.View;
25import android.view.animation.AnimationUtils;
26import android.view.animation.Interpolator;
27
28
29/**
30 * Custom progress bar that shows a cycle of colors as widening circles that
31 * overdraw each other. When finished, the bar is cleared from the inside out as
32 * the main cycle continues. Before running, this can also indicate how close
33 * the user is to triggering something (e.g. how far they need to pull down to
34 * trigger a refresh).
35 */
36final class SwipeProgressBar {
37
38    // Default progress animation colors are grays.
39    private final static int COLOR1 = 0xB3000000;
40    private final static int COLOR2 = 0x80000000;
41    private final static int COLOR3 = 0x4d000000;
42    private final static int COLOR4 = 0x1a000000;
43
44    // The duration of the animation cycle.
45    private static final int ANIMATION_DURATION_MS = 2000;
46
47    // The duration of the animation to clear the bar.
48    private static final int FINISH_ANIMATION_DURATION_MS = 1000;
49
50    // Interpolator for varying the speed of the animation.
51    private static final Interpolator INTERPOLATOR = BakedBezierInterpolator.getInstance();
52
53    private final Paint mPaint = new Paint();
54    private final RectF mClipRect = new RectF();
55    private float mTriggerPercentage;
56    private long mStartTime;
57    private long mFinishTime;
58    private boolean mRunning;
59
60    // Colors used when rendering the animation,
61    private int mColor1;
62    private int mColor2;
63    private int mColor3;
64    private int mColor4;
65    private View mParent;
66
67    private Rect mBounds = new Rect();
68
69    public SwipeProgressBar(View parent) {
70        mParent = parent;
71        mColor1 = COLOR1;
72        mColor2 = COLOR2;
73        mColor3 = COLOR3;
74        mColor4 = COLOR4;
75    }
76
77    /**
78     * Set the four colors used in the progress animation. The first color will
79     * also be the color of the bar that grows in response to a user swipe
80     * gesture.
81     *
82     * @param color1 Integer representation of a color.
83     * @param color2 Integer representation of a color.
84     * @param color3 Integer representation of a color.
85     * @param color4 Integer representation of a color.
86     */
87    void setColorScheme(int color1, int color2, int color3, int color4) {
88        mColor1 = color1;
89        mColor2 = color2;
90        mColor3 = color3;
91        mColor4 = color4;
92    }
93
94    /**
95     * Update the progress the user has made toward triggering the swipe
96     * gesture. and use this value to update the percentage of the trigger that
97     * is shown.
98     */
99    void setTriggerPercentage(float triggerPercentage) {
100        mTriggerPercentage = triggerPercentage;
101        mStartTime = 0;
102        ViewCompat.postInvalidateOnAnimation(mParent);
103    }
104
105    /**
106     * Start showing the progress animation.
107     */
108    void start() {
109        if (!mRunning) {
110            mTriggerPercentage = 0;
111            mStartTime = AnimationUtils.currentAnimationTimeMillis();
112            mRunning = true;
113            mParent.postInvalidate();
114        }
115    }
116
117    /**
118     * Stop showing the progress animation.
119     */
120    void stop() {
121        if (mRunning) {
122            mTriggerPercentage = 0;
123            mFinishTime = AnimationUtils.currentAnimationTimeMillis();
124            mRunning = false;
125            mParent.postInvalidate();
126        }
127    }
128
129    /**
130     * @return Return whether the progress animation is currently running.
131     */
132    boolean isRunning() {
133        return mRunning || mFinishTime > 0;
134    }
135
136    void draw(Canvas canvas) {
137        final int width = mBounds.width();
138        final int height = mBounds.height();
139        final int cx = width / 2;
140        final int cy = mBounds.top + height / 2;
141        boolean drawTriggerWhileFinishing = false;
142        int restoreCount = canvas.save();
143        canvas.clipRect(mBounds);
144
145        if (mRunning || (mFinishTime > 0)) {
146            long now = AnimationUtils.currentAnimationTimeMillis();
147            long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS;
148            long iterations = (now - mStartTime) / ANIMATION_DURATION_MS;
149            float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f));
150
151            // If we're not running anymore, that means we're running through
152            // the finish animation.
153            if (!mRunning) {
154                // If the finish animation is done, don't draw anything, and
155                // don't repost.
156                if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) {
157                    mFinishTime = 0;
158                    return;
159                }
160
161                // Otherwise, use a 0 opacity alpha layer to clear the animation
162                // from the inside out. This layer will prevent the circles from
163                // drawing within its bounds.
164                long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS;
165                float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f));
166                float pct = (finishProgress / 100f);
167                // Radius of the circle is half of the screen.
168                float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct);
169                mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height);
170                canvas.saveLayerAlpha(mClipRect, 0, 0);
171                // Only draw the trigger if there is a space in the center of
172                // this refreshing view that needs to be filled in by the
173                // trigger. If the progress view is just still animating, let it
174                // continue animating.
175                drawTriggerWhileFinishing = true;
176            }
177
178            // First fill in with the last color that would have finished drawing.
179            if (iterations == 0) {
180                canvas.drawColor(mColor1);
181            } else {
182                if (rawProgress >= 0 && rawProgress < 25) {
183                    canvas.drawColor(mColor4);
184                } else if (rawProgress >= 25 && rawProgress < 50) {
185                    canvas.drawColor(mColor1);
186                } else if (rawProgress >= 50 && rawProgress < 75) {
187                    canvas.drawColor(mColor2);
188                } else {
189                    canvas.drawColor(mColor3);
190                }
191            }
192
193            // Then draw up to 4 overlapping concentric circles of varying radii, based on how far
194            // along we are in the cycle.
195            // progress 0-50 draw mColor2
196            // progress 25-75 draw mColor3
197            // progress 50-100 draw mColor4
198            // progress 75 (wrap to 25) draw mColor1
199            if ((rawProgress >= 0 && rawProgress <= 25)) {
200                float pct = (((rawProgress + 25) * 2) / 100f);
201                drawCircle(canvas, cx, cy, mColor1, pct);
202            }
203            if (rawProgress >= 0 && rawProgress <= 50) {
204                float pct = ((rawProgress * 2) / 100f);
205                drawCircle(canvas, cx, cy, mColor2, pct);
206            }
207            if (rawProgress >= 25 && rawProgress <= 75) {
208                float pct = (((rawProgress - 25) * 2) / 100f);
209                drawCircle(canvas, cx, cy, mColor3, pct);
210            }
211            if (rawProgress >= 50 && rawProgress <= 100) {
212                float pct = (((rawProgress - 50) * 2) / 100f);
213                drawCircle(canvas, cx, cy, mColor4, pct);
214            }
215            if ((rawProgress >= 75 && rawProgress <= 100)) {
216                float pct = (((rawProgress - 75) * 2) / 100f);
217                drawCircle(canvas, cx, cy, mColor1, pct);
218            }
219            if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) {
220                // There is some portion of trigger to draw. Restore the canvas,
221                // then draw the trigger. Otherwise, the trigger does not appear
222                // until after the bar has finished animating and appears to
223                // just jump in at a larger width than expected.
224                canvas.restoreToCount(restoreCount);
225                restoreCount = canvas.save();
226                canvas.clipRect(mBounds);
227                drawTrigger(canvas, cx, cy);
228            }
229            // Keep running until we finish out the last cycle.
230            ViewCompat.postInvalidateOnAnimation(mParent);
231        } else {
232            // Otherwise if we're in the middle of a trigger, draw that.
233            if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) {
234                drawTrigger(canvas, cx, cy);
235            }
236        }
237        canvas.restoreToCount(restoreCount);
238    }
239
240    private void drawTrigger(Canvas canvas, int cx, int cy) {
241        mPaint.setColor(mColor1);
242        canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint);
243    }
244
245    /**
246     * Draws a circle centered in the view.
247     *
248     * @param canvas the canvas to draw on
249     * @param cx the center x coordinate
250     * @param cy the center y coordinate
251     * @param color the color to draw
252     * @param pct the percentage of the view that the circle should cover
253     */
254    private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) {
255        mPaint.setColor(color);
256        canvas.save();
257        canvas.translate(cx, cy);
258        float radiusScale = INTERPOLATOR.getInterpolation(pct);
259        canvas.scale(radiusScale, radiusScale);
260        canvas.drawCircle(0, 0, cx, mPaint);
261        canvas.restore();
262    }
263
264    /**
265     * Set the drawing bounds of this SwipeProgressBar.
266     */
267    void setBounds(int left, int top, int right, int bottom) {
268        mBounds.left = left;
269        mBounds.top = top;
270        mBounds.right = right;
271        mBounds.bottom = bottom;
272    }
273}
274