/*
* Copyright (c) 2015 [1076559197@qq.com | tchen0707@gmail.com]
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package com.dreamspace.uucampus.widget.progress;
import android.animation.ArgbEvaluator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import com.dreamspace.uucampus.R;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.ValueAnimator;
public class CircularProgressDrawable extends Drawable
implements Animatable {
public enum Style {NORMAL, ROUNDED}
public interface OnEndListener {
public void onEnd(CircularProgressDrawable drawable);
}
private static final ArgbEvaluator COLOR_EVALUATOR = new ArgbEvaluator();
public static final Interpolator END_INTERPOLATOR = new LinearInterpolator();
private static final Interpolator DEFAULT_ROTATION_INTERPOLATOR = new LinearInterpolator();
private static final Interpolator DEFAULT_SWEEP_INTERPOLATOR = new DecelerateInterpolator();
private static final int ROTATION_ANIMATOR_DURATION = 2000;
private static final int SWEEP_ANIMATOR_DURATION = 600;
private static final int END_ANIMATOR_DURATION = 200;
private final RectF fBounds = new RectF();
private ValueAnimator mSweepAppearingAnimator;
private ValueAnimator mSweepDisappearingAnimator;
private ValueAnimator mRotationAnimator;
private ValueAnimator mEndAnimator;
private OnEndListener mOnEndListener;
private boolean mModeAppearing;
private Paint mPaint;
private boolean mRunning;
private int mCurrentColor;
private int mCurrentIndexColor;
private float mCurrentSweepAngle;
private float mCurrentRotationAngleOffset = 0;
private float mCurrentRotationAngle = 0;
private float mCurrentEndRatio = 1f;
//params
private Interpolator mAngleInterpolator;
private Interpolator mSweepInterpolator;
private float mBorderWidth;
private int[] mColors;
private float mSweepSpeed;
private float mRotationSpeed;
private int mMinSweepAngle;
private int mMaxSweepAngle;
private boolean mFirstSweepAnimation;
private CircularProgressDrawable(int[] colors,
float borderWidth,
float sweepSpeed,
float rotationSpeed,
int minSweepAngle,
int maxSweepAngle,
Style style,
Interpolator angleInterpolator,
Interpolator sweepInterpolator) {
mSweepInterpolator = sweepInterpolator;
mAngleInterpolator = angleInterpolator;
mBorderWidth = borderWidth;
mCurrentIndexColor = 0;
mColors = colors;
mCurrentColor = mColors[0];
mSweepSpeed = sweepSpeed;
mRotationSpeed = rotationSpeed;
mMinSweepAngle = minSweepAngle;
mMaxSweepAngle = maxSweepAngle;
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(borderWidth);
mPaint.setStrokeCap(style == Style.ROUNDED ? Paint.Cap.ROUND : Paint.Cap.BUTT);
mPaint.setColor(mColors[0]);
setupAnimations();
}
private void reinitValues() {
mFirstSweepAnimation = true;
mCurrentEndRatio = 1f;
mPaint.setColor(mCurrentColor);
}
@Override
public void draw(Canvas canvas) {
float startAngle = mCurrentRotationAngle - mCurrentRotationAngleOffset;
float sweepAngle = mCurrentSweepAngle;
if (!mModeAppearing) {
startAngle = startAngle + (360 - sweepAngle);
}
startAngle %= 360;
if (mCurrentEndRatio < 1f) {
float newSweepAngle = sweepAngle * mCurrentEndRatio;
startAngle = (startAngle + (sweepAngle - newSweepAngle)) % 360;
sweepAngle = newSweepAngle;
}
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.TRANSLUCENT;
}
@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;
}
private void setAppearing() {
mModeAppearing = true;
mCurrentRotationAngleOffset += mMinSweepAngle;
}
private void setDisappearing() {
mModeAppearing = false;
mCurrentRotationAngleOffset = mCurrentRotationAngleOffset + (360 - mMaxSweepAngle);
}
//////////////////////////////////////////////////////////////////////////////
//////////////// Animation
private void setupAnimations() {
mRotationAnimator = ValueAnimator.ofFloat(0f, 360f);
mRotationAnimator.setInterpolator(mAngleInterpolator);
mRotationAnimator.setDuration((long) (ROTATION_ANIMATOR_DURATION / mRotationSpeed));
mRotationAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float angle = animation.getAnimatedFraction() * 360f;
setCurrentRotationAngle(angle);
}
});
mRotationAnimator.setRepeatCount(ValueAnimator.INFINITE);
mRotationAnimator.setRepeatMode(ValueAnimator.RESTART);
mSweepAppearingAnimator = ValueAnimator.ofFloat(mMinSweepAngle, mMaxSweepAngle);
mSweepAppearingAnimator.setInterpolator(mSweepInterpolator);
mSweepAppearingAnimator.setDuration((long) (SWEEP_ANIMATOR_DURATION / mSweepSpeed));
mSweepAppearingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = animation.getAnimatedFraction();
float angle;
if (mFirstSweepAnimation) {
angle = animatedFraction * mMaxSweepAngle;
} else {
angle = mMinSweepAngle + animatedFraction * (mMaxSweepAngle - mMinSweepAngle);
}
setCurrentSweepAngle(angle);
}
});
mSweepAppearingAnimator.addListener(new Animator.AnimatorListener() {
boolean cancelled = false;
@Override
public void onAnimationStart(Animator animation) {
cancelled = false;
mModeAppearing = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!cancelled) {
mFirstSweepAnimation = false;
setDisappearing();
mSweepDisappearingAnimator.start();
}
}
@Override
public void onAnimationCancel(Animator animation) {
cancelled = true;
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mSweepDisappearingAnimator = ValueAnimator.ofFloat(mMaxSweepAngle, mMinSweepAngle);
mSweepDisappearingAnimator.setInterpolator(mSweepInterpolator);
mSweepDisappearingAnimator.setDuration((long) (SWEEP_ANIMATOR_DURATION / mSweepSpeed));
mSweepDisappearingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedFraction = animation.getAnimatedFraction();
setCurrentSweepAngle(mMaxSweepAngle - animatedFraction * (mMaxSweepAngle - mMinSweepAngle));
long duration = animation.getDuration();
long played = animation.getCurrentPlayTime();
float fraction = (float) played / duration;
if (mColors.length > 1 && fraction > .7f) { //because
int prevColor = mCurrentColor;
int nextColor = mColors[(mCurrentIndexColor + 1) % mColors.length];
int newColor = (Integer) COLOR_EVALUATOR.evaluate((fraction - .7f) / (1 - .7f), prevColor, nextColor);
mPaint.setColor(newColor);
}
}
});
mSweepDisappearingAnimator.addListener(new Animator.AnimatorListener() {
boolean cancelled;
@Override
public void onAnimationStart(Animator animation) {
cancelled = false;
}
@Override
public void onAnimationEnd(Animator animation) {
if (!cancelled) {
setAppearing();
mCurrentIndexColor = (mCurrentIndexColor + 1) % mColors.length;
mCurrentColor = mColors[mCurrentIndexColor];
mPaint.setColor(mCurrentColor);
mSweepAppearingAnimator.start();
}
}
@Override
public void onAnimationCancel(Animator animation) {
cancelled = true;
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mEndAnimator = ValueAnimator.ofFloat(1f, 0f);
mEndAnimator.setInterpolator(END_INTERPOLATOR);
mEndAnimator.setDuration(END_ANIMATOR_DURATION);
mEndAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
setEndRatio(1f - animation.getAnimatedFraction());
}
});
mEndAnimator.addListener(new Animator.AnimatorListener() {
private boolean cancelled;
@Override
public void onAnimationStart(Animator animation) {
cancelled = false;
}
@Override
public void onAnimationEnd(Animator animation) {
setEndRatio(0f);
if (!cancelled) stop();
}
@Override
public void onAnimationCancel(Animator animation) {
cancelled = true;
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
@Override
public void start() {
if (isRunning()) {
return;
}
mRunning = true;
reinitValues();
mRotationAnimator.start();
mSweepAppearingAnimator.start();
invalidateSelf();
}
@Override
public void stop() {
if (!isRunning()) {
return;
}
mRunning = false;
stopAnimators();
invalidateSelf();
}
private void stopAnimators() {
mRotationAnimator.cancel();
mSweepAppearingAnimator.cancel();
mSweepDisappearingAnimator.cancel();
mEndAnimator.cancel();
}
public void progressiveStop(OnEndListener listener) {
if (!isRunning() || mEndAnimator.isRunning()) {
return;
}
mOnEndListener = listener;
mEndAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
mEndAnimator.removeListener(this);
if (mOnEndListener != null) mOnEndListener.onEnd(CircularProgressDrawable.this);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
mEndAnimator.start();
}
public void progressiveStop() {
progressiveStop(null);
}
@Override
public boolean isRunning() {
return mRunning;
}
public void setCurrentRotationAngle(float currentRotationAngle) {
mCurrentRotationAngle = currentRotationAngle;
invalidateSelf();
}
public void setCurrentSweepAngle(float currentSweepAngle) {
mCurrentSweepAngle = currentSweepAngle;
invalidateSelf();
}
private void setEndRatio(float ratio) {
mCurrentEndRatio = ratio;
invalidateSelf();
}
public static class Builder {
private int[] mColors;
private float mSweepSpeed;
private float mRotationSpeed;
private float mStrokeWidth;
private int mMinSweepAngle;
private int mMaxSweepAngle;
private Style mStyle;
private Interpolator mSweepInterpolator = DEFAULT_SWEEP_INTERPOLATOR;
private Interpolator mAngleInterpolator = DEFAULT_ROTATION_INTERPOLATOR;
public Builder(Context context) {
this(context, false);
}
public Builder(Context context, boolean editMode) {
initValues(context, editMode);
}
private void initValues(Context context, boolean editMode) {
mStrokeWidth = context.getResources().getDimension(R.dimen.cpb_default_stroke_width);
mSweepSpeed = 1f;
mRotationSpeed = 1f;
if (editMode) {
mColors = new int[]{Color.BLUE};
mMinSweepAngle = 20;
mMaxSweepAngle = 300;
} else {
mColors = new int[]{context.getResources().getColor(R.color.cpb_default_color)};
mMinSweepAngle = context.getResources().getInteger(R.integer.cpb_default_min_sweep_angle);
mMaxSweepAngle = context.getResources().getInteger(R.integer.cpb_default_max_sweep_angle);
}
mStyle = Style.ROUNDED;
}
public Builder color(int color) {
mColors = new int[]{color};
return this;
}
public Builder colors(int[] colors) {
CircularProgressBarUtils.checkColors(colors);
mColors = colors;
return this;
}
public Builder sweepSpeed(float sweepSpeed) {
CircularProgressBarUtils.checkSpeed(sweepSpeed);
mSweepSpeed = sweepSpeed;
return this;
}
public Builder rotationSpeed(float rotationSpeed) {
CircularProgressBarUtils.checkSpeed(rotationSpeed);
mRotationSpeed = rotationSpeed;
return this;
}
public Builder minSweepAngle(int minSweepAngle) {
CircularProgressBarUtils.checkAngle(minSweepAngle);
mMinSweepAngle = minSweepAngle;
return this;
}
public Builder maxSweepAngle(int maxSweepAngle) {
CircularProgressBarUtils.checkAngle(maxSweepAngle);
mMaxSweepAngle = maxSweepAngle;
return this;
}
public Builder strokeWidth(float strokeWidth) {
CircularProgressBarUtils.checkPositiveOrZero(strokeWidth, "StrokeWidth");
mStrokeWidth = strokeWidth;
return this;
}
public Builder style(Style style) {
CircularProgressBarUtils.checkNotNull(style, "Style");
mStyle = style;
return this;
}
public Builder sweepInterpolator(Interpolator interpolator) {
CircularProgressBarUtils.checkNotNull(interpolator, "Sweep interpolator");
mSweepInterpolator = interpolator;
return this;
}
public Builder angleInterpolator(Interpolator interpolator) {
CircularProgressBarUtils.checkNotNull(interpolator, "Angle interpolator");
mAngleInterpolator = interpolator;
return this;
}
public CircularProgressDrawable build() {
return new CircularProgressDrawable(mColors,
mStrokeWidth,
mSweepSpeed,
mRotationSpeed,
mMinSweepAngle,
mMaxSweepAngle,
mStyle,
mAngleInterpolator,
mSweepInterpolator);
}
}
}