Home | History | Annotate | Download | only in widget
      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      */
    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. */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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      */
    389     public void setColorSchemeColors(@NonNull int... colors) {
    390         mRing.setColors(colors);
    391         mRing.setColorIndex(0);
    392         invalidateSelf();
    393     }
    394 
    395     @Override
    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
    405     public void setAlpha(int alpha) {
    406         mRing.setAlpha(alpha);
    407         invalidateSelf();
    408     }
    409 
    410     @Override
    411     public int getAlpha() {
    412         return mRing.getAlpha();
    413     }
    414 
    415     @Override
    416     public void setColorFilter(ColorFilter colorFilter) {
    417         mRing.setColorFilter(colorFilter);
    418         invalidateSelf();
    419     }
    420 
    421     private void setRotation(float rotation) {
    422         mRotation = rotation;
    423     }
    424 
    425     private float getRotation() {
    426         return mRotation;
    427     }
    428 
    429     @Override
    430     public int getOpacity() {
    431         return PixelFormat.TRANSLUCENT;
    432     }
    433 
    434     @Override
    435     public boolean isRunning() {
    436         return mAnimator.isRunning();
    437     }
    438 
    439     /**
    440      * Starts the animation for the spinner.
    441      */
    442     @Override
    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
    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
    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      */
    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      */
    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      */
    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 
    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 
    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          */
    663         void setArrowDimensions(float width, float height) {
    664             mArrowWidth = (int) width;
    665             mArrowHeight = (int) height;
    666         }
    667 
    668         void setStrokeCap(Paint.Cap strokeCap) {
    669             mPaint.setStrokeCap(strokeCap);
    670         }
    671 
    672         Paint.Cap getStrokeCap() {
    673             return mPaint.getStrokeCap();
    674         }
    675 
    676         float getArrowWidth() {
    677             return mArrowWidth;
    678         }
    679 
    680         float getArrowHeight() {
    681             return mArrowHeight;
    682         }
    683 
    684         /**
    685          * Draw the progress spinner
    686          */
    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 
    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          */
    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 
    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          */
    773         void setColor(int color) {
    774             mCurrentColor = color;
    775         }
    776 
    777         /**
    778          * Sets the background color of the circle inside the spinner.
    779          */
    780         void setBackgroundColor(int color) {
    781             mCirclePaint.setColor(color);
    782         }
    783 
    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          */
    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          */
    800         int getNextColor() {
    801             return mColors[getNextColorIndex()];
    802         }
    803 
    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          */
    812         void goToNextColor() {
    813             setColorIndex(getNextColorIndex());
    814         }
    815 
    816         void setColorFilter(ColorFilter filter) {
    817             mPaint.setColorFilter(filter);
    818         }
    819 
    820         /**
    821          * @param alpha alpha of the progress spinner and associated arrowhead.
    822          */
    823         void setAlpha(int alpha) {
    824             mAlpha = alpha;
    825         }
    826 
    827         /**
    828          * @return current alpha of the progress spinner and arrowhead
    829          */
    830         int getAlpha() {
    831             return mAlpha;
    832         }
    833 
    834         /**
    835          * @param strokeWidth set the stroke width of the progress spinner in pixels.
    836          */
    837         void setStrokeWidth(float strokeWidth) {
    838             mStrokeWidth = strokeWidth;
    839             mPaint.setStrokeWidth(strokeWidth);
    840         }
    841 
    842         float getStrokeWidth() {
    843             return mStrokeWidth;
    844         }
    845 
    846         void setStartTrim(float startTrim) {
    847             mStartTrim = startTrim;
    848         }
    849 
    850         float getStartTrim() {
    851             return mStartTrim;
    852         }
    853 
    854         float getStartingStartTrim() {
    855             return mStartingStartTrim;
    856         }
    857 
    858         float getStartingEndTrim() {
    859             return mStartingEndTrim;
    860         }
    861 
    862         int getStartingColor() {
    863             return mColors[mColorIndex];
    864         }
    865 
    866         void setEndTrim(float endTrim) {
    867             mEndTrim = endTrim;
    868         }
    869 
    870         float getEndTrim() {
    871             return mEndTrim;
    872         }
    873 
    874         void setRotation(float rotation) {
    875             mRotation = rotation;
    876         }
    877 
    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          */
    885         void setCenterRadius(float centerRadius) {
    886             mRingCenterRadius = centerRadius;
    887         }
    888 
    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          */
    896         void setShowArrow(boolean show) {
    897             if (mShowArrow != show) {
    898                 mShowArrow = show;
    899             }
    900         }
    901 
    902         boolean getShowArrow() {
    903             return mShowArrow;
    904         }
    905 
    906         /**
    907          * @param scale scale of the arrowhead for the spinner
    908          */
    909         void setArrowScale(float scale) {
    910             if (scale != mArrowScale) {
    911                 mArrowScale = scale;
    912             }
    913         }
    914 
    915         float getArrowScale() {
    916             return mArrowScale;
    917         }
    918 
    919         /**
    920          * @return The amount the progress spinner is currently rotated, between [0..1].
    921          */
    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          */
    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          */
    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