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