• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2018 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 
17 package androidx.swiperefreshlayout.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20 
21 import android.animation.Animator;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.ColorFilter;
28 import android.graphics.Paint;
29 import android.graphics.Paint.Style;
30 import android.graphics.Path;
31 import android.graphics.PixelFormat;
32 import android.graphics.Rect;
33 import android.graphics.RectF;
34 import android.graphics.drawable.Animatable;
35 import android.graphics.drawable.Drawable;
36 import android.util.DisplayMetrics;
37 import android.view.animation.Interpolator;
38 import android.view.animation.LinearInterpolator;
39 
40 import androidx.annotation.IntDef;
41 import androidx.annotation.NonNull;
42 import androidx.annotation.RestrictTo;
43 import androidx.core.util.Preconditions;
44 import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
45 
46 import java.lang.annotation.Retention;
47 import java.lang.annotation.RetentionPolicy;
48 
49 /**
50  * Drawable that renders the animated indeterminate progress indicator in the Material design style
51  * without depending on API level 11.
52  *
53  * <p>While this may be used to draw an indeterminate spinner using {@link #start()} and {@link
54  * #stop()} methods, this may also be used to draw a progress arc using {@link
55  * #setStartEndTrim(float, float)} method. CircularProgressDrawable also supports adding an arrow
56  * at the end of the arc by {@link #setArrowEnabled(boolean)} and {@link #setArrowDimensions(float,
57  * float)} methods.
58  *
59  * <p>To use one of the pre-defined sizes instead of using your own, {@link #setStyle(int)} should
60  * be called with one of the {@link #DEFAULT} or {@link #LARGE} styles as its parameter. Doing it
61  * so will update the arrow dimensions, ring size and stroke width to fit the one specified.
62  *
63  * <p>If no center radius is set via {@link #setCenterRadius(float)} or {@link #setStyle(int)}
64  * methods, CircularProgressDrawable will fill the bounds set via {@link #setBounds(Rect)}.
65  */
66 public class CircularProgressDrawable extends Drawable implements Animatable {
67     private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
68     private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator();
69 
70     /** @hide */
71     @RestrictTo(LIBRARY_GROUP)
72     @Retention(RetentionPolicy.SOURCE)
73     @IntDef({LARGE, DEFAULT})
74     public @interface ProgressDrawableSize {
75     }
76 
77     /** Maps to ProgressBar.Large style. */
78     public static final int LARGE = 0;
79 
80     private static final float CENTER_RADIUS_LARGE = 11f;
81     private static final float STROKE_WIDTH_LARGE = 3f;
82     private static final int ARROW_WIDTH_LARGE = 12;
83     private static final int ARROW_HEIGHT_LARGE = 6;
84 
85     /** Maps to ProgressBar default style. */
86     public static final int DEFAULT = 1;
87 
88     private static final float CENTER_RADIUS = 7.5f;
89     private static final float STROKE_WIDTH = 2.5f;
90     private static final int ARROW_WIDTH = 10;
91     private static final int ARROW_HEIGHT = 5;
92 
93     /**
94      * This is the default set of colors that's used in spinner. {@link
95      * #setColorSchemeColors(int...)} allows modifying colors.
96      */
97     private static final int[] COLORS = new int[]{
98             Color.BLACK
99     };
100 
101     /**
102      * The value in the linear interpolator for animating the drawable at which
103      * the color transition should start
104      */
105     private static final float COLOR_CHANGE_OFFSET = 0.75f;
106     private static final float SHRINK_OFFSET = 0.5f;
107 
108     /** The duration of a single progress spin in milliseconds. */
109     private static final int ANIMATION_DURATION = 1332;
110 
111     /** Full rotation that's done for the animation duration in degrees. */
112     private static final float GROUP_FULL_ROTATION = 1080f / 5f;
113 
114     /** The indicator ring, used to manage animation state. */
115     private final Ring mRing;
116 
117     /** Canvas rotation in degrees. */
118     private float mRotation;
119 
120     /** Maximum length of the progress arc during the animation. */
121     private static final float MAX_PROGRESS_ARC = .8f;
122     /** Minimum length of the progress arc during the animation. */
123     private static final float MIN_PROGRESS_ARC = .01f;
124 
125     /** Rotation applied to ring during the animation, to complete it to a full circle. */
126     private static final float RING_ROTATION = 1f - (MAX_PROGRESS_ARC - MIN_PROGRESS_ARC);
127 
128     private Resources mResources;
129     private Animator mAnimator;
130     private float mRotationCount;
131     private boolean mFinishing;
132 
133     /**
134      * @param context application context
135      */
CircularProgressDrawable(@onNull Context context)136     public CircularProgressDrawable(@NonNull Context context) {
137         mResources = Preconditions.checkNotNull(context).getResources();
138 
139         mRing = new Ring();
140         mRing.setColors(COLORS);
141 
142         setStrokeWidth(STROKE_WIDTH);
143         setupAnimators();
144     }
145 
146     /** Sets all parameters at once in dp. */
setSizeParameters(float centerRadius, float strokeWidth, float arrowWidth, float arrowHeight)147     private void setSizeParameters(float centerRadius, float strokeWidth, float arrowWidth,
148             float arrowHeight) {
149         final Ring ring = mRing;
150         final DisplayMetrics metrics = mResources.getDisplayMetrics();
151         final float screenDensity = metrics.density;
152 
153         ring.setStrokeWidth(strokeWidth * screenDensity);
154         ring.setCenterRadius(centerRadius * screenDensity);
155         ring.setColorIndex(0);
156         ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity);
157     }
158 
159     /**
160      * Sets the overall size for the progress spinner. This updates the radius
161      * and stroke width of the ring, and arrow dimensions.
162      *
163      * @param size one of {@link #LARGE} or {@link #DEFAULT}
164      */
setStyle(@rogressDrawableSize int size)165     public void setStyle(@ProgressDrawableSize int size) {
166         if (size == LARGE) {
167             setSizeParameters(CENTER_RADIUS_LARGE, STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE,
168                     ARROW_HEIGHT_LARGE);
169         } else {
170             setSizeParameters(CENTER_RADIUS, STROKE_WIDTH, ARROW_WIDTH, ARROW_HEIGHT);
171         }
172         invalidateSelf();
173     }
174 
175     /**
176      * Returns the stroke width for the progress spinner in pixels.
177      *
178      * @return stroke width in pixels
179      */
getStrokeWidth()180     public float getStrokeWidth() {
181         return mRing.getStrokeWidth();
182     }
183 
184     /**
185      * Sets the stroke width for the progress spinner in pixels.
186      *
187      * @param strokeWidth stroke width in pixels
188      */
setStrokeWidth(float strokeWidth)189     public void setStrokeWidth(float strokeWidth) {
190         mRing.setStrokeWidth(strokeWidth);
191         invalidateSelf();
192     }
193 
194     /**
195      * Returns the center radius for the progress spinner in pixels.
196      *
197      * @return center radius in pixels
198      */
getCenterRadius()199     public float getCenterRadius() {
200         return mRing.getCenterRadius();
201     }
202 
203     /**
204      * Sets the center radius for the progress spinner in pixels. If set to 0, this drawable will
205      * fill the bounds when drawn.
206      *
207      * @param centerRadius center radius in pixels
208      */
setCenterRadius(float centerRadius)209     public void setCenterRadius(float centerRadius) {
210         mRing.setCenterRadius(centerRadius);
211         invalidateSelf();
212     }
213 
214     /**
215      * Sets the stroke cap of the progress spinner. Default stroke cap is {@link Paint.Cap#SQUARE}.
216      *
217      * @param strokeCap stroke cap
218      */
setStrokeCap(@onNull Paint.Cap strokeCap)219     public void setStrokeCap(@NonNull Paint.Cap strokeCap) {
220         mRing.setStrokeCap(strokeCap);
221         invalidateSelf();
222     }
223 
224     /**
225      * Returns the stroke cap of the progress spinner.
226      *
227      * @return stroke cap
228      */
229     @NonNull
getStrokeCap()230     public Paint.Cap getStrokeCap() {
231         return mRing.getStrokeCap();
232     }
233 
234     /**
235      * Returns the arrow width in pixels.
236      *
237      * @return arrow width in pixels
238      */
getArrowWidth()239     public float getArrowWidth() {
240         return mRing.getArrowWidth();
241     }
242 
243     /**
244      * Returns the arrow height in pixels.
245      *
246      * @return arrow height in pixels
247      */
getArrowHeight()248     public float getArrowHeight() {
249         return mRing.getArrowHeight();
250     }
251 
252     /**
253      * Sets the dimensions of the arrow at the end of the spinner in pixels.
254      *
255      * @param width width of the baseline of the arrow in pixels
256      * @param height distance from tip of the arrow to its baseline in pixels
257      */
setArrowDimensions(float width, float height)258     public void setArrowDimensions(float width, float height) {
259         mRing.setArrowDimensions(width, height);
260         invalidateSelf();
261     }
262 
263     /**
264      * Returns {@code true} if the arrow at the end of the spinner is shown.
265      *
266      * @return {@code true} if the arrow is shown, {@code false} otherwise.
267      */
getArrowEnabled()268     public boolean getArrowEnabled() {
269         return mRing.getShowArrow();
270     }
271 
272     /**
273      * Sets if the arrow at the end of the spinner should be shown.
274      *
275      * @param show {@code true} if the arrow should be drawn, {@code false} otherwise
276      */
setArrowEnabled(boolean show)277     public void setArrowEnabled(boolean show) {
278         mRing.setShowArrow(show);
279         invalidateSelf();
280     }
281 
282     /**
283      * Returns the scale of the arrow at the end of the spinner.
284      *
285      * @return scale of the arrow
286      */
getArrowScale()287     public float getArrowScale() {
288         return mRing.getArrowScale();
289     }
290 
291     /**
292      * Sets the scale of the arrow at the end of the spinner.
293      *
294      * @param scale scaling that will be applied to the arrow's both width and height when drawing.
295      */
setArrowScale(float scale)296     public void setArrowScale(float scale) {
297         mRing.setArrowScale(scale);
298         invalidateSelf();
299     }
300 
301     /**
302      * Returns the start trim for the progress spinner arc
303      *
304      * @return start trim from [0..1]
305      */
getStartTrim()306     public float getStartTrim() {
307         return mRing.getStartTrim();
308     }
309 
310     /**
311      * Returns the end trim for the progress spinner arc
312      *
313      * @return end trim from [0..1]
314      */
getEndTrim()315     public float getEndTrim() {
316         return mRing.getEndTrim();
317     }
318 
319     /**
320      * Sets the start and end trim for the progress spinner arc. 0 corresponds to the geometric
321      * angle of 0 degrees (3 o'clock on a watch) and it increases clockwise, coming to a full circle
322      * at 1.
323      *
324      * @param start starting position of the arc from [0..1]
325      * @param end ending position of the arc from [0..1]
326      */
setStartEndTrim(float start, float end)327     public void setStartEndTrim(float start, float end) {
328         mRing.setStartTrim(start);
329         mRing.setEndTrim(end);
330         invalidateSelf();
331     }
332 
333     /**
334      * Returns the amount of rotation applied to the progress spinner.
335      *
336      * @return amount of rotation from [0..1]
337      */
getProgressRotation()338     public float getProgressRotation() {
339         return mRing.getRotation();
340     }
341 
342     /**
343      * Sets the amount of rotation to apply to the progress spinner.
344      *
345      * @param rotation rotation from [0..1]
346      */
setProgressRotation(float rotation)347     public void setProgressRotation(float rotation) {
348         mRing.setRotation(rotation);
349         invalidateSelf();
350     }
351 
352     /**
353      * Returns the background color of the circle drawn inside the drawable.
354      *
355      * @return an ARGB color
356      */
getBackgroundColor()357     public int getBackgroundColor() {
358         return mRing.getBackgroundColor();
359     }
360 
361     /**
362      * Sets the background color of the circle inside the drawable. Calling {@link
363      * #setAlpha(int)} does not affect the visibility background color, so it should be set
364      * separately if it needs to be hidden or visible.
365      *
366      * @param color an ARGB color
367      */
setBackgroundColor(int color)368     public void setBackgroundColor(int color) {
369         mRing.setBackgroundColor(color);
370         invalidateSelf();
371     }
372 
373     /**
374      * Returns the colors used in the progress animation
375      *
376      * @return list of ARGB colors
377      */
378     @NonNull
getColorSchemeColors()379     public int[] getColorSchemeColors() {
380         return mRing.getColors();
381     }
382 
383     /**
384      * Sets the colors used in the progress animation from a color list. The first color will also
385      * be the color to be used if animation is not started yet.
386      *
387      * @param colors list of ARGB colors to be used in the spinner
388      */
setColorSchemeColors(@onNull int... colors)389     public void setColorSchemeColors(@NonNull int... colors) {
390         mRing.setColors(colors);
391         mRing.setColorIndex(0);
392         invalidateSelf();
393     }
394 
395     @Override
draw(Canvas canvas)396     public void draw(Canvas canvas) {
397         final Rect bounds = getBounds();
398         canvas.save();
399         canvas.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
400         mRing.draw(canvas, bounds);
401         canvas.restore();
402     }
403 
404     @Override
setAlpha(int alpha)405     public void setAlpha(int alpha) {
406         mRing.setAlpha(alpha);
407         invalidateSelf();
408     }
409 
410     @Override
getAlpha()411     public int getAlpha() {
412         return mRing.getAlpha();
413     }
414 
415     @Override
setColorFilter(ColorFilter colorFilter)416     public void setColorFilter(ColorFilter colorFilter) {
417         mRing.setColorFilter(colorFilter);
418         invalidateSelf();
419     }
420 
setRotation(float rotation)421     private void setRotation(float rotation) {
422         mRotation = rotation;
423     }
424 
getRotation()425     private float getRotation() {
426         return mRotation;
427     }
428 
429     @Override
getOpacity()430     public int getOpacity() {
431         return PixelFormat.TRANSLUCENT;
432     }
433 
434     @Override
isRunning()435     public boolean isRunning() {
436         return mAnimator.isRunning();
437     }
438 
439     /**
440      * Starts the animation for the spinner.
441      */
442     @Override
start()443     public void start() {
444         mAnimator.cancel();
445         mRing.storeOriginals();
446         // Already showing some part of the ring
447         if (mRing.getEndTrim() != mRing.getStartTrim()) {
448             mFinishing = true;
449             mAnimator.setDuration(ANIMATION_DURATION / 2);
450             mAnimator.start();
451         } else {
452             mRing.setColorIndex(0);
453             mRing.resetOriginals();
454             mAnimator.setDuration(ANIMATION_DURATION);
455             mAnimator.start();
456         }
457     }
458 
459     /**
460      * Stops the animation for the spinner.
461      */
462     @Override
stop()463     public void stop() {
464         mAnimator.cancel();
465         setRotation(0);
466         mRing.setShowArrow(false);
467         mRing.setColorIndex(0);
468         mRing.resetOriginals();
469         invalidateSelf();
470     }
471 
472     // Adapted from ArgbEvaluator.java
evaluateColorChange(float fraction, int startValue, int endValue)473     private int evaluateColorChange(float fraction, int startValue, int endValue) {
474         int startA = (startValue >> 24) & 0xff;
475         int startR = (startValue >> 16) & 0xff;
476         int startG = (startValue >> 8) & 0xff;
477         int startB = startValue & 0xff;
478 
479         int endA = (endValue >> 24) & 0xff;
480         int endR = (endValue >> 16) & 0xff;
481         int endG = (endValue >> 8) & 0xff;
482         int endB = endValue & 0xff;
483 
484         return (startA + (int) (fraction * (endA - startA))) << 24
485                 | (startR + (int) (fraction * (endR - startR))) << 16
486                 | (startG + (int) (fraction * (endG - startG))) << 8
487                 | (startB + (int) (fraction * (endB - startB)));
488     }
489 
490     /**
491      * Update the ring color if this is within the last 25% of the animation.
492      * The new ring color will be a translation from the starting ring color to
493      * the next color.
494      */
updateRingColor(float interpolatedTime, Ring ring)495     private void updateRingColor(float interpolatedTime, Ring ring) {
496         if (interpolatedTime > COLOR_CHANGE_OFFSET) {
497             ring.setColor(evaluateColorChange((interpolatedTime - COLOR_CHANGE_OFFSET)
498                             / (1f - COLOR_CHANGE_OFFSET), ring.getStartingColor(),
499                     ring.getNextColor()));
500         } else {
501             ring.setColor(ring.getStartingColor());
502         }
503     }
504 
505     /**
506      * Update the ring start and end trim if the animation is finishing (i.e. it started with
507      * already visible progress, so needs to shrink back down before starting the spinner).
508      */
applyFinishTranslation(float interpolatedTime, Ring ring)509     private void applyFinishTranslation(float interpolatedTime, Ring ring) {
510         // shrink back down and complete a full rotation before
511         // starting other circles
512         // Rotation goes between [0..1].
513         updateRingColor(interpolatedTime, ring);
514         float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC)
515                 + 1f);
516         final float startTrim = ring.getStartingStartTrim()
517                 + (ring.getStartingEndTrim() - MIN_PROGRESS_ARC - ring.getStartingStartTrim())
518                 * interpolatedTime;
519         ring.setStartTrim(startTrim);
520         ring.setEndTrim(ring.getStartingEndTrim());
521         final float rotation = ring.getStartingRotation()
522                 + ((targetRotation - ring.getStartingRotation()) * interpolatedTime);
523         ring.setRotation(rotation);
524     }
525 
526     /**
527      * Update the ring start and end trim according to current time of the animation.
528      */
applyTransformation(float interpolatedTime, Ring ring, boolean lastFrame)529     private void applyTransformation(float interpolatedTime, Ring ring, boolean lastFrame) {
530         if (mFinishing) {
531             applyFinishTranslation(interpolatedTime, ring);
532             // Below condition is to work around a ValueAnimator issue where onAnimationRepeat is
533             // called before last frame (1f).
534         } else if (interpolatedTime != 1f || lastFrame) {
535             final float startingRotation = ring.getStartingRotation();
536             float startTrim, endTrim;
537 
538             if (interpolatedTime < SHRINK_OFFSET) { // Expansion occurs on first half of animation
539                 final float scaledTime = interpolatedTime / SHRINK_OFFSET;
540                 startTrim = ring.getStartingStartTrim();
541                 endTrim = startTrim + ((MAX_PROGRESS_ARC - MIN_PROGRESS_ARC)
542                         * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime) + MIN_PROGRESS_ARC);
543             } else { // Shrinking occurs on second half of animation
544                 float scaledTime = (interpolatedTime - SHRINK_OFFSET) / (1f - SHRINK_OFFSET);
545                 endTrim = ring.getStartingStartTrim() + (MAX_PROGRESS_ARC - MIN_PROGRESS_ARC);
546                 startTrim = endTrim - ((MAX_PROGRESS_ARC - MIN_PROGRESS_ARC)
547                         * (1f - MATERIAL_INTERPOLATOR.getInterpolation(scaledTime))
548                         + MIN_PROGRESS_ARC);
549             }
550 
551             final float rotation = startingRotation + (RING_ROTATION * interpolatedTime);
552             float groupRotation = GROUP_FULL_ROTATION * (interpolatedTime + mRotationCount);
553 
554             ring.setStartTrim(startTrim);
555             ring.setEndTrim(endTrim);
556             ring.setRotation(rotation);
557             setRotation(groupRotation);
558         }
559     }
560 
setupAnimators()561     private void setupAnimators() {
562         final Ring ring = mRing;
563         final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
564         animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
565             @Override
566             public void onAnimationUpdate(ValueAnimator animation) {
567                 float interpolatedTime = (float) animation.getAnimatedValue();
568                 updateRingColor(interpolatedTime, ring);
569                 applyTransformation(interpolatedTime, ring, false);
570                 invalidateSelf();
571             }
572         });
573         animator.setRepeatCount(ValueAnimator.INFINITE);
574         animator.setRepeatMode(ValueAnimator.RESTART);
575         animator.setInterpolator(LINEAR_INTERPOLATOR);
576         animator.addListener(new Animator.AnimatorListener() {
577 
578             @Override
579             public void onAnimationStart(Animator animator) {
580                 mRotationCount = 0;
581             }
582 
583             @Override
584             public void onAnimationEnd(Animator animator) {
585                 // do nothing
586             }
587 
588             @Override
589             public void onAnimationCancel(Animator animation) {
590                 // do nothing
591             }
592 
593             @Override
594             public void onAnimationRepeat(Animator animator) {
595                 applyTransformation(1f, ring, true);
596                 ring.storeOriginals();
597                 ring.goToNextColor();
598                 if (mFinishing) {
599                     // finished closing the last ring from the swipe gesture; go
600                     // into progress mode
601                     mFinishing = false;
602                     animator.cancel();
603                     animator.setDuration(ANIMATION_DURATION);
604                     animator.start();
605                     ring.setShowArrow(false);
606                 } else {
607                     mRotationCount = mRotationCount + 1;
608                 }
609             }
610         });
611         mAnimator = animator;
612     }
613 
614     /**
615      * A private class to do all the drawing of CircularProgressDrawable, which includes background,
616      * progress spinner and the arrow. This class is to separate drawing from animation.
617      */
618     private static class Ring {
619         final RectF mTempBounds = new RectF();
620         final Paint mPaint = new Paint();
621         final Paint mArrowPaint = new Paint();
622         final Paint mCirclePaint = new Paint();
623 
624         float mStartTrim = 0f;
625         float mEndTrim = 0f;
626         float mRotation = 0f;
627         float mStrokeWidth = 5f;
628 
629         int[] mColors;
630         // mColorIndex represents the offset into the available mColors that the
631         // progress circle should currently display. As the progress circle is
632         // animating, the mColorIndex moves by one to the next available color.
633         int mColorIndex;
634         float mStartingStartTrim;
635         float mStartingEndTrim;
636         float mStartingRotation;
637         boolean mShowArrow;
638         Path mArrow;
639         float mArrowScale = 1;
640         float mRingCenterRadius;
641         int mArrowWidth;
642         int mArrowHeight;
643         int mAlpha = 255;
644         int mCurrentColor;
645 
Ring()646         Ring() {
647             mPaint.setStrokeCap(Paint.Cap.SQUARE);
648             mPaint.setAntiAlias(true);
649             mPaint.setStyle(Style.STROKE);
650 
651             mArrowPaint.setStyle(Paint.Style.FILL);
652             mArrowPaint.setAntiAlias(true);
653 
654             mCirclePaint.setColor(Color.TRANSPARENT);
655         }
656 
657         /**
658          * Sets the dimensions of the arrowhead.
659          *
660          * @param width width of the hypotenuse of the arrow head
661          * @param height height of the arrow point
662          */
setArrowDimensions(float width, float height)663         void setArrowDimensions(float width, float height) {
664             mArrowWidth = (int) width;
665             mArrowHeight = (int) height;
666         }
667 
setStrokeCap(Paint.Cap strokeCap)668         void setStrokeCap(Paint.Cap strokeCap) {
669             mPaint.setStrokeCap(strokeCap);
670         }
671 
getStrokeCap()672         Paint.Cap getStrokeCap() {
673             return mPaint.getStrokeCap();
674         }
675 
getArrowWidth()676         float getArrowWidth() {
677             return mArrowWidth;
678         }
679 
getArrowHeight()680         float getArrowHeight() {
681             return mArrowHeight;
682         }
683 
684         /**
685          * Draw the progress spinner
686          */
draw(Canvas c, Rect bounds)687         void draw(Canvas c, Rect bounds) {
688             final RectF arcBounds = mTempBounds;
689             float arcRadius = mRingCenterRadius + mStrokeWidth / 2f;
690             if (mRingCenterRadius <= 0) {
691                 // If center radius is not set, fill the bounds
692                 arcRadius = Math.min(bounds.width(), bounds.height()) / 2f - Math.max(
693                         (mArrowWidth * mArrowScale) / 2f, mStrokeWidth / 2f);
694             }
695             arcBounds.set(bounds.centerX() - arcRadius,
696                     bounds.centerY() - arcRadius,
697                     bounds.centerX() + arcRadius,
698                     bounds.centerY() + arcRadius);
699 
700             final float startAngle = (mStartTrim + mRotation) * 360;
701             final float endAngle = (mEndTrim + mRotation) * 360;
702             float sweepAngle = endAngle - startAngle;
703 
704             mPaint.setColor(mCurrentColor);
705             mPaint.setAlpha(mAlpha);
706 
707             // Draw the background first
708             float inset = mStrokeWidth / 2f; // Calculate inset to draw inside the arc
709             arcBounds.inset(inset, inset); // Apply inset
710             c.drawCircle(arcBounds.centerX(), arcBounds.centerY(), arcBounds.width() / 2f,
711                     mCirclePaint);
712             arcBounds.inset(-inset, -inset); // Revert the inset
713 
714             c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
715 
716             drawTriangle(c, startAngle, sweepAngle, arcBounds);
717         }
718 
drawTriangle(Canvas c, float startAngle, float sweepAngle, RectF bounds)719         void drawTriangle(Canvas c, float startAngle, float sweepAngle, RectF bounds) {
720             if (mShowArrow) {
721                 if (mArrow == null) {
722                     mArrow = new android.graphics.Path();
723                     mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD);
724                 } else {
725                     mArrow.reset();
726                 }
727                 float centerRadius = Math.min(bounds.width(), bounds.height()) / 2f;
728                 float inset = mArrowWidth * mArrowScale / 2f;
729                 // Update the path each time. This works around an issue in SKIA
730                 // where concatenating a rotation matrix to a scale matrix
731                 // ignored a starting negative rotation. This appears to have
732                 // been fixed as of API 21.
733                 mArrow.moveTo(0, 0);
734                 mArrow.lineTo(mArrowWidth * mArrowScale, 0);
735                 mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight
736                         * mArrowScale));
737                 mArrow.offset(centerRadius + bounds.centerX() - inset,
738                         bounds.centerY() + mStrokeWidth / 2f);
739                 mArrow.close();
740                 // draw a triangle
741                 mArrowPaint.setColor(mCurrentColor);
742                 mArrowPaint.setAlpha(mAlpha);
743                 c.save();
744                 c.rotate(startAngle + sweepAngle, bounds.centerX(),
745                         bounds.centerY());
746                 c.drawPath(mArrow, mArrowPaint);
747                 c.restore();
748             }
749         }
750 
751         /**
752          * Sets the colors the progress spinner alternates between.
753          *
754          * @param colors array of ARGB colors. Must be non-{@code null}.
755          */
setColors(@onNull int[] colors)756         void setColors(@NonNull int[] colors) {
757             mColors = colors;
758             // if colors are reset, make sure to reset the color index as well
759             setColorIndex(0);
760         }
761 
getColors()762         int[] getColors() {
763             return mColors;
764         }
765 
766         /**
767          * Sets the absolute color of the progress spinner. This is should only
768          * be used when animating between current and next color when the
769          * spinner is rotating.
770          *
771          * @param color an ARGB color
772          */
setColor(int color)773         void setColor(int color) {
774             mCurrentColor = color;
775         }
776 
777         /**
778          * Sets the background color of the circle inside the spinner.
779          */
setBackgroundColor(int color)780         void setBackgroundColor(int color) {
781             mCirclePaint.setColor(color);
782         }
783 
getBackgroundColor()784         int getBackgroundColor() {
785             return mCirclePaint.getColor();
786         }
787 
788         /**
789          * @param index index into the color array of the color to display in
790          *              the progress spinner.
791          */
setColorIndex(int index)792         void setColorIndex(int index) {
793             mColorIndex = index;
794             mCurrentColor = mColors[mColorIndex];
795         }
796 
797         /**
798          * @return int describing the next color the progress spinner should use when drawing.
799          */
getNextColor()800         int getNextColor() {
801             return mColors[getNextColorIndex()];
802         }
803 
getNextColorIndex()804         int getNextColorIndex() {
805             return (mColorIndex + 1) % (mColors.length);
806         }
807 
808         /**
809          * Proceed to the next available ring color. This will automatically
810          * wrap back to the beginning of colors.
811          */
goToNextColor()812         void goToNextColor() {
813             setColorIndex(getNextColorIndex());
814         }
815 
setColorFilter(ColorFilter filter)816         void setColorFilter(ColorFilter filter) {
817             mPaint.setColorFilter(filter);
818         }
819 
820         /**
821          * @param alpha alpha of the progress spinner and associated arrowhead.
822          */
setAlpha(int alpha)823         void setAlpha(int alpha) {
824             mAlpha = alpha;
825         }
826 
827         /**
828          * @return current alpha of the progress spinner and arrowhead
829          */
getAlpha()830         int getAlpha() {
831             return mAlpha;
832         }
833 
834         /**
835          * @param strokeWidth set the stroke width of the progress spinner in pixels.
836          */
setStrokeWidth(float strokeWidth)837         void setStrokeWidth(float strokeWidth) {
838             mStrokeWidth = strokeWidth;
839             mPaint.setStrokeWidth(strokeWidth);
840         }
841 
getStrokeWidth()842         float getStrokeWidth() {
843             return mStrokeWidth;
844         }
845 
setStartTrim(float startTrim)846         void setStartTrim(float startTrim) {
847             mStartTrim = startTrim;
848         }
849 
getStartTrim()850         float getStartTrim() {
851             return mStartTrim;
852         }
853 
getStartingStartTrim()854         float getStartingStartTrim() {
855             return mStartingStartTrim;
856         }
857 
getStartingEndTrim()858         float getStartingEndTrim() {
859             return mStartingEndTrim;
860         }
861 
getStartingColor()862         int getStartingColor() {
863             return mColors[mColorIndex];
864         }
865 
setEndTrim(float endTrim)866         void setEndTrim(float endTrim) {
867             mEndTrim = endTrim;
868         }
869 
getEndTrim()870         float getEndTrim() {
871             return mEndTrim;
872         }
873 
setRotation(float rotation)874         void setRotation(float rotation) {
875             mRotation = rotation;
876         }
877 
getRotation()878         float getRotation() {
879             return mRotation;
880         }
881 
882         /**
883          * @param centerRadius inner radius in px of the circle the progress spinner arc traces
884          */
setCenterRadius(float centerRadius)885         void setCenterRadius(float centerRadius) {
886             mRingCenterRadius = centerRadius;
887         }
888 
getCenterRadius()889         float getCenterRadius() {
890             return mRingCenterRadius;
891         }
892 
893         /**
894          * @param show {@code true} if should show the arrow head on the progress spinner
895          */
setShowArrow(boolean show)896         void setShowArrow(boolean show) {
897             if (mShowArrow != show) {
898                 mShowArrow = show;
899             }
900         }
901 
getShowArrow()902         boolean getShowArrow() {
903             return mShowArrow;
904         }
905 
906         /**
907          * @param scale scale of the arrowhead for the spinner
908          */
setArrowScale(float scale)909         void setArrowScale(float scale) {
910             if (scale != mArrowScale) {
911                 mArrowScale = scale;
912             }
913         }
914 
getArrowScale()915         float getArrowScale() {
916             return mArrowScale;
917         }
918 
919         /**
920          * @return The amount the progress spinner is currently rotated, between [0..1].
921          */
getStartingRotation()922         float getStartingRotation() {
923             return mStartingRotation;
924         }
925 
926         /**
927          * If the start / end trim are offset to begin with, store them so that animation starts
928          * from that offset.
929          */
storeOriginals()930         void storeOriginals() {
931             mStartingStartTrim = mStartTrim;
932             mStartingEndTrim = mEndTrim;
933             mStartingRotation = mRotation;
934         }
935 
936         /**
937          * Reset the progress spinner to default rotation, start and end angles.
938          */
resetOriginals()939         void resetOriginals() {
940             mStartingStartTrim = 0;
941             mStartingEndTrim = 0;
942             mStartingRotation = 0;
943             setStartTrim(0);
944             setEndTrim(0);
945             setRotation(0);
946         }
947     }
948 }
949