MaterialProgressDrawable.java revision 12ffe36178df269e0c2d3b33f7de360e74c63f71
1/*
2 * Copyright (C) 2014 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.view.animation.AccelerateDecelerateInterpolator;
20import android.view.animation.Interpolator;
21import android.view.animation.Animation;
22import android.view.animation.LinearInterpolator;
23import android.view.animation.Transformation;
24import android.content.Context;
25import android.content.res.Resources;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.ColorFilter;
29import android.graphics.Paint;
30import android.graphics.Paint.Cap;
31import android.graphics.Paint.Style;
32import android.graphics.PixelFormat;
33import android.graphics.Rect;
34import android.graphics.RectF;
35import android.graphics.drawable.Drawable;
36import android.graphics.drawable.Animatable;
37import android.util.DisplayMetrics;
38import android.view.View;
39import java.util.ArrayList;
40
41/**
42 * Fancy progress indicator for Material theme.
43 */
44class MaterialProgressDrawable extends Drawable implements Animatable {
45    private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
46    private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator();
47    private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator();
48
49    // Maps to ProgressBar.Large style
50    static final int LARGE = 0;
51    // Maps to ProgressBar default style
52    static final int DEFAULT = 1;
53    // Maps to ProgressBar.Small style
54    static final int SMALL = 2;
55
56    // Maps to ProgressBar default style
57    private static final int CIRCLE_DIAMETER = 48;
58    private static final int INNER_RADIUS = 19;
59    private static final int STROKE_WIDTH = 4;
60
61    // Maps to ProgressBar.Large style
62    private static final int CIRCLE_DIAMETER_LARGE = 76;
63    private static final float INNER_RADIUS_LARGE = 30.1f;
64    private static final float STROKE_WIDTH_LARGE = 6.3f;
65
66    // Maps to ProgressBar.Small style
67    private static final int CIRCLE_DIAMETER_SMALL = 16;
68    private static final float INNER_RADIUS_SMALL = 6.3f;
69    private static final float STROKE_WIDTH_SMALL = 1.3f;
70
71    private final int[] COLORS = new int[] {
72        Color.BLACK
73    };
74
75    /** The duration of a single progress spin in milliseconds. */
76    private static final int ANIMATION_DURATION = 1000 * 80 / 60;
77
78    /** The number of points in the progress "star". */
79    private static final float NUM_POINTS = 5f;
80
81    /** The list of animators operating on this drawable. */
82    private final ArrayList<Animation> mAnimators = new ArrayList<Animation>();
83
84    /** The indicator ring, used to manage animation state. */
85    private final Ring mRing;
86
87    /** Canvas rotation in degrees. */
88    private float mRotation;
89
90    private Resources mResources;
91    private int mColorIndex;
92    private View mParent;
93    private Animation mAnimation;
94    private float mRotationCount;
95    private int[] mColors;
96    private double mWidth;
97    private double mHeight;
98    private double mInnerRadius;
99    private double mStrokeWidth;
100    private Animation mFinishAnimation;
101
102    public MaterialProgressDrawable(Context context, View parent) {
103        mParent = parent;
104        mResources = context.getResources();
105
106        mRing = new Ring(mCallback);
107        mColors = COLORS;
108        mRing.setColors(mColors);
109
110        initialize(CIRCLE_DIAMETER, CIRCLE_DIAMETER, INNER_RADIUS, STROKE_WIDTH);
111        setupAnimators();
112    }
113
114    private void initialize(double progressCircleWidth, double progressCircleHeight,
115            double innerRadius, double strokeWidth) {
116        final Ring ring = mRing;
117        final DisplayMetrics metrics = mResources.getDisplayMetrics();
118        final float screenDensity = metrics.density;
119
120        mWidth = progressCircleWidth * screenDensity;
121        mHeight = progressCircleHeight * screenDensity;
122        mInnerRadius = innerRadius * screenDensity;
123        mStrokeWidth = strokeWidth * screenDensity;
124        ring.setStrokeWidth((float) mStrokeWidth);
125
126        final int color = mColors[0];
127        ring.setColor(color);
128
129        final float minEdge = (float) Math.min(mWidth, mHeight);
130        if (mInnerRadius <= 0 || minEdge < 0) {
131            ring.setInsets((int) Math.ceil(mStrokeWidth / 2.0f));
132        } else {
133            float insets = (float) (minEdge / 2.0f - mInnerRadius);
134            ring.setInsets(insets);
135        }
136    }
137
138    public void updateSizes(int size) {
139        final DisplayMetrics metrics = mResources.getDisplayMetrics();
140        final float screenDensity = metrics.density;
141        int progressCircleWidth;
142        int progressCircleHeight;
143        float innerRadius;
144        float strokeWidth;
145
146        if (size == LARGE) {
147            progressCircleWidth = progressCircleHeight = CIRCLE_DIAMETER_LARGE;
148            innerRadius = INNER_RADIUS_LARGE;
149            strokeWidth = STROKE_WIDTH_LARGE;
150        } else if (size == SMALL) {
151            progressCircleWidth = progressCircleHeight = CIRCLE_DIAMETER_SMALL;
152            innerRadius = INNER_RADIUS_SMALL;
153            strokeWidth = STROKE_WIDTH_SMALL;
154        } else {
155            progressCircleWidth = progressCircleHeight = CIRCLE_DIAMETER;
156            innerRadius = INNER_RADIUS;
157            strokeWidth = STROKE_WIDTH;
158        }
159        mWidth = progressCircleWidth * screenDensity;
160        mHeight = progressCircleHeight * screenDensity;
161        mInnerRadius = innerRadius * screenDensity;
162        mStrokeWidth = strokeWidth * screenDensity;
163    }
164
165    public void setStartEndTrim(float s, float e) {
166        mRing.setStartTrim(s);
167        mRing.setEndTrim(e);
168    }
169
170    public void setProgressRotation(float r) {
171        mRing.setRotation(r);
172    }
173
174    /**
175     * Set the colors used in the progress animation from color resources.
176     * The first color will also be the color of the bar that grows in response
177     * to a user swipe gesture.
178     *
179     * @param colors
180     */
181    public void setColorSchemeColors(int... colors) {
182        mColors = colors;
183        mRing.setColors(mColors);
184    }
185
186    @Override
187    public int getIntrinsicHeight() {
188        return (int) mHeight;
189    }
190
191    @Override
192    public int getIntrinsicWidth() {
193        return (int) mWidth;
194    }
195
196    @Override
197    public void draw(Canvas c) {
198        final Rect bounds = getBounds();
199        final int saveCount = c.save();
200        c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
201        mRing.draw(c, bounds);
202        c.restoreToCount(saveCount);
203    }
204
205    @Override
206    public void setAlpha(int alpha) {
207        mRing.setAlpha(alpha);
208    }
209
210    public int getAlpha() {
211        return mRing.getAlpha();
212    }
213
214    @Override
215    public void setColorFilter(ColorFilter colorFilter) {
216        mRing.setColorFilter(colorFilter);
217    }
218
219    @SuppressWarnings("unused")
220    private void setRotation(float rotation) {
221        mRotation = rotation;
222        invalidateSelf();
223    }
224
225    @SuppressWarnings("unused")
226    private float getRotation() {
227        return mRotation;
228    }
229
230    @Override
231    public int getOpacity() {
232        return PixelFormat.TRANSLUCENT;
233    }
234
235    @Override
236    public boolean isRunning() {
237        final ArrayList<Animation> animators = mAnimators;
238        final int N = animators.size();
239        for (int i = 0; i < N; i++) {
240            final Animation animator = animators.get(i);
241            if (animator.hasStarted() && !animator.hasEnded()) {
242                return true;
243            }
244        }
245        return false;
246    }
247
248    @Override
249    public void start() {
250        mAnimation.reset();
251        mRing.storeOriginals();
252        if (mRing.getStartingStartTrim() != 0) {
253            mParent.startAnimation(mFinishAnimation);
254        } else {
255            mColorIndex = 0;
256            mRing.setColorIndex(mColorIndex);
257            mRing.resetOriginals();
258            mParent.startAnimation(mAnimation);
259        }
260    }
261
262    @Override
263    public void stop() {
264        mParent.clearAnimation();
265        setRotation(0);
266        mColorIndex = 0;
267        mRing.setColorIndex(mColorIndex);
268        mRing.resetOriginals();
269    }
270
271    private void setupAnimators() {
272        final Ring ring = mRing;
273        final Animation finishRingAnimation = new Animation() {
274            public void applyTransformation(float interpolatedTime, Transformation t) {
275                // shrink back down and complete a full roation before starting other circles
276                float targetRotation = (float) (Math.floor(ring.getStartingRotation() / .75f) + 1f);
277                final float startTrim = ring.getStartingEndTrim()
278                        + (ring.getStartingStartTrim() - ring.getStartingEndTrim())
279                        * interpolatedTime;
280                ring.setEndTrim(startTrim);
281                final float rotation = ring.getStartingRotation()
282                        + ((targetRotation - ring.getStartingRotation()) * interpolatedTime);
283                ring.setRotation(rotation);
284            }
285        };
286        finishRingAnimation.setInterpolator(LINEAR_INTERPOLATOR);
287        finishRingAnimation.setDuration(ANIMATION_DURATION / 2);
288        finishRingAnimation.setAnimationListener(new Animation.AnimationListener() {
289
290            @Override
291            public void onAnimationStart(Animation animation) {
292            }
293
294            @Override
295            public void onAnimationEnd(Animation animation) {
296                mColorIndex = (mColorIndex + 1) % (mColors.length);
297                ring.setColorIndex(mColorIndex);
298                ring.resetOriginals();
299                mParent.startAnimation(mAnimation);
300            }
301
302            @Override
303            public void onAnimationRepeat(Animation animation) {
304            }
305        });
306        final Animation animation = new Animation() {
307            @Override
308            public void applyTransformation(float interpolatedTime, Transformation t) {
309                final float endTrim =
310                        0.75f * START_CURVE_INTERPOLATOR
311                                .getInterpolation(interpolatedTime);
312                ring.setEndTrim(endTrim);
313                final float startTrim = 0.75f * END_CURVE_INTERPOLATOR
314                                .getInterpolation(interpolatedTime);
315                ring.setStartTrim(startTrim);
316                final float rotation = 0.25f * interpolatedTime;
317                ring.setRotation(rotation);
318                float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime)
319                        + (720.0f * (mRotationCount / NUM_POINTS));
320                setRotation(groupRotation);
321            }
322        };
323        animation.setRepeatCount(Animation.INFINITE);
324        animation.setRepeatMode(Animation.RESTART);
325        animation.setInterpolator(LINEAR_INTERPOLATOR);
326        animation.setDuration(ANIMATION_DURATION);
327        animation.setAnimationListener(new Animation.AnimationListener() {
328
329            @Override
330            public void onAnimationStart(Animation animation) {
331                mRotationCount = 0;
332            }
333
334            @Override
335            public void onAnimationEnd(Animation animation) {
336                // do nothing
337            }
338
339            @Override
340            public void onAnimationRepeat(Animation animation) {
341                mColorIndex = (mColorIndex + 1) % (mColors.length);
342                ring.setColorIndex(mColorIndex);
343                ring.resetOriginals();
344                mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
345            }
346        });
347        mFinishAnimation = finishRingAnimation;
348        mAnimation = animation;
349    }
350
351    private final Callback mCallback = new Callback() {
352        @Override
353        public void invalidateDrawable(Drawable d) {
354            invalidateSelf();
355        }
356
357        @Override
358        public void scheduleDrawable(Drawable d, Runnable what, long when) {
359            scheduleSelf(what, when);
360        }
361
362        @Override
363        public void unscheduleDrawable(Drawable d, Runnable what) {
364            unscheduleSelf(what);
365        }
366    };
367
368    private static class Ring {
369        private final RectF mTempBounds = new RectF();
370        private final Paint mPaint = new Paint();
371
372        private final Callback mCallback;
373
374        private float mStartTrim = 0.0f;
375        private float mEndTrim = 0.0f;
376        private float mRotation = 0.0f;
377        private float mStrokeWidth = 5.0f;
378        private float mStrokeInset = 2.5f;
379
380        private int mAlpha = 0xFF;
381        private int mColor = Color.BLACK;
382        private int[] mColors;
383        private int mColorIndex;
384        private float mStartingStartTrim;
385        private float mStartingEndTrim;
386        private float mStartingRotation;
387
388        public Ring(Callback callback) {
389            mCallback = callback;
390
391            mPaint.setStrokeCap(Cap.ROUND);
392            mPaint.setAntiAlias(true);
393            mPaint.setStyle(Style.STROKE);
394        }
395
396        public float getStartingRotation() {
397            return mStartingRotation;
398        }
399
400        /**
401         * If the start / end trim are offset to begin with, store them so that
402         * animation starts from that offset.
403         */
404        public void storeOriginals() {
405            mStartingStartTrim = mStartTrim;
406            mStartingEndTrim = mEndTrim;
407            mStartingRotation = mRotation;
408        }
409
410        public void resetOriginals() {
411            mStartingStartTrim = 0;
412            mStartingEndTrim = 0;
413            mStartingRotation = 0;
414            setStartTrim(0);
415            setEndTrim(0);
416            setRotation(0);
417        }
418
419        public void draw(Canvas c, Rect bounds) {
420            final RectF arcBounds = mTempBounds;
421            arcBounds.set(bounds);
422            arcBounds.inset(mStrokeInset, mStrokeInset);
423
424            final float startAngle = (mStartTrim + mRotation) * 360;
425            final float endAngle = (mEndTrim + mRotation) * 360;
426            float sweepAngle = endAngle - startAngle;
427
428            // Ensure the sweep angle isn't too small to draw.
429            final float diameter = Math.min(arcBounds.width(), arcBounds.height());
430            final float minAngle = (float) (360.0 / (diameter * Math.PI));
431            if (sweepAngle < minAngle && sweepAngle > -minAngle) {
432                sweepAngle = Math.signum(sweepAngle) * minAngle;
433            }
434            mPaint.setColor(mColors[mColorIndex]);
435            c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
436        }
437
438        public void setColors(int[] colors) {
439            mColors = colors;
440        }
441
442        public void setColorIndex(int index) {
443            mColorIndex = index;
444        }
445
446        public void setColorFilter(ColorFilter filter) {
447            mPaint.setColorFilter(filter);
448            invalidateSelf();
449        }
450
451        public ColorFilter getColorFilter() {
452            return mPaint.getColorFilter();
453        }
454
455        public void setAlpha(int alpha) {
456            mAlpha = alpha;
457            mPaint.setColor(mColor & 0xFFFFFF | alpha << 24);
458            invalidateSelf();
459        }
460
461        public int getAlpha() {
462            return mAlpha;
463        }
464
465        public void setColor(int color) {
466            mColor = color;
467            mPaint.setColor(color & 0xFFFFFF | mAlpha << 24);
468            invalidateSelf();
469        }
470
471        public int getColor() {
472            return mColor;
473        }
474
475        public void setStrokeWidth(float strokeWidth) {
476            mStrokeWidth = strokeWidth;
477            mPaint.setStrokeWidth(strokeWidth);
478            invalidateSelf();
479        }
480
481        @SuppressWarnings("unused")
482        public float getStrokeWidth() {
483            return mStrokeWidth;
484        }
485
486        @SuppressWarnings("unused")
487        public void setStartTrim(float startTrim) {
488            mStartTrim = startTrim;
489            invalidateSelf();
490        }
491
492        @SuppressWarnings("unused")
493        public float getStartTrim() {
494            return mStartTrim;
495        }
496
497        public float getStartingStartTrim() {
498            return mStartingStartTrim;
499        }
500
501        public float getStartingEndTrim() {
502            return mStartingEndTrim;
503        }
504
505        @SuppressWarnings("unused")
506        public void setEndTrim(float endTrim) {
507            mEndTrim = endTrim;
508            invalidateSelf();
509        }
510
511        @SuppressWarnings("unused")
512        public float getEndTrim() {
513            return mEndTrim;
514        }
515
516        @SuppressWarnings("unused")
517        public void setRotation(float rotation) {
518            mRotation = rotation;
519            invalidateSelf();
520        }
521
522        @SuppressWarnings("unused")
523        public float getRotation() {
524            return mRotation;
525        }
526
527        public void setInsets(float insets) {
528            mStrokeInset = insets;
529        }
530
531        @SuppressWarnings("unused")
532        public float getInsets() {
533            return mStrokeInset;
534        }
535
536        private void invalidateSelf() {
537            mCallback.invalidateDrawable(null);
538        }
539    }
540
541    /**
542     * Squishes the interpolation curve into the second half of the animation.
543     */
544    private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator {
545        @Override
546        public float getInterpolation(float input) {
547            return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f));
548        }
549    }
550
551    /**
552     * Squishes the interpolation curve into the first half of the animation.
553     */
554    private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator {
555        @Override
556        public float getInterpolation(float input) {
557            return super.getInterpolation(Math.min(1, input * 2.0f));
558        }
559    }
560}
561