package org.wheelmap.android.view.progress;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
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.support.annotation.ColorInt;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.LinearInterpolator;
import java.util.ArrayList;
import java.util.List;
class CircularProgressView extends View {
private static final float INDETERMINANT_MIN_SWEEP = 15f;
public interface CircularProgressViewListener {
/**
* Called when resetAnimation() is called.
*/
void onAnimationStart();
void onAnimationEnd();
}
private Paint paint;
private int size = 0;
private RectF bounds;
private float indeterminateSweep, indeterminateRotateOffset;
private int thickness;
@ColorInt
private int color = Color.BLUE;
private int animDuration;
private int animSteps;
private boolean animationRunning = true;
private List<CircularProgressViewListener> listeners;
// Animation related stuff
private float startAngle;
private AnimatorSet indeterminateAnimator;
private AnimatorSet completeAnimator;
public CircularProgressView(Context context) {
super(context);
init(null, 0);
}
public CircularProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, 0);
}
public CircularProgressView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs, defStyle);
}
protected void init(AttributeSet attrs, int defStyle) {
listeners = new ArrayList<>();
initAttributes(attrs, defStyle);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
updatePaint();
bounds = new RectF();
}
private void initAttributes(AttributeSet attrs, int defStyle) {
// Initialize attributes from styleable attributes
thickness = (int) Utils.dpToPx(getContext(), 4);
int accentColor = getContext().getResources().getIdentifier("colorAccent", "attr", getContext().getPackageName());
color = Color.parseColor("#2196F3");
// If using support library v7 accentColor
if (accentColor != 0) {
TypedValue t = new TypedValue();
getContext().getTheme().resolveAttribute(accentColor, t, true);
color = t.data;
}
// If using native accentColor (SDK >21)
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
TypedArray t = getContext().obtainStyledAttributes(new int[]{android.R.attr.colorAccent});
color = t.getColor(0, color);
}
animDuration = 4000;
animSteps = 3;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int xPad = getPaddingLeft() + getPaddingRight();
int yPad = getPaddingTop() + getPaddingBottom();
int width = getMeasuredWidth() - xPad;
int height = getMeasuredHeight() - yPad;
size = (width < height) ? width : height;
setMeasuredDimension(size + xPad, size + yPad);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
size = (w < h) ? w : h;
updateBounds();
}
private void updateBounds() {
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
bounds.set(paddingLeft + thickness, paddingTop + thickness, size - paddingLeft - thickness, size - paddingTop - thickness);
}
private void updatePaint() {
paint.setColor(color);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(thickness);
paint.setStrokeCap(Paint.Cap.BUTT);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(color);
// Draw the arc
canvas.drawArc(bounds, startAngle + indeterminateRotateOffset, indeterminateSweep, false, paint);
}
/**
* Get the thickness of the progress bar arc.
*
* @return the thickness of the progress bar arc
*/
public int getThickness() {
return thickness;
}
/**
* Sets the thickness of the progress bar arc.
*
* @param thickness the thickness of the progress bar arc
*/
public void setThickness(int thickness) {
this.thickness = thickness;
updatePaint();
updateBounds();
invalidate();
}
/**
* @return the color of the progress bar
*/
public int getColor() {
return color;
}
/**
* Sets the color of the progress bar.
*
* @param color the color of the progress bar
*/
public void setColor(int color) {
this.color = color;
updatePaint();
invalidate();
}
public void completeAnimation(@ColorInt int color) {
animationRunning = false;
stopAnimation();
completeAnimator = createCompleteAnimator(color);
completeAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
for (CircularProgressViewListener listener : listeners) {
listener.onAnimationEnd();
}
}
});
completeAnimator.start();
}
/**
* Register a CircularProgressViewListener with this View
*
* @param listener The listener to register
*/
public void addListener(CircularProgressViewListener listener) {
if (listener != null)
listeners.add(listener);
}
/**
* Unregister a CircularProgressViewListener with this View
*
* @param listener The listener to unregister
*/
public void removeListener(CircularProgressViewListener listener) {
listeners.remove(listener);
}
/**
* Starts the progress bar animation.
* (This is an alias of resetAnimation() so it does the same thing.)
*/
public void startAnimation() {
animationRunning = true;
indeterminateRotateOffset = 0;
startAngle = -90;
resetAnimation();
}
/**
* Resets the animation.
*/
void resetAnimation() {
if (getVisibility() != VISIBLE || !animationRunning) {
return;
}
if (completeAnimator != null && completeAnimator.isRunning()) {
completeAnimator.cancel();
}
// Cancel all the old animators
if (indeterminateAnimator != null && indeterminateAnimator.isRunning()) {
indeterminateAnimator.cancel();
}
indeterminateSweep = INDETERMINANT_MIN_SWEEP;
// Build the whole AnimatorSet
indeterminateAnimator = new AnimatorSet();
AnimatorSet prevSet = null, nextSet;
for (int k = 0; k < animSteps; k++) {
nextSet = createIndeterminateAnimator(k);
AnimatorSet.Builder builder = indeterminateAnimator.play(nextSet);
if (prevSet != null) {
builder.after(prevSet);
}
prevSet = nextSet;
}
// Listen to end of animation so we can infinitely loop
indeterminateAnimator.addListener(new AnimatorListenerAdapter() {
boolean wasCancelled = false;
@Override
public void onAnimationCancel(Animator animation) {
wasCancelled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!wasCancelled) {
resetAnimation();
}
}
});
indeterminateAnimator.start();
for (CircularProgressViewListener listener : listeners) {
listener.onAnimationStart();
}
}
/**
* Stops the animation
*/
public void stopAnimation() {
if (indeterminateAnimator != null) {
indeterminateAnimator.cancel();
indeterminateAnimator = null;
}
}
// Creates the animators for one step of the animation
private AnimatorSet createIndeterminateAnimator(final float step) {
final float maxSweep = 360f * (animSteps - 1) / animSteps + INDETERMINANT_MIN_SWEEP;
final float start = -90f + step * (maxSweep - INDETERMINANT_MIN_SWEEP);
// Extending the front of the arc
ValueAnimator frontEndExtend = ValueAnimator.ofFloat(INDETERMINANT_MIN_SWEEP, maxSweep);
frontEndExtend.setDuration(animDuration / animSteps / 2);
frontEndExtend.setInterpolator(new DecelerateInterpolator(1));
frontEndExtend.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
indeterminateSweep = (Float) animation.getAnimatedValue();
invalidate();
}
});
// Overall rotation
ValueAnimator rotateAnimator1 = ValueAnimator.ofFloat(step * 720f / animSteps, (step + .5f) * 720f / animSteps);
rotateAnimator1.setDuration(animDuration / animSteps / 2);
rotateAnimator1.setInterpolator(new LinearInterpolator());
rotateAnimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
indeterminateRotateOffset = (Float) animation.getAnimatedValue();
}
});
// Followed by...
// Retracting the back end of the arc
ValueAnimator backEndRetract = ValueAnimator.ofFloat(start, start + maxSweep - INDETERMINANT_MIN_SWEEP);
backEndRetract.setDuration(animDuration / animSteps / 2);
backEndRetract.setInterpolator(new DecelerateInterpolator(1));
backEndRetract.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
startAngle = (Float) animation.getAnimatedValue();
indeterminateSweep = maxSweep - startAngle + start;
invalidate();
}
});
// More overall rotation
ValueAnimator rotateAnimator2 = ValueAnimator.ofFloat((step + .5f) * 720f / animSteps, (step + 1) * 720f / animSteps);
rotateAnimator2.setDuration(animDuration / animSteps / 2);
rotateAnimator2.setInterpolator(new LinearInterpolator());
rotateAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
indeterminateRotateOffset = (Float) animation.getAnimatedValue();
}
});
AnimatorSet set = new AnimatorSet();
set.play(frontEndExtend).with(rotateAnimator1);
set.play(backEndRetract).with(rotateAnimator2).after(rotateAnimator1);
return set;
}
// Creates the animators for one step of the animation
private AnimatorSet createCompleteAnimator(@ColorInt final int color) {
final float maxSweep = 360f;
final float start = startAngle;
int durationPerAngle = animDuration / 360;
int duration = (int) ((360 - (indeterminateSweep % 360)) * durationPerAngle) / 2;
// Extending the front of the arc
ValueAnimator frontEndExtend = ValueAnimator.ofFloat(indeterminateSweep, maxSweep);
frontEndExtend.setDuration(duration);
frontEndExtend.setInterpolator(new DecelerateInterpolator(1));
frontEndExtend.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
indeterminateSweep = (Float) animation.getAnimatedValue();
invalidate();
}
});
// Followed by...
// Retracting the back end of the arc
ValueAnimator backEndRetract = ValueAnimator.ofFloat(start, start + maxSweep - INDETERMINANT_MIN_SWEEP);
backEndRetract.setDuration(duration);
backEndRetract.setInterpolator(new DecelerateInterpolator(1));
backEndRetract.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
startAngle = (Float) animation.getAnimatedValue();
invalidate();
}
});
ValueAnimator colorAnimator = ValueAnimator.ofObject(new ArgbEvaluator(), this.color, color);
colorAnimator.setDuration(duration);
colorAnimator.setInterpolator(new DecelerateInterpolator(1));
colorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
CircularProgressView.this.color = (Integer) valueAnimator.getAnimatedValue();
}
});
AnimatorSet set = new AnimatorSet();
set.play(frontEndExtend).with(backEndRetract).with(colorAnimator);
return set;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
startAnimation();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopAnimation();
}
@Override
public void setVisibility(int visibility) {
int currentVisibility = getVisibility();
super.setVisibility(visibility);
if (visibility != currentVisibility) {
if (visibility == View.VISIBLE) {
resetAnimation();
} else if (visibility == View.GONE || visibility == View.INVISIBLE) {
stopAnimation();
}
}
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == View.VISIBLE) {
resetAnimation();
} else if (visibility == View.GONE || visibility == View.INVISIBLE) {
stopAnimation();
}
}
}