Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
public class CircularProgressDrawable extends Drawable
implements Animatable {
private static final Interpolator ANGLE_INTERPOLATOR = new LinearInterpolator();
private static 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 fBounds = 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 CircularProgressDrawable(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(fBounds, 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);
fBounds.left = bounds.left + mBorderWidth / 2f + .5f;
fBounds.right = bounds.right - mBorderWidth / 2f - .5f;
fBounds.top = bounds.top + mBorderWidth / 2f + .5f;
fBounds.bottom = bounds.bottom - mBorderWidth / 2f - .5f;
}
//////////////////////////////////////////////////////////////////////////////
//////////////// Animation
private Property<SmoothCircularIndeterminateProgressBarDrawable, Float> mAngleProperty
= new Property<SmoothCircularIndeterminateProgressBarDrawable, Float>(Float.class, "angle") {
@Override
public Float get(SmoothCircularIndeterminateProgressBarDrawable object) {
return object.getCurrentGlobalAngle();
}
@Override
public void set(SmoothCircularIndeterminateProgressBarDrawable object, Float value) {
object.setCurrentGlobalAngle(value);
}
};
private Property<SmoothCircularIndeterminateProgressBarDrawable, Float> mSweepProperty
= new Property<SmoothCircularIndeterminateProgressBarDrawable, Float>(Float.class, "arc") {
@Override
public Float get(SmoothCircularIndeterminateProgressBarDrawable object) {
return object.getCurrentSweepAngle();
}
@Override
public void set(SmoothCircularIndeterminateProgressBarDrawable 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;
}
}
/**
* Simplest custom view possible, using CircularProgressDrawable
*/
public class CustomView extends View {
private CircularProgressDrawable mDrawable;
public CustomView(Context context) {
this(context, null);
}
public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDrawable = new CircularProgressDrawable(Color.RED, 10);
mDrawable.setCallback(this);
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (visibility == VISIBLE) {
mDrawable.start();
} else {
mDrawable.stop();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mDrawable.setBounds(0, 0, w, h);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
mDrawable.draw(canvas);
}
@Override
protected boolean verifyDrawable(Drawable who) {
return who == mDrawable || super.verifyDrawable(who);
}
}
@zcweng

This comment has been minimized.

Copy link

@zcweng zcweng commented Aug 16, 2014

hi,where can i find class SmoothCircularIndeterminateProgressBarDrawable?

@castorflex

This comment has been minimized.

Copy link
Owner Author

@castorflex castorflex commented Sep 17, 2014

@zcweng sorry, it's been replaced by CircularProgressDrawable

@Aspirinkb

This comment has been minimized.

Copy link

@Aspirinkb Aspirinkb commented Mar 11, 2015

It is cool, but can you give your thought? It is not easy to understand your code...

@rafasimionato

This comment has been minimized.

Copy link

@rafasimionato rafasimionato commented Apr 16, 2015

Hi dear Castorflex, which is the license for this class ?

@castorflex

This comment has been minimized.

Copy link
Owner Author

@castorflex castorflex commented Apr 17, 2015

Hi @rafasimionato, you can do whatever you want with this stuff, thanks for asking.
If you want a more complete version, check out this repo: https://github.com/castorflex/SmoothProgressBar

@ungesehn

This comment has been minimized.

Copy link

@ungesehn ungesehn commented May 29, 2015

This code will paint the circle always with the same pixel value which can be very ugly on devices with high or low screen densities (depends on what you are using when developing). Thats because of the Paint.setStrokeWidth expects an int value passed by. I made a small fix for this in the MaterialProgressBar class and added support for DP arguments:

 public MaterialProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);

    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MaterialProgressBar, defStyleAttr, 0);
    int color = ta.getColor(R.styleable.MaterialProgressBar_strokeColor, getResources().getColor(R.color.orange));
    int strokeWidth = ta.getInt(R.styleable.MaterialProgressBar_strokeWidth, 3);

    DisplayMetrics dm = getResources().getDisplayMetrics();
    float pixelWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, strokeWidth, dm);

    mDrawable = new CircularProgressDrawable(color, pixelWidth);
    mDrawable.setCallback(this);
    if (getVisibility() == VISIBLE) {
      mDrawable.start();
    }

    ta.recycle();
  }

Anyway - very good work! :)

@anderseliz

This comment has been minimized.

Copy link

@anderseliz anderseliz commented Jul 11, 2015

I love it! For anyone who is looking for an easy integration with DIP/DP or any other units, take a look at this:
Modify your CustomView constructor to look like this:

public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        float width = 5; // Add the width you want
        int unit = TypedValue.COMPLEX_UNIT_DIP; // In the units you want it
        // Remember this is the ring's line, not the whole view.
        float result = TypedValue.applyDimension(unit, width, getResources().getDisplayMetrics());
        mDrawable = new CircularProgressDrawable(Color.BLUE, result);
        mDrawable.setCallback(this);
    }

Thanks, Castorflex!

@castorflex

This comment has been minimized.

Copy link
Owner Author

@castorflex castorflex commented Aug 25, 2015

Well this CustomView is described as the simplest possible. If you want a more complete version, you can check out the project on Github.

@tasneembohra

This comment has been minimized.

Copy link

@tasneembohra tasneembohra commented Sep 10, 2015

Hi ,

The animation is not starting when you enter first time in the screen, I have checked isRunning also and that is false so that should start

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.