MaterialProgressDrawable.java revision 4221e345d86111915467679815ead8888da5e2f4
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.Matrix;
30import android.graphics.Paint;
31import android.graphics.Paint.Style;
32import android.graphics.Path;
33import android.graphics.PixelFormat;
34import android.graphics.Rect;
35import android.graphics.RectF;
36import android.graphics.drawable.Drawable;
37import android.graphics.drawable.Animatable;
38import android.support.annotation.IntDef;
39import android.support.annotation.NonNull;
40import android.util.DisplayMetrics;
41import android.view.View;
42
43import java.lang.annotation.Retention;
44import java.lang.annotation.RetentionPolicy;
45import java.util.ArrayList;
46
47/**
48 * Fancy progress indicator for Material theme.
49 *
50 * @hide
51 */
52class MaterialProgressDrawable extends Drawable implements Animatable {
53    private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
54    private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator();
55    private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator();
56    private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator();
57
58    @Retention(RetentionPolicy.CLASS)
59    @IntDef({LARGE, DEFAULT})
60    public @interface ProgressDrawableSize {}
61    // Maps to ProgressBar.Large style
62    static final int LARGE = 0;
63    // Maps to ProgressBar default style
64    static final int DEFAULT = 1;
65
66    // Maps to ProgressBar default style
67    private static final int CIRCLE_DIAMETER = 40;
68    private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width
69    private static final float STROKE_WIDTH = 2.5f;
70
71    // Maps to ProgressBar.Large style
72    private static final int CIRCLE_DIAMETER_LARGE = 56;
73    private static final float CENTER_RADIUS_LARGE = 12.5f;
74    private static final float STROKE_WIDTH_LARGE = 3f;
75
76    private final int[] COLORS = new int[] {
77        Color.BLACK
78    };
79
80    /** The duration of a single progress spin in milliseconds. */
81    private static final int ANIMATION_DURATION = 1000 * 80 / 60;
82
83    /** The number of points in the progress "star". */
84    private static final float NUM_POINTS = 5f;
85    /** The list of animators operating on this drawable. */
86    private final ArrayList<Animation> mAnimators = new ArrayList<Animation>();
87
88    /** The indicator ring, used to manage animation state. */
89    private final Ring mRing;
90
91    /** Canvas rotation in degrees. */
92    private float mRotation;
93
94    /** Layout info for the arrowhead in dp */
95    private static final int ARROW_WIDTH = 10;
96    private static final int ARROW_HEIGHT = 5;
97    private static final float ARROW_OFFSET_ANGLE = 5;
98
99    /** Layout info for the arrowhead for the large spinner in dp */
100    private static final int ARROW_WIDTH_LARGE = 12;
101    private static final int ARROW_HEIGHT_LARGE = 6;
102    private static final float MAX_PROGRESS_ARC = .8f;
103
104    private Resources mResources;
105    private View mParent;
106    private Animation mAnimation;
107    private float mRotationCount;
108    private double mWidth;
109    private double mHeight;
110    private Animation mFinishAnimation;
111
112    public MaterialProgressDrawable(Context context, View parent) {
113        mParent = parent;
114        mResources = context.getResources();
115
116        mRing = new Ring(mCallback);
117        mRing.setColors(COLORS);
118
119        updateSizes(DEFAULT);
120        setupAnimators();
121    }
122
123    private void setSizeParameters(double progressCircleWidth, double progressCircleHeight,
124            double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) {
125        final Ring ring = mRing;
126        final DisplayMetrics metrics = mResources.getDisplayMetrics();
127        final float screenDensity = metrics.density;
128
129        mWidth = progressCircleWidth * screenDensity;
130        mHeight = progressCircleHeight * screenDensity;
131        ring.setStrokeWidth((float) strokeWidth * screenDensity);
132        ring.setCenterRadius(centerRadius * screenDensity);
133        ring.setColorIndex(0);
134        ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity);
135        ring.setInsets((int) mWidth, (int) mHeight);
136    }
137
138    /**
139     * Set the overall size for the progress spinner. This updates the radius
140     * and stroke width of the ring.
141     *
142     * @param size One of {@link MaterialProgressDrawable.LARGE} or
143     *            {@link MaterialProgressDrawable.DEFAULT}
144     */
145    public void updateSizes(@ProgressDrawableSize int size) {
146        if (size == LARGE) {
147            setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE,
148                    STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE);
149        } else {
150            setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH,
151                    ARROW_WIDTH, ARROW_HEIGHT);
152        }
153    }
154
155    /**
156     * @param show Set to true to display the arrowhead on the progress spinner.
157     */
158    public void showArrow(boolean show) {
159        mRing.setShowArrow(show);
160    }
161
162    /**
163     * @param scale Set the scale of the arrowhead for the spinner.
164     */
165    public void setArrowScale(float scale) {
166        mRing.setArrowScale(scale);
167    }
168
169    /**
170     * Set the start and end trim for the progress spinner arc.
171     *
172     * @param startAngle start angle
173     * @param endAngle end angle
174     */
175    public void setStartEndTrim(float startAngle, float endAngle) {
176        mRing.setStartTrim(startAngle);
177        mRing.setEndTrim(endAngle);
178    }
179
180    /**
181     * Set the amount of rotation to apply to the progress spinner.
182     *
183     * @param rotation Rotation is from [0..1]
184     */
185    public void setProgressRotation(float rotation) {
186        mRing.setRotation(rotation);
187    }
188
189    /**
190     * Update the background color of the circle image view.
191     */
192    public void setBackgroundColor(int color) {
193        mRing.setBackgroundColor(color);
194     }
195
196    /**
197     * Set the colors used in the progress animation from color resources.
198     * The first color will also be the color of the bar that grows in response
199     * to a user swipe gesture.
200     *
201     * @param colors
202     */
203    public void setColorSchemeColors(int... colors) {
204        mRing.setColors(colors);
205        mRing.setColorIndex(0);
206    }
207
208    @Override
209    public int getIntrinsicHeight() {
210        return (int) mHeight;
211    }
212
213    @Override
214    public int getIntrinsicWidth() {
215        return (int) mWidth;
216    }
217
218    @Override
219    public void draw(Canvas c) {
220        final Rect bounds = getBounds();
221        final int saveCount = c.save();
222        c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
223        mRing.draw(c, bounds);
224        c.restoreToCount(saveCount);
225    }
226
227    @Override
228    public void setAlpha(int alpha) {
229        mRing.setAlpha(alpha);
230    }
231
232    public int getAlpha() {
233        return mRing.getAlpha();
234    }
235
236    @Override
237    public void setColorFilter(ColorFilter colorFilter) {
238        mRing.setColorFilter(colorFilter);
239    }
240
241    @SuppressWarnings("unused")
242    void setRotation(float rotation) {
243        mRotation = rotation;
244        invalidateSelf();
245    }
246
247    @SuppressWarnings("unused")
248    private float getRotation() {
249        return mRotation;
250    }
251
252    @Override
253    public int getOpacity() {
254        return PixelFormat.TRANSLUCENT;
255    }
256
257    @Override
258    public boolean isRunning() {
259        final ArrayList<Animation> animators = mAnimators;
260        final int N = animators.size();
261        for (int i = 0; i < N; i++) {
262            final Animation animator = animators.get(i);
263            if (animator.hasStarted() && !animator.hasEnded()) {
264                return true;
265            }
266        }
267        return false;
268    }
269
270    @Override
271    public void start() {
272        mAnimation.reset();
273        mRing.storeOriginals();
274        // Already showing some part of the ring
275        if (mRing.getEndTrim() != mRing.getStartTrim()) {
276            mParent.startAnimation(mFinishAnimation);
277        } else {
278            mRing.setColorIndex(0);
279            mRing.resetOriginals();
280            mParent.startAnimation(mAnimation);
281        }
282    }
283
284    @Override
285    public void stop() {
286        mParent.clearAnimation();
287        setRotation(0);
288        mRing.setShowArrow(false);
289        mRing.setColorIndex(0);
290        mRing.resetOriginals();
291    }
292
293    private void setupAnimators() {
294        final Ring ring = mRing;
295        final Animation finishRingAnimation = new Animation() {
296            public void applyTransformation(float interpolatedTime, Transformation t) {
297                // shrink back down and complete a full rotation before starting other circles
298                // Rotation goes between [0..1].
299                float targetRotation = (float) (Math.floor(ring.getStartingRotation()
300                        / MAX_PROGRESS_ARC) + 1f);
301                final float startTrim = ring.getStartingStartTrim()
302                        + (ring.getStartingEndTrim() - ring.getStartingStartTrim())
303                        * interpolatedTime;
304                ring.setStartTrim(startTrim);
305                final float rotation = ring.getStartingRotation()
306                        + ((targetRotation - ring.getStartingRotation()) * interpolatedTime);
307                ring.setRotation(rotation);
308                ring.setArrowScale(1 - interpolatedTime);
309            }
310        };
311        finishRingAnimation.setInterpolator(EASE_INTERPOLATOR);
312        finishRingAnimation.setDuration(ANIMATION_DURATION/2);
313        finishRingAnimation.setAnimationListener(new Animation.AnimationListener() {
314
315            @Override
316            public void onAnimationStart(Animation animation) {
317            }
318
319            @Override
320            public void onAnimationEnd(Animation animation) {
321                ring.goToNextColor();
322                ring.storeOriginals();
323                ring.setShowArrow(false);
324                mParent.startAnimation(mAnimation);
325            }
326
327            @Override
328            public void onAnimationRepeat(Animation animation) {
329            }
330        });
331        final Animation animation = new Animation() {
332            @Override
333            public void applyTransformation(float interpolatedTime, Transformation t) {
334                // The minProgressArc is calculated from 0 to create an angle that
335                // matches the stroke width.
336                final float minProgressArc = (float) Math.toRadians(ring.getStrokeWidth()
337                        / (2 * Math.PI * ring.getCenterRadius()));
338                final float startingEndTrim = ring.getStartingEndTrim();
339                final float startingTrim = ring.getStartingStartTrim();
340                final float startingRotation = ring.getStartingRotation();
341
342                // Offset the minProgressArc to where the endTrim is located.
343                final float minArc = MAX_PROGRESS_ARC - minProgressArc;
344                final float endTrim = startingEndTrim
345                        + (minArc * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime));
346                ring.setEndTrim(endTrim);
347
348                final float startTrim = startingTrim
349                        + (MAX_PROGRESS_ARC * END_CURVE_INTERPOLATOR
350                                .getInterpolation(interpolatedTime));
351                ring.setStartTrim(startTrim);
352
353                final float rotation = startingRotation + (0.25f * interpolatedTime);
354                ring.setRotation(rotation);
355
356                float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime)
357                        + (720.0f * (mRotationCount / NUM_POINTS));
358                setRotation(groupRotation);
359            }
360        };
361        animation.setRepeatCount(Animation.INFINITE);
362        animation.setRepeatMode(Animation.RESTART);
363        animation.setInterpolator(LINEAR_INTERPOLATOR);
364        animation.setDuration(ANIMATION_DURATION);
365        animation.setAnimationListener(new Animation.AnimationListener() {
366
367            @Override
368            public void onAnimationStart(Animation animation) {
369                mRotationCount = 0;
370            }
371
372            @Override
373            public void onAnimationEnd(Animation animation) {
374                // do nothing
375            }
376
377            @Override
378            public void onAnimationRepeat(Animation animation) {
379                ring.storeOriginals();
380                ring.goToNextColor();
381                ring.setStartTrim(ring.getEndTrim());
382                mRotationCount = (mRotationCount + 1) % (NUM_POINTS);
383            }
384        });
385        mFinishAnimation = finishRingAnimation;
386        mAnimation = animation;
387    }
388
389    private final Callback mCallback = new Callback() {
390        @Override
391        public void invalidateDrawable(Drawable d) {
392            invalidateSelf();
393        }
394
395        @Override
396        public void scheduleDrawable(Drawable d, Runnable what, long when) {
397            scheduleSelf(what, when);
398        }
399
400        @Override
401        public void unscheduleDrawable(Drawable d, Runnable what) {
402            unscheduleSelf(what);
403        }
404    };
405
406    private static class Ring {
407        private final RectF mTempBounds = new RectF();
408        private final Paint mPaint = new Paint();
409        private final Paint mArrowPaint = new Paint();
410
411        private final Callback mCallback;
412
413        private float mStartTrim = 0.0f;
414        private float mEndTrim = 0.0f;
415        private float mRotation = 0.0f;
416        private float mStrokeWidth = 5.0f;
417        private float mStrokeInset = 2.5f;
418
419        private int[] mColors;
420        // mColorIndex represents the offset into the available mColors that the
421        // progress circle should currently display. As the progress circle is
422        // animating, the mColorIndex moves by one to the next available color.
423        private int mColorIndex;
424        private float mStartingStartTrim;
425        private float mStartingEndTrim;
426        private float mStartingRotation;
427        private boolean mShowArrow;
428        private Path mArrow;
429        private float mArrowScale;
430        private double mRingCenterRadius;
431        private int mArrowWidth;
432        private int mArrowHeight;
433        private int mAlpha;
434        private final Paint mCirclePaint = new Paint();
435        private int mBackgroundColor;
436
437        public Ring(Callback callback) {
438            mCallback = callback;
439
440            mPaint.setStrokeCap(Paint.Cap.SQUARE);
441            mPaint.setAntiAlias(true);
442            mPaint.setStyle(Style.STROKE);
443
444            mArrowPaint.setStyle(Paint.Style.FILL);
445            mArrowPaint.setAntiAlias(true);
446        }
447
448        public void setBackgroundColor(int color) {
449            mBackgroundColor = color;
450        }
451
452        /**
453         * Set the dimensions of the arrowhead.
454         *
455         * @param width Width of the hypotenuse of the arrow head
456         * @param height Height of the arrow point
457         */
458        public void setArrowDimensions(float width, float height) {
459            mArrowWidth = (int) width;
460            mArrowHeight = (int) height;
461        }
462
463        /**
464         * Draw the progress spinner
465         */
466        public void draw(Canvas c, Rect bounds) {
467            final RectF arcBounds = mTempBounds;
468            arcBounds.set(bounds);
469            arcBounds.inset(mStrokeInset, mStrokeInset);
470
471            final float startAngle = (mStartTrim + mRotation) * 360;
472            final float endAngle = (mEndTrim + mRotation) * 360;
473            float sweepAngle = endAngle - startAngle;
474
475            mPaint.setColor(mColors[mColorIndex]);
476            c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
477
478            drawTriangle(c, startAngle, sweepAngle, bounds);
479
480            if (mAlpha < 255) {
481                mCirclePaint.setColor(mBackgroundColor);
482                mCirclePaint.setAlpha(255 - mAlpha);
483                c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2,
484                        mCirclePaint);
485            }
486        }
487
488        private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) {
489            if (mShowArrow) {
490                if (mArrow == null) {
491                    mArrow = new android.graphics.Path();
492                    mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD);
493                } else {
494                    mArrow.reset();
495                }
496
497                // Adjust the position of the triangle so that it is inset as
498                // much as the arc, but also centered on the arc.
499                float inset = (int) mStrokeInset / 2 * mArrowScale;
500                float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX());
501                float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY());
502
503                // Update the path each time. This works around an issue in SKIA
504                // where concatenating a rotation matrix to a scale matrix
505                // ignored a starting negative rotation. This appears to have
506                // been fixed as of API 21.
507                mArrow.moveTo(0, 0);
508                mArrow.lineTo(mArrowWidth * mArrowScale, 0);
509                mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight
510                        * mArrowScale));
511                mArrow.offset(x - inset, y);
512                mArrow.close();
513                // draw a triangle
514                mArrowPaint.setColor(mColors[mColorIndex]);
515                c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(),
516                        bounds.exactCenterY());
517                c.drawPath(mArrow, mArrowPaint);
518            }
519        }
520
521        /**
522         * Set the colors the progress spinner alternates between.
523         *
524         * @param colors Array of integers describing the colors. Must be non-<code>null</code>.
525         */
526        public void setColors(@NonNull int[] colors) {
527            mColors = colors;
528            // if colors are reset, make sure to reset the color index as well
529            setColorIndex(0);
530        }
531
532        /**
533         * @param index Index into the color array of the color to display in
534         *            the progress spinner.
535         */
536        public void setColorIndex(int index) {
537            mColorIndex = index;
538        }
539
540        /**
541         * Proceed to the next available ring color. This will automatically
542         * wrap back to the beginning of colors.
543         */
544        public void goToNextColor() {
545            mColorIndex = (mColorIndex + 1) % (mColors.length);
546        }
547
548        public void setColorFilter(ColorFilter filter) {
549            mPaint.setColorFilter(filter);
550            invalidateSelf();
551        }
552
553        /**
554         * @param alpha Set the alpha of the progress spinner and associated arrowhead.
555         */
556        public void setAlpha(int alpha) {
557            mAlpha = alpha;
558        }
559
560        /**
561         * @return Current alpha of the progress spinner and arrowhead.
562         */
563        public int getAlpha() {
564            return mAlpha;
565        }
566
567        /**
568         * @param strokeWidth Set the stroke width of the progress spinner in pixels.
569         */
570        public void setStrokeWidth(float strokeWidth) {
571            mStrokeWidth = strokeWidth;
572            mPaint.setStrokeWidth(strokeWidth);
573            invalidateSelf();
574        }
575
576        @SuppressWarnings("unused")
577        public float getStrokeWidth() {
578            return mStrokeWidth;
579        }
580
581        @SuppressWarnings("unused")
582        public void setStartTrim(float startTrim) {
583            mStartTrim = startTrim;
584            invalidateSelf();
585        }
586
587        @SuppressWarnings("unused")
588        public float getStartTrim() {
589            return mStartTrim;
590        }
591
592        public float getStartingStartTrim() {
593            return mStartingStartTrim;
594        }
595
596        public float getStartingEndTrim() {
597            return mStartingEndTrim;
598        }
599
600        @SuppressWarnings("unused")
601        public void setEndTrim(float endTrim) {
602            mEndTrim = endTrim;
603            invalidateSelf();
604        }
605
606        @SuppressWarnings("unused")
607        public float getEndTrim() {
608            return mEndTrim;
609        }
610
611        @SuppressWarnings("unused")
612        public void setRotation(float rotation) {
613            mRotation = rotation;
614            invalidateSelf();
615        }
616
617        @SuppressWarnings("unused")
618        public float getRotation() {
619            return mRotation;
620        }
621
622        public void setInsets(int width, int height) {
623            final float minEdge = (float) Math.min(width, height);
624            float insets;
625            if (mRingCenterRadius <= 0 || minEdge < 0) {
626                insets = (float) Math.ceil(mStrokeWidth / 2.0f);
627            } else {
628                insets = (float) (minEdge / 2.0f - mRingCenterRadius);
629            }
630            mStrokeInset = insets;
631        }
632
633        @SuppressWarnings("unused")
634        public float getInsets() {
635            return mStrokeInset;
636        }
637
638        /**
639         * @param centerRadius Inner radius in px of the circle the progress
640         *            spinner arc traces.
641         */
642        public void setCenterRadius(double centerRadius) {
643            mRingCenterRadius = centerRadius;
644        }
645
646        public double getCenterRadius() {
647            return mRingCenterRadius;
648        }
649
650        /**
651         * @param show Set to true to show the arrow head on the progress spinner.
652         */
653        public void setShowArrow(boolean show) {
654            if (mShowArrow != show) {
655                mShowArrow = show;
656                invalidateSelf();
657            }
658        }
659
660        /**
661         * @param scale Set the scale of the arrowhead for the spinner.
662         */
663        public void setArrowScale(float scale) {
664            if (scale != mArrowScale) {
665                mArrowScale = scale;
666                invalidateSelf();
667            }
668        }
669
670        /**
671         * @return The amount the progress spinner is currently rotated, between [0..1].
672         */
673        public float getStartingRotation() {
674            return mStartingRotation;
675        }
676
677        /**
678         * If the start / end trim are offset to begin with, store them so that
679         * animation starts from that offset.
680         */
681        public void storeOriginals() {
682            mStartingStartTrim = mStartTrim;
683            mStartingEndTrim = mEndTrim;
684            mStartingRotation = mRotation;
685        }
686
687        /**
688         * Reset the progress spinner to default rotation, start and end angles.
689         */
690        public void resetOriginals() {
691            mStartingStartTrim = 0;
692            mStartingEndTrim = 0;
693            mStartingRotation = 0;
694            setStartTrim(0);
695            setEndTrim(0);
696            setRotation(0);
697        }
698
699        private void invalidateSelf() {
700            mCallback.invalidateDrawable(null);
701        }
702    }
703
704    /**
705     * Squishes the interpolation curve into the second half of the animation.
706     */
707    private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator {
708        @Override
709        public float getInterpolation(float input) {
710            return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f));
711        }
712    }
713
714    /**
715     * Squishes the interpolation curve into the first half of the animation.
716     */
717    private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator {
718        @Override
719        public float getInterpolation(float input) {
720            return super.getInterpolation(Math.min(1, input * 2.0f));
721        }
722    }
723}
724