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