package com.material.widget;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.*;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Property;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
/**
* https://github.com/castorflex/SmoothProgressBar
* <p/>
* Copyright 2014 Antoine Merle
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
public class CircularProgress extends View {
private static final int MAX_LEVEL = 10000;
private static final int ANIMATION_RESOLUTION = 200;
private static final int SMALL_SIZE = 0;
private static final int NORMAL_SIZE = 1;
private static final int LARGE_SIZE = 2;
private int mMinWidth;
private int mMaxWidth;
private int mMinHeight;
private int mMaxHeight;
private int mColor;
private int mSize;
private boolean mIndeterminate;
private int mBorderWidth;
private RectF arcRectF;
private int mMax;
private int mProgress;
private int mDuration;
private boolean mHasAnimation;
private boolean mAttached;
private long mLastDrawTime;
private IndeterminateProgressDrawable mIndeterminateProgressDrawable;
private DeterminateProgressDrawable mDeterminateProgressDrawable;
public CircularProgress(Context context) {
this(context, null);
}
public CircularProgress(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircularProgress(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.CircularProgress);
mColor = attributes.getColor(R.styleable.CircularProgress_circular_progress_color,
getResources().getColor(R.color.circular_progress_color));
mSize = attributes.getInt(R.styleable.CircularProgress_circular_progress_size, NORMAL_SIZE);
mIndeterminate = attributes.getBoolean(R.styleable.CircularProgress_circular_progress_indeterminate,
getResources().getBoolean(R.bool.circular_progress_indeterminate));
mBorderWidth = attributes.getDimensionPixelSize(R.styleable.CircularProgress_circular_progress_border_width,
getResources().getDimensionPixelSize(R.dimen.circular_progress_border_width));
mDuration = attributes.getInteger(R.styleable.CircularProgress_circular_progress_duration, ANIMATION_RESOLUTION);
mMax = attributes.getInteger(R.styleable.CircularProgress_circular_progress_max,
getResources().getInteger(R.integer.circular_progress_max));
attributes.recycle();
if (mIndeterminate) {
mIndeterminateProgressDrawable = new IndeterminateProgressDrawable(mColor, mBorderWidth);
mIndeterminateProgressDrawable.setCallback(this);
} else {
mDeterminateProgressDrawable = new DeterminateProgressDrawable(mColor, mBorderWidth, 0);
mDeterminateProgressDrawable.setCallback(this);
}
}
public void setColor(int color) {
mColor = color;
invalidate();
}
public void setIndeterminate(boolean indeterminate) {
mIndeterminate = indeterminate;
invalidate();
}
public void startAnimation() {
if (getVisibility() != VISIBLE) {
return;
}
mIndeterminateProgressDrawable.start();
}
public void stopAnimation() {
mIndeterminateProgressDrawable.stop();
}
public void setProgress(int progress) {
if (mIndeterminate || progress > mMax || progress < 0) {
return;
}
mProgress = progress;
invalidate();
}
public synchronized int getProgress() {
return mIndeterminate ? 0 : mProgress;
}
public synchronized int getMax() {
return mMax;
}
public synchronized void setMax(int max) {
if (max < 0) {
max = 0;
}
if (max != mMax) {
mMax = max;
postInvalidate();
if (mProgress > max) {
mProgress = max;
}
}
}
private RectF getArcRectF() {
if (arcRectF == null) {
int size = Math.min(getWidth() - mBorderWidth * 2, getHeight() - mBorderWidth * 2);
arcRectF = new RectF();
arcRectF.left = (getWidth() - size) / 2;
arcRectF.top = (getHeight() - size) / 2;
arcRectF.right = getWidth() - (getWidth() - size) / 2;
arcRectF.bottom = getHeight() - (getHeight() - size) / 2;
}
return arcRectF;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
int size = 0;
switch (mSize) {
case SMALL_SIZE:
size = getResources().getDimensionPixelSize(R.dimen.circular_progress_small_size);
break;
case NORMAL_SIZE:
size = getResources().getDimensionPixelSize(R.dimen.circular_progress_normal_size);
break;
case LARGE_SIZE:
size = getResources().getDimensionPixelSize(R.dimen.circular_progress_large_size);
break;
}
widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void draw(@NonNull Canvas canvas) {
super.draw(canvas);
if (mIndeterminate) {
mIndeterminateProgressDrawable.draw(canvas);
} else {
mDeterminateProgressDrawable.draw(canvas);
}
}
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (mIndeterminate) {
if (visibility == VISIBLE) {
mIndeterminateProgressDrawable.start();
} else {
mIndeterminateProgressDrawable.stop();
}
}
}
@Override
protected boolean verifyDrawable(Drawable drawable) {
if (mIndeterminate) {
return drawable == mIndeterminateProgressDrawable || super.verifyDrawable(drawable);
} else {
return drawable == mDeterminateProgressDrawable || super.verifyDrawable(drawable);
}
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
if (mIndeterminate) {
mIndeterminateProgressDrawable.setBounds(0, 0, width, height);
} else {
mDeterminateProgressDrawable.setBounds(0, 0, width, height);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (mIndeterminate) {
startAnimation();
}
mAttached = true;
}
@Override
protected void onDetachedFromWindow() {
if (mIndeterminate) {
stopAnimation();
}
super.onDetachedFromWindow();
mAttached = false;
}
private class DeterminateProgressDrawable extends Drawable {
private Paint mPaint;
private float mBorderWidth;
private float mEndAngle;
private final RectF mDrawableBounds = new RectF();
public DeterminateProgressDrawable(int color, int borderWidth, int angle) {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(borderWidth);
mPaint.setColor(color);
mBorderWidth = borderWidth;
mEndAngle = angle;
}
public void setAngle(float angle) {
mEndAngle = angle;
}
@Override
public void draw(Canvas canvas) {
canvas.drawArc(mDrawableBounds, -90.f, 20.f, false, mPaint);
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter colorFilter) {
mPaint.setColorFilter(colorFilter);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mDrawableBounds.left = bounds.left + mBorderWidth / 2f + .5f;
mDrawableBounds.right = bounds.right - mBorderWidth / 2f - .5f;
mDrawableBounds.top = bounds.top + mBorderWidth / 2f + .5f;
mDrawableBounds.bottom = bounds.bottom - mBorderWidth / 2f - .5f;
}
}
/**
* https://gist.github.com/castorflex/4e46a9dc2c3a4245a28e
*/
private class IndeterminateProgressDrawable extends Drawable implements Animatable {
private final Interpolator ANGLE_INTERPOLATOR = new LinearInterpolator();
private final Interpolator SWEEP_INTERPOLATOR = new DecelerateInterpolator();
private static final int ANGLE_ANIMATOR_DURATION = 2000;
private static final int SWEEP_ANIMATOR_DURATION = 600;
private static final int MIN_SWEEP_ANGLE = 30;
private final RectF mDrawableBounds = new RectF();
private ObjectAnimator mObjectAnimatorSweep;
private ObjectAnimator mObjectAnimatorAngle;
private boolean mModeAppearing;
private Paint mPaint;
private float mCurrentGlobalAngleOffset;
private float mCurrentGlobalAngle;
private float mCurrentSweepAngle;
private float mBorderWidth;
private boolean mRunning;
public IndeterminateProgressDrawable(int color, float borderWidth) {
mBorderWidth = borderWidth;
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(borderWidth);
mPaint.setColor(color);
setupAnimations();
}
@Override
public void draw(Canvas canvas) {
float startAngle = mCurrentGlobalAngle - mCurrentGlobalAngleOffset;
float sweepAngle = mCurrentSweepAngle;
if (!mModeAppearing) {
startAngle = startAngle + sweepAngle;
sweepAngle = 360 - sweepAngle - MIN_SWEEP_ANGLE;
} else {
sweepAngle += MIN_SWEEP_ANGLE;
}
canvas.drawArc(mDrawableBounds, startAngle, sweepAngle, false, mPaint);
}
@Override
public void setAlpha(int alpha) {
mPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
mPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSPARENT;
}
private void toggleAppearingMode() {
mModeAppearing = !mModeAppearing;
if (mModeAppearing) {
mCurrentGlobalAngleOffset = (mCurrentGlobalAngleOffset + MIN_SWEEP_ANGLE * 2) % 360;
}
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
mDrawableBounds.left = bounds.left + mBorderWidth / 2f + .5f;
mDrawableBounds.right = bounds.right - mBorderWidth / 2f - .5f;
mDrawableBounds.top = bounds.top + mBorderWidth / 2f + .5f;
mDrawableBounds.bottom = bounds.bottom - mBorderWidth / 2f - .5f;
}
///////////////////////////////////////// Animation /////////////////////////////////////////
private Property<IndeterminateProgressDrawable, Float> mAngleProperty
= new Property<IndeterminateProgressDrawable, Float>(Float.class, "angle") {
@Override
public Float get(IndeterminateProgressDrawable object) {
return object.getCurrentGlobalAngle();
}
@Override
public void set(IndeterminateProgressDrawable object, Float value) {
object.setCurrentGlobalAngle(value);
}
};
private Property<IndeterminateProgressDrawable, Float> mSweepProperty
= new Property<IndeterminateProgressDrawable, Float>(Float.class, "arc") {
@Override
public Float get(IndeterminateProgressDrawable object) {
return object.getCurrentSweepAngle();
}
@Override
public void set(IndeterminateProgressDrawable object, Float value) {
object.setCurrentSweepAngle(value);
}
};
private void setupAnimations() {
mObjectAnimatorAngle = ObjectAnimator.ofFloat(this, mAngleProperty, 360f);
mObjectAnimatorAngle.setInterpolator(ANGLE_INTERPOLATOR);
mObjectAnimatorAngle.setDuration(ANGLE_ANIMATOR_DURATION);
mObjectAnimatorAngle.setRepeatMode(ValueAnimator.RESTART);
mObjectAnimatorAngle.setRepeatCount(ValueAnimator.INFINITE);
mObjectAnimatorSweep = ObjectAnimator.ofFloat(this, mSweepProperty, 360f - MIN_SWEEP_ANGLE * 2);
mObjectAnimatorSweep.setInterpolator(SWEEP_INTERPOLATOR);
mObjectAnimatorSweep.setDuration(SWEEP_ANIMATOR_DURATION);
mObjectAnimatorSweep.setRepeatMode(ValueAnimator.RESTART);
mObjectAnimatorSweep.setRepeatCount(ValueAnimator.INFINITE);
mObjectAnimatorSweep.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
toggleAppearingMode();
}
});
}
@Override
public void start() {
if (isRunning()) {
return;
}
mRunning = true;
mObjectAnimatorAngle.start();
mObjectAnimatorSweep.start();
invalidateSelf();
}
@Override
public void stop() {
if (!isRunning()) {
return;
}
mRunning = false;
mObjectAnimatorAngle.cancel();
mObjectAnimatorSweep.cancel();
invalidateSelf();
}
@Override
public boolean isRunning() {
return mRunning;
}
public void setCurrentGlobalAngle(float currentGlobalAngle) {
mCurrentGlobalAngle = currentGlobalAngle;
invalidateSelf();
}
public float getCurrentGlobalAngle() {
return mCurrentGlobalAngle;
}
public void setCurrentSweepAngle(float currentSweepAngle) {
mCurrentSweepAngle = currentSweepAngle;
invalidateSelf();
}
public float getCurrentSweepAngle() {
return mCurrentSweepAngle;
}
}
}