/* * MIT License * * Copyright (c) 2017 Yuriy Budiyev [[email protected]] * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.budiyev.android.circularprogressbar; import android.animation.Animator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.os.Build; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.view.animation.LinearInterpolator; import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.FloatRange; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StyleRes; /** * Circular progress bar */ public final class CircularProgressBar extends View { private static final float DEFAULT_SIZE_DP = 48f; private static final float DEFAULT_MAXIMUM = 100f; private static final float DEFAULT_PROGRESS = 0f; private static final float DEFAULT_FOREGROUND_STROKE_WIDTH_DP = 3f; private static final float DEFAULT_BACKGROUND_STROKE_WIDTH_DP = 1f; private static final float DEFAULT_START_ANGLE = 270f; private static final float DEFAULT_INDETERMINATE_MINIMUM_ANGLE = 60f; private static final int DEFAULT_FOREGROUND_STROKE_CAP = 0; private static final int DEFAULT_FOREGROUND_STROKE_COLOR = Color.BLUE; private static final int DEFAULT_BACKGROUND_STROKE_COLOR = Color.BLACK; private static final int DEFAULT_PROGRESS_ANIMATION_DURATION = 100; private static final int DEFAULT_INDETERMINATE_ROTATION_ANIMATION_DURATION = 1200; private static final int DEFAULT_INDETERMINATE_SWEEP_ANIMATION_DURATION = 600; private static final boolean DEFAULT_ANIMATE_PROGRESS = true; private static final boolean DEFAULT_DRAW_BACKGROUND_STROKE = false; private static final boolean DEFAULT_INDETERMINATE = false; private final Runnable mSweepRestartAction = new SweepRestartAction(); private final RectF mDrawRect = new RectF(); private final ValueAnimator mProgressAnimator = new ValueAnimator(); private final ValueAnimator mIndeterminateRotationAnimator = new ValueAnimator(); private final ValueAnimator mIndeterminateSweepAnimator = new ValueAnimator(); private final Paint mForegroundStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint mBackgroundStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private int mDefaultSize = 0; private float mMaximum = 0f; private float mProgress = 0f; private float mStartAngle = 0f; private float mIndeterminateStartAngle = 0f; private float mIndeterminateSweepAngle = 0f; private float mIndeterminateOffsetAngle = 0f; private float mIndeterminateMinimumAngle = 0f; private float mForegroundStrokeCapAngle = 0f; private boolean mIndeterminate = false; private boolean mAnimateProgress = false; private boolean mDrawBackgroundStroke = false; private boolean mIndeterminateGrowMode = false; private boolean mVisible = false; public CircularProgressBar(@NonNull final Context context) { super(context); initialize(context, null, 0, 0); } public CircularProgressBar(@NonNull final Context context, @Nullable final AttributeSet attrs) { super(context, attrs); initialize(context, attrs, 0, 0); } public CircularProgressBar(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(context, attrs, defStyleAttr, 0); } @RequiresApi(Build.VERSION_CODES.LOLLIPOP) public CircularProgressBar(@NonNull final Context context, @Nullable final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initialize(context, attrs, defStyleAttr, defStyleRes); } /** * Indeterminate mode */ public boolean isIndeterminate() { return mIndeterminate; } /** * Indeterminate mode, disabled by default */ public void setIndeterminate(final boolean indeterminate) { cancelIndeterminateAnimations(); mIndeterminate = indeterminate; invalidate(); if (mVisible && indeterminate) { endProgressAnimation(); startIndeterminateAnimations(); } } /** * Get current progress value for non-indeterminate mode */ public float getProgress() { return mProgress; } /** * Set current progress value for non-indeterminate mode */ public void setProgress(final float progress) { if (mIndeterminate) { mProgress = progress; } else { cancelProgressAnimation(); if (mVisible && mAnimateProgress) { setProgressAnimated(progress); } else { setProgressInternal(progress); } } } /** * Maximum progress for non-indeterminate mode */ public float getMaximum() { return mMaximum; } /** * Maximum progress for non-indeterminate mode */ public void setMaximum(final float maximum) { mMaximum = maximum; invalidate(); } /** * Start angle for non-indeterminate mode, between -360 and 360 degrees */ @FloatRange(from = -360f, to = 360f) public float getStartAngle() { return mStartAngle; } /** * Start angle for non-indeterminate mode, between -360 and 360 degrees */ public void setStartAngle(@FloatRange(from = -360f, to = 360f) final float angle) { if (angle < -360f || angle > 360f) { throw new IllegalArgumentException("Start angle value should be between -360 and 360 degrees (inclusive)"); } mStartAngle = angle; invalidate(); } /** * Whether to animate progress for non-indeterminate mode */ public boolean isAnimateProgress() { return mAnimateProgress; } /** * Whether to animate progress for non-indeterminate mode */ public void setAnimateProgress(final boolean animate) { mAnimateProgress = animate; } /** * Progress animation duration for non-indeterminate mode (in milliseconds) */ @IntRange(from = 0) public long getProgressAnimationDuration() { return mProgressAnimator.getDuration(); } /** * Progress animation duration for non-indeterminate mode (in milliseconds) */ public void setProgressAnimationDuration(@IntRange(from = 0) final long duration) { if (duration < 0) { throw new IllegalArgumentException("Animation duration can't be negative"); } if (mVisible) { endProgressAnimation(); } mProgressAnimator.setDuration(duration); } /** * Progress animation interpolator for non-indeterminate mode */ @NonNull public TimeInterpolator getProgressAnimationInterpolator() { return mProgressAnimator.getInterpolator(); } /** * Progress animation interpolator for non-indeterminate mode */ public void setProgressAnimationInterpolator(@NonNull final TimeInterpolator interpolator) { //noinspection ConstantConditions if (interpolator == null) { throw new IllegalArgumentException("Interpolator can't be null"); } if (mVisible) { endProgressAnimation(); } mProgressAnimator.setInterpolator(interpolator); } /** * Minimum angle for indeterminate mode, between 0 and 180 degrees */ @FloatRange(from = 0f, to = 180f) public float getIndeterminateMinimumAngle() { return mIndeterminateMinimumAngle; } /** * Minimum angle for indeterminate mode, between 0 and 180 degrees */ public void setIndeterminateMinimumAngle(@FloatRange(from = 0f, to = 180f) final float angle) { if (angle < 0f || angle > 180f) { throw new IllegalArgumentException( "Indeterminate minimum angle value should be between 0 and 180 degrees (inclusive)"); } cancelIndeterminateAnimations(); mIndeterminateMinimumAngle = angle; mIndeterminateSweepAnimator.setFloatValues(360f - angle * 2f); invalidate(); if (mVisible && mIndeterminate) { startIndeterminateAnimations(); } } /** * Rotation animation duration for indeterminate mode (in milliseconds) */ @IntRange(from = 0) public long getIndeterminateRotationAnimationDuration() { return mIndeterminateRotationAnimator.getDuration(); } /** * Rotation animation duration for indeterminate mode (in milliseconds) */ public void setIndeterminateRotationAnimationDuration(@IntRange(from = 0) final long duration) { if (duration < 0) { throw new IllegalArgumentException("Animation duration can't be negative"); } cancelIndeterminateAnimations(); mIndeterminateRotationAnimator.setDuration(duration); invalidate(); if (mVisible && mIndeterminate) { startIndeterminateAnimations(); } } /** * Rotation animation interpolator for indeterminate mode */ @NonNull public TimeInterpolator getIndeterminateRotationAnimationInterpolator() { return mIndeterminateRotationAnimator.getInterpolator(); } /** * Rotation animation interpolator for indeterminate mode */ public void setIndeterminateRotationAnimationInterpolator(@NonNull final TimeInterpolator interpolator) { //noinspection ConstantConditions if (interpolator == null) { throw new IllegalArgumentException("Interpolator can't be null"); } cancelIndeterminateAnimations(); mIndeterminateRotationAnimator.setInterpolator(interpolator); invalidate(); if (mVisible && mIndeterminate) { startIndeterminateAnimations(); } } /** * Sweep animation duration for indeterminate mode (in milliseconds) */ @IntRange(from = 0) public long getIndeterminateSweepAnimationDuration() { return mIndeterminateSweepAnimator.getDuration(); } /** * Sweep animation duration for indeterminate mode (in milliseconds) */ public void setIndeterminateSweepAnimationDuration(@IntRange(from = 0) final long duration) { if (duration < 0) { throw new IllegalArgumentException("Animation duration can't be negative"); } cancelIndeterminateAnimations(); mIndeterminateSweepAnimator.setDuration(duration); invalidate(); if (mVisible && mIndeterminate) { startIndeterminateAnimations(); } } /** * Sweep animation interpolator for indeterminate mode */ @NonNull public TimeInterpolator getIndeterminateSweepAnimationInterpolator() { return mIndeterminateSweepAnimator.getInterpolator(); } /** * Sweep animation interpolator for indeterminate mode */ public void setIndeterminateSweepAnimationInterpolator(@NonNull final TimeInterpolator interpolator) { //noinspection ConstantConditions if (interpolator == null) { throw new IllegalArgumentException("Interpolator can't be null"); } cancelIndeterminateAnimations(); mIndeterminateSweepAnimator.setInterpolator(interpolator); invalidate(); if (mVisible && mIndeterminate) { startIndeterminateAnimations(); } } /** * Foreground stroke cap */ @NonNull public Paint.Cap getForegroundStrokeCap() { return mBackgroundStrokePaint.getStrokeCap(); } /** * Foreground stroke cap */ public void setForegroundStrokeCap(@NonNull final Paint.Cap cap) { //noinspection ConstantConditions if (cap == null) { throw new IllegalArgumentException("Cap can't be null"); } mForegroundStrokePaint.setStrokeCap(cap); invalidateForegroundStrokeCapAngle(); invalidate(); } /** * Foreground stroke color */ @ColorInt public int getForegroundStrokeColor() { return mForegroundStrokePaint.getColor(); } /** * Foreground stroke color */ public void setForegroundStrokeColor(@ColorInt final int color) { mForegroundStrokePaint.setColor(color); invalidate(); } /** * Foreground stroke width (in pixels) */ @FloatRange(from = 0f, to = Float.MAX_VALUE) public float getForegroundStrokeWidth() { return mForegroundStrokePaint.getStrokeWidth(); } /** * Foreground stroke width (in pixels) */ public void setForegroundStrokeWidth(@FloatRange(from = 0f, to = Float.MAX_VALUE) final float width) { if (width < 0f) { throw new IllegalArgumentException("Width can't be negative"); } mForegroundStrokePaint.setStrokeWidth(width); invalidateDrawRect(); invalidate(); } /** * Background stroke color */ @ColorInt public int getBackgroundStrokeColor() { return mBackgroundStrokePaint.getColor(); } /** * Background stroke color */ public void setBackgroundStrokeColor(@ColorInt final int color) { mBackgroundStrokePaint.setColor(color); invalidate(); } /** * Background stroke width (in pixels) */ @FloatRange(from = 0f, to = Float.MAX_VALUE) public float getBackgroundStrokeWidth() { return mBackgroundStrokePaint.getStrokeWidth(); } /** * Background stroke width (in pixels) */ public void setBackgroundStrokeWidth(@FloatRange(from = 0f, to = Float.MAX_VALUE) final float width) { if (width < 0f) { throw new IllegalArgumentException("Width can't be negative"); } mBackgroundStrokePaint.setStrokeWidth(width); invalidateDrawRect(); invalidate(); } /** * Whether to draw background stroke */ public boolean isDrawBackgroundStroke() { return mDrawBackgroundStroke; } /** * Whether to draw background stroke */ public void setDrawBackgroundStroke(final boolean draw) { mDrawBackgroundStroke = draw; invalidateDrawRect(); invalidate(); } @Override public void onVisibilityAggregated(final boolean visible) { super.onVisibilityAggregated(visible); mVisible = visible; if (mIndeterminate) { if (visible) { startIndeterminateAnimations(); } else { cancelIndeterminateAnimations(); } } else if (!visible) { endProgressAnimation(); } } @Override protected void onDraw(final Canvas canvas) { if (mDrawBackgroundStroke) { canvas.drawOval(mDrawRect, mBackgroundStrokePaint); } float start; float sweep; if (mIndeterminate) { final float startAngle = mIndeterminateStartAngle; final float sweepAngle = mIndeterminateSweepAngle; final float offsetAngle = mIndeterminateOffsetAngle; final float minimumAngle = mIndeterminateMinimumAngle; if (mIndeterminateGrowMode) { start = startAngle - offsetAngle; sweep = sweepAngle + minimumAngle; } else { start = startAngle + sweepAngle - offsetAngle; sweep = 360f - sweepAngle - minimumAngle; } } else { final float maximum = mMaximum; final float progress = mProgress; start = mStartAngle; if (Math.abs(progress) < Math.abs(maximum)) { sweep = progress / maximum * 360f; } else { sweep = 360f; } } final float capAngle = mForegroundStrokeCapAngle; if (capAngle != 0f && Math.abs(sweep) != 360f) { if (sweep > 0) { start += capAngle; sweep -= capAngle * 2f; if (sweep < 0.0001f) { sweep = 0.0001f; } } else if (sweep < 0) { start -= capAngle; sweep += capAngle * 2f; if (sweep > -0.0001f) { sweep = -0.0001f; } } } canvas.drawArc(mDrawRect, start, sweep, false, mForegroundStrokePaint); } @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { final int widthMode = MeasureSpec.getMode(widthMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightMode = MeasureSpec.getMode(heightMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); final int defaultSize = mDefaultSize; final int defaultWidth = Math.max(getSuggestedMinimumWidth(), defaultSize); final int defaultHeight = Math.max(getSuggestedMinimumHeight(), defaultSize); final int width; final int height; switch (widthMode) { case MeasureSpec.EXACTLY: { width = widthSize; break; } case MeasureSpec.AT_MOST: { width = Math.min(defaultWidth, widthSize); break; } case MeasureSpec.UNSPECIFIED: default: { width = defaultWidth; break; } } switch (heightMode) { case MeasureSpec.EXACTLY: { height = heightSize; break; } case MeasureSpec.AT_MOST: { height = Math.min(defaultHeight, heightSize); break; } case MeasureSpec.UNSPECIFIED: default: { height = defaultHeight; break; } } setMeasuredDimension(width, height); invalidateDrawRect(width, height); } @Override protected void onSizeChanged(final int width, final int height, final int oldWidth, final int oldHeight) { invalidateDrawRect(width, height); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mVisible = true; if (mIndeterminate) { startIndeterminateAnimations(); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mVisible = false; cancelIndeterminateAnimations(); cancelProgressAnimation(); } private void initialize(@NonNull final Context context, @Nullable final AttributeSet attributeSet, @AttrRes final int defStyleAttr, @StyleRes final int defStyleRes) { mForegroundStrokePaint.setStyle(Paint.Style.STROKE); mBackgroundStrokePaint.setStyle(Paint.Style.STROKE); final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); mDefaultSize = Math.round(DEFAULT_SIZE_DP * displayMetrics.density); if (attributeSet == null) { mMaximum = DEFAULT_MAXIMUM; mProgress = DEFAULT_PROGRESS; mStartAngle = DEFAULT_START_ANGLE; mIndeterminateMinimumAngle = DEFAULT_INDETERMINATE_MINIMUM_ANGLE; mProgressAnimator.setDuration(DEFAULT_PROGRESS_ANIMATION_DURATION); mIndeterminate = DEFAULT_INDETERMINATE; mAnimateProgress = DEFAULT_ANIMATE_PROGRESS; mDrawBackgroundStroke = DEFAULT_DRAW_BACKGROUND_STROKE; mForegroundStrokePaint.setColor(DEFAULT_FOREGROUND_STROKE_COLOR); mForegroundStrokePaint .setStrokeWidth(Math.round(DEFAULT_FOREGROUND_STROKE_WIDTH_DP * displayMetrics.density)); mForegroundStrokePaint.setStrokeCap(getStrokeCap(DEFAULT_FOREGROUND_STROKE_CAP)); mBackgroundStrokePaint.setColor(DEFAULT_BACKGROUND_STROKE_COLOR); mBackgroundStrokePaint .setStrokeWidth(Math.round(DEFAULT_BACKGROUND_STROKE_WIDTH_DP * displayMetrics.density)); mIndeterminateRotationAnimator.setDuration(DEFAULT_INDETERMINATE_ROTATION_ANIMATION_DURATION); mIndeterminateSweepAnimator.setDuration(DEFAULT_INDETERMINATE_SWEEP_ANIMATION_DURATION); } else { TypedArray attributes = null; try { attributes = context.getTheme() .obtainStyledAttributes(attributeSet, R.styleable.CircularProgressBar, defStyleAttr, defStyleRes); setMaximum(attributes.getFloat(R.styleable.CircularProgressBar_maximum, DEFAULT_MAXIMUM)); setProgress(attributes.getFloat(R.styleable.CircularProgressBar_progress, DEFAULT_PROGRESS)); setStartAngle(attributes.getFloat(R.styleable.CircularProgressBar_startAngle, DEFAULT_START_ANGLE)); setIndeterminateMinimumAngle(attributes .getFloat(R.styleable.CircularProgressBar_indeterminateMinimumAngle, DEFAULT_INDETERMINATE_MINIMUM_ANGLE)); setProgressAnimationDuration(attributes .getInteger(R.styleable.CircularProgressBar_progressAnimationDuration, DEFAULT_PROGRESS_ANIMATION_DURATION)); setIndeterminateRotationAnimationDuration(attributes .getInteger(R.styleable.CircularProgressBar_indeterminateRotationAnimationDuration, DEFAULT_INDETERMINATE_ROTATION_ANIMATION_DURATION)); setIndeterminateSweepAnimationDuration(attributes .getInteger(R.styleable.CircularProgressBar_indeterminateSweepAnimationDuration, DEFAULT_INDETERMINATE_SWEEP_ANIMATION_DURATION)); setForegroundStrokeColor(attributes.getColor(R.styleable.CircularProgressBar_foregroundStrokeColor, DEFAULT_FOREGROUND_STROKE_COLOR)); setBackgroundStrokeColor(attributes.getColor(R.styleable.CircularProgressBar_backgroundStrokeColor, DEFAULT_BACKGROUND_STROKE_COLOR)); setForegroundStrokeWidth(attributes.getDimension(R.styleable.CircularProgressBar_foregroundStrokeWidth, Math.round(DEFAULT_FOREGROUND_STROKE_WIDTH_DP * displayMetrics.density))); setForegroundStrokeCap(getStrokeCap(attributes .getInt(R.styleable.CircularProgressBar_foregroundStrokeCap, DEFAULT_FOREGROUND_STROKE_CAP))); setBackgroundStrokeWidth(attributes.getDimension(R.styleable.CircularProgressBar_backgroundStrokeWidth, Math.round(DEFAULT_BACKGROUND_STROKE_WIDTH_DP * displayMetrics.density))); setAnimateProgress(attributes .getBoolean(R.styleable.CircularProgressBar_animateProgress, DEFAULT_ANIMATE_PROGRESS)); setDrawBackgroundStroke(attributes.getBoolean(R.styleable.CircularProgressBar_drawBackgroundStroke, DEFAULT_DRAW_BACKGROUND_STROKE)); setIndeterminate( attributes.getBoolean(R.styleable.CircularProgressBar_indeterminate, DEFAULT_INDETERMINATE)); } finally { if (attributes != null) { attributes.recycle(); } } } mProgressAnimator.setInterpolator(new DecelerateInterpolator()); mProgressAnimator.addUpdateListener(new ProgressUpdateListener()); mIndeterminateRotationAnimator.setFloatValues(360f); mIndeterminateRotationAnimator.setRepeatMode(ValueAnimator.RESTART); mIndeterminateRotationAnimator.setRepeatCount(ValueAnimator.INFINITE); mIndeterminateRotationAnimator.setInterpolator(new LinearInterpolator()); mIndeterminateRotationAnimator.addUpdateListener(new StartUpdateListener()); mIndeterminateSweepAnimator.setFloatValues(360f - mIndeterminateMinimumAngle * 2f); mIndeterminateSweepAnimator.setInterpolator(new DecelerateInterpolator()); mIndeterminateSweepAnimator.addUpdateListener(new SweepUpdateListener()); mIndeterminateSweepAnimator.addListener(new SweepAnimatorListener()); } private void invalidateDrawRect() { final int width = getWidth(); final int height = getHeight(); if (width > 0 && height > 0) { invalidateDrawRect(width, height); } } private void invalidateDrawRect(final int width, final int height) { final float thickness; if (mDrawBackgroundStroke) { thickness = Math.max(mForegroundStrokePaint.getStrokeWidth(), mBackgroundStrokePaint.getStrokeWidth()); } else { thickness = mForegroundStrokePaint.getStrokeWidth(); } if (width > height) { final float offset = (width - height) / 2f; mDrawRect.set(offset + thickness / 2f + 1f, thickness / 2f + 1f, width - offset - thickness / 2f - 1f, height - thickness / 2f - 1f); } else if (width < height) { final float offset = (height - width) / 2f; mDrawRect.set(thickness / 2f + 1f, offset + thickness / 2f + 1f, width - thickness / 2f - 1f, height - offset - thickness / 2f - 1f); } else { mDrawRect.set(thickness / 2f + 1f, thickness / 2f + 1f, width - thickness / 2f - 1f, height - thickness / 2f - 1f); } invalidateForegroundStrokeCapAngle(); } private void invalidateForegroundStrokeCapAngle() { final Paint.Cap strokeCap = mForegroundStrokePaint.getStrokeCap(); if (strokeCap == null) { mForegroundStrokeCapAngle = 0f; return; } switch (strokeCap) { case SQUARE: case ROUND: { final float r = mDrawRect.width() / 2f; if (r != 0) { mForegroundStrokeCapAngle = 90f * mForegroundStrokePaint.getStrokeWidth() / (float) Math.PI / r; } else { mForegroundStrokeCapAngle = 0f; } break; } case BUTT: default: { mForegroundStrokeCapAngle = 0f; break; } } } private void setProgressInternal(final float progress) { mProgress = progress; invalidate(); } private void setProgressAnimated(final float progress) { mProgressAnimator.setFloatValues(mProgress, progress); mProgressAnimator.start(); } private void endProgressAnimation() { if (mProgressAnimator.isRunning()) { mProgressAnimator.end(); } } private void cancelProgressAnimation() { if (mProgressAnimator.isRunning()) { mProgressAnimator.cancel(); } } private void cancelIndeterminateAnimations() { if (mIndeterminateRotationAnimator.isRunning()) { mIndeterminateRotationAnimator.cancel(); } if (mIndeterminateSweepAnimator.isRunning()) { mIndeterminateSweepAnimator.cancel(); } } private void startIndeterminateAnimations() { if (!mIndeterminateRotationAnimator.isRunning()) { mIndeterminateRotationAnimator.start(); } if (!mIndeterminateSweepAnimator.isRunning()) { mIndeterminateSweepAnimator.start(); } } @NonNull private static Paint.Cap getStrokeCap(final int value) { switch (value) { case 2: { return Paint.Cap.SQUARE; } case 1: { return Paint.Cap.ROUND; } case 0: default: { return Paint.Cap.BUTT; } } } private final class ProgressUpdateListener implements ValueAnimator.AnimatorUpdateListener { @Override public void onAnimationUpdate(final ValueAnimator animation) { setProgressInternal(((Number) animation.getAnimatedValue()).floatValue()); } } private final class StartUpdateListener implements ValueAnimator.AnimatorUpdateListener { @Override public void onAnimationUpdate(final ValueAnimator animation) { mIndeterminateStartAngle = ((Number) animation.getAnimatedValue()).floatValue(); invalidate(); } } private final class SweepUpdateListener implements ValueAnimator.AnimatorUpdateListener { @Override public void onAnimationUpdate(final ValueAnimator animation) { mIndeterminateSweepAngle = ((Number) animation.getAnimatedValue()).floatValue(); } } private final class SweepAnimatorListener implements ValueAnimator.AnimatorListener { private boolean mCancelled; @Override public void onAnimationStart(final Animator animation) { mCancelled = false; } @Override public void onAnimationEnd(final Animator animation) { if (!mCancelled) { post(mSweepRestartAction); } } @Override public void onAnimationCancel(final Animator animation) { mCancelled = true; } @Override public void onAnimationRepeat(final Animator animation) { // Do nothing } } private final class SweepRestartAction implements Runnable { @Override public void run() { mIndeterminateGrowMode = !mIndeterminateGrowMode; if (mIndeterminateGrowMode) { mIndeterminateOffsetAngle = (mIndeterminateOffsetAngle + mIndeterminateMinimumAngle * 2f) % 360f; } if (mIndeterminateSweepAnimator.isRunning()) { mIndeterminateSweepAnimator.cancel(); } if (mVisible) { mIndeterminateSweepAnimator.start(); } } } }