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