package com.pitt.library.fresh; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.Rect; import android.graphics.RectF; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.view.AbsSavedState; import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.OvershootInterpolator; /** * A download progressbar with cool animator * https://github.com/dudu90/FreshDownloadView * * @author Pitt * * Licensed under the Apache License 2.0 license see: * http://www.apache.org/licenses/LICENSE-2.0 */ public class FreshDownloadView extends View { private final String TAG = FreshDownloadView.class.getSimpleName(); //the circular radius private float radius; private int circular_color; private int circular_progress_color; private float circular_width; private float circular_edge; private Rect bounds; private RectF mTempBounds; private float mRealLeft; private boolean mPrepareAniRun = false; private float mRealTop; private float mProgressTextSize; private Rect textBounds; private final String STR_PERCENT = "%"; private float mMarkOklength; private AnimatorSet mOkAnimatorSet; private AnimatorSet mErrorAnimatorSet; private float mMarkArcAngle; private float mMarkOkdegree; private float mMarkOkstart; private boolean mMarkOkAniRun; private float mErrorPathLengthLeft; private float mErrorPathLengthRight; private float mErrorRightDegree; private boolean mIfShowError; private float mErrorLeftDegree; private boolean mIfShowMarkRun = false; private boolean mAttached; private boolean mUsing; private Path mDst = new Path(); /** * the view's Status */ public enum STATUS { PREPARE, DOWNLOADING, DOWNLOADED, ERROR } //used when in downloaded private enum STATUS_MARK { DRAW_ARC, DRAW_MARK } private Paint publicPaint; private Path path1; private Path path2; private Path path3; private PathMeasure pathMeasure1; private PathMeasure pathMeasure2; private PathMeasure pathMeasure3; private float mArrowStart; private float startingArrow; private float mArrow_left_length; private float mArrow_right_length; private float mArrow_center_length; private DashPathEffect mArrow_center_effect; private DashPathEffect mArrow_left_effect; private DashPathEffect mArrow_right_effect; private STATUS status = STATUS.PREPARE; private STATUS_MARK status_mark; private AnimatorSet prepareAnimator; private float mProgress; private final static float START_ANGLE = -90f; private final static float TOTAL_ANGLE = 360f; private final static float MARK_START_ANGLE = 65f; private final static float DEGREE_END_ANGLE = 270f; public FreshDownloadView(Context context) { this(context, null); } public FreshDownloadView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FreshDownloadView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); circular_edge = getResources().getDimension(R.dimen.edge); bounds = new Rect(); mTempBounds = new RectF(); publicPaint = new Paint(); path1 = new Path(); path2 = new Path(); path3 = new Path(); pathMeasure1 = new PathMeasure(); pathMeasure2 = new PathMeasure(); pathMeasure3 = new PathMeasure(); textBounds = new Rect(); parseAttrs(context.obtainStyledAttributes(attrs, R.styleable.FreshDownloadView)); initPaint(); } private void parseAttrs(TypedArray array) { if (array != null) { try { setRadius(array.getDimension(R.styleable.FreshDownloadView_circular_radius, getResources().getDimension(R.dimen.default_radius))); setCircularColor(array.getColor(R.styleable.FreshDownloadView_circular_color, getResources().getColor(R.color.default_circular_color))); setProgressColor(array.getColor(R.styleable.FreshDownloadView_circular_progress_color, getResources().getColor(R.color.default_circular_progress_color))); setCircularWidth(array.getDimension(R.styleable.FreshDownloadView_circular_width, getResources().getDimension(R.dimen.default_circular_width))); setProgressTextSize(array.getDimension(R.styleable.FreshDownloadView_progress_text_size, getResources().getDimension(R.dimen.default_text_size))); } finally { array.recycle(); } } } private void initPaint() { publicPaint.setStrokeCap(Paint.Cap.ROUND); publicPaint.setStrokeWidth(getCircularWidth()); publicPaint.setStyle(Paint.Style.STROKE); publicPaint.setAntiAlias(true); } public void startDownload() { mUsing = true; if (prepareAnimator != null && mPrepareAniRun) { return; } if (prepareAnimator == null) { prepareAnimator = getPrepareAnimator(); } prepareAnimator.start(); } private AnimatorSet getPrepareAnimator() { AnimatorSet animatorSet = new AnimatorSet(); ValueAnimator downAnimaor = ValueAnimator.ofFloat(0f, 0.3f, 0f).setDuration(500); downAnimaor.setInterpolator(new DecelerateInterpolator()); downAnimaor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { final float value = (float) animation.getAnimatedValue(); mArrowStart = startingArrow + (2 - .48f - 1f) * getRadius() * value; updateArrow(); invalidate(); } }); ValueAnimator upAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(800); upAnimator.setInterpolator(new DecelerateInterpolator()); upAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { final float value = (float) animation.getAnimatedValue(); mArrow_left_effect = new DashPathEffect(new float[]{mArrow_left_length, mArrow_left_length}, value * mArrow_left_length); mArrow_right_effect = new DashPathEffect(new float[]{mArrow_right_length, mArrow_right_length}, value * mArrow_right_length); float reduceDis = (1 - value) * (startingArrow - mRealTop); path1.reset(); path1.moveTo(mRealLeft + radius, mRealTop + reduceDis); path1.lineTo(mRealLeft + radius, mRealTop + reduceDis + mArrow_center_length); mArrow_center_effect = new DashPathEffect(new float[]{mArrow_center_length, mArrow_center_length}, value * mArrow_center_length); invalidate(); } }); upAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mArrow_center_effect = null; mArrow_right_effect = null; mArrow_left_effect = null; updateArrow(); } @Override public void onAnimationStart(Animator animation) { } }); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { status = STATUS.DOWNLOADING; invalidate(); } }); animatorSet.play(downAnimaor).before(upAnimator); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationCancel(Animator animation) { mPrepareAniRun = false; } @Override public void onAnimationStart(Animator animation) { mPrepareAniRun = true; } @Override public void onAnimationEnd(Animator animation) { mPrepareAniRun = false; } }); return animatorSet; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int dx = 0; int dy = 0; dx += getPaddingLeft() + getPaddingRight() + getCurrentWidth(); dy += getPaddingTop() + getPaddingBottom() + getCurrentHeight(); final int measureWidth = resolveSizeAndState(dx, widthMeasureSpec, 0); final int measureHeight = resolveSizeAndState(dy, heightMeasureSpec, 0); setMeasuredDimension(Math.max(getSuggestedMinimumWidth(), measureWidth), Math.max(getSuggestedMinimumHeight(), measureHeight)); } private int getCurrentHeight() { return (int) ((getRadius() * 2) + circular_edge * 2); } private int getCurrentWidth() { return (int) ((getRadius() * 2) + circular_edge * 2); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { final int top = 0 + getPaddingTop(); final int bottom = getHeight() - getPaddingBottom(); final int left = 0 + getPaddingLeft(); final int right = getWidth() - getPaddingRight(); updateBounds(top, bottom, left, right); initArrowPath(top, bottom, left, right, getRadius()); } private float buildAngle_percent_of_pain_width() { final double perimeter = getRadius() * 2 * Math.PI; final double width = getCircularWidth(); return (float) (width / perimeter); } /** * update the Bounds of circular * * @param top * @param bottom * @param left * @param right */ private void updateBounds(int top, int bottom, int left, int right) { bounds.set(left, top, right, bottom); } private void initArrowPath(int top, int bottom, int left, int right, float radius) { final float realTop = top + circular_edge; mRealLeft = left + circular_edge; mRealTop = realTop; startingArrow = realTop + radius * .48f; mArrowStart = startingArrow; status = STATUS.PREPARE; updateArrow(); } /** * update the Arrow's Status */ private void updateArrow() { path1.reset(); path2.reset(); path3.reset(); path1.moveTo(mRealLeft + radius, mArrowStart); path1.lineTo(mRealLeft + radius, mArrowStart + radius); path2.moveTo(mRealLeft + radius, mArrowStart + radius); path2.lineTo((float) (mRealLeft + radius - Math.tan(Math.toRadians(40)) * radius * 0.46f), mArrowStart + radius - radius * .46f); path3.moveTo(mRealLeft + radius, mArrowStart + radius); path3.lineTo((float) (mRealLeft + radius + Math.tan(Math.toRadians(40)) * radius * 0.46f), mArrowStart + radius - radius * .46f); pathMeasure1.setPath(path1, false); pathMeasure2.setPath(path2, false); pathMeasure3.setPath(path3, false); mArrow_center_length = pathMeasure1.getLength(); mArrow_left_length = pathMeasure2.getLength(); mArrow_right_length = pathMeasure3.getLength(); } @Override protected void onDraw(Canvas canvas) { publicPaint.setPathEffect(null); publicPaint.setStyle(Paint.Style.STROKE); publicPaint.setColor(getCircularColor()); final RectF arcBounds = mTempBounds; arcBounds.set(bounds); arcBounds.inset(circular_edge, circular_edge); canvas.drawArc(arcBounds, 0, 360, false, publicPaint); switch (status) { case PREPARE: drawPrepare(canvas); break; case DOWNLOADING: drawDownLoading(canvas, arcBounds); break; case DOWNLOADED: drawDownLoaded(canvas, status_mark, arcBounds, mMarkArcAngle); break; case ERROR: drawDownError(canvas); break; default: } } /** * Draw the Arrow */ private void drawPrepare(Canvas canvas) { publicPaint.setColor(getProgressColor()); if (mArrow_center_effect != null) { publicPaint.setPathEffect(mArrow_center_effect); } canvas.drawPath(path1, publicPaint); if (mArrow_left_effect != null) { publicPaint.setPathEffect(mArrow_left_effect); } canvas.drawPath(path2, publicPaint); if (mArrow_right_effect != null) { publicPaint.setPathEffect(mArrow_right_effect); } canvas.drawPath(path3, publicPaint); } /** * Draw the Progress */ private void drawDownLoading(Canvas canvas, RectF arcBounds) { final float progress_degree = mProgress; publicPaint.setColor(getProgressColor()); if (progress_degree <= 0) { canvas.drawPoint(mRealLeft + radius, mRealTop, publicPaint); } else { canvas.drawArc(arcBounds, START_ANGLE, (progress_degree) * TOTAL_ANGLE, false, publicPaint); } drawText(canvas, progress_degree); } private void drawText(Canvas canvas, float progress_degree) { final String sDegree = String.valueOf(Math.round(progress_degree * 100)); final Rect rect = bounds; publicPaint.setStyle(Paint.Style.FILL); publicPaint.setTextSize(getProgressTextSize()); publicPaint.setTextAlign(Paint.Align.CENTER); Paint.FontMetricsInt fontMetrics = publicPaint.getFontMetricsInt(); int baseline = (rect.bottom + rect.top - fontMetrics.bottom - fontMetrics.top) / 2; canvas.drawText(sDegree, rect.centerX(), baseline, publicPaint); publicPaint.getTextBounds(sDegree, 0, sDegree.length(), textBounds); publicPaint.setTextSize(getProgressTextSize() / 3); publicPaint.setTextAlign(Paint.Align.LEFT); canvas.drawText(STR_PERCENT, rect.centerX() + textBounds.width() / 2 + .1f * radius, baseline, publicPaint); } /** * Draw success */ private void drawDownLoaded(Canvas canvas, STATUS_MARK status, RectF bounds, float angle) { publicPaint.setColor(getProgressColor()); switch (status) { case DRAW_ARC: canvas.drawArc(bounds, DEGREE_END_ANGLE - angle, 0.001f * TOTAL_ANGLE, false, publicPaint); break; case DRAW_MARK: final Path dst = mDst; dst.reset(); //to fix hardware speedup bug dst.lineTo(0, 0); pathMeasure1.getSegment(mMarkOkstart * mMarkOklength, (mMarkOkstart + mMarkOkdegree) * mMarkOklength, dst, true); canvas.drawPath(dst, publicPaint); break; } } /** * Draw error */ private void drawDownError(Canvas canvas) { if (mIfShowMarkRun) { final float progress = mProgress; drawText(canvas, progress); } publicPaint.setColor(Color.WHITE); final Path dst = mDst; dst.reset(); //to fix hardware speedup bug dst.lineTo(0, 0); pathMeasure1.getSegment(0.2f * mErrorPathLengthLeft, mErrorRightDegree * mErrorPathLengthLeft, dst, true); canvas.drawPath(dst, publicPaint); dst.reset(); //to fix hardware speedup bug dst.lineTo(0, 0); pathMeasure2.getSegment(0.2f * mErrorPathLengthRight, mErrorLeftDegree * mErrorPathLengthRight, dst, true); canvas.drawPath(dst, publicPaint); } /** * update progress * * @param progress percent of 100,the value must from 0f to 1f */ public void upDateProgress(float progress) { setProgressInternal(progress); } /** * update progress * * @param progress the value must from 0 to 100; */ public void upDateProgress(int progress) { upDateProgress((float) progress / 100); } /** * call it when you want to reset all; */ public void reset() { resetStatus(); } /** * Called when you want to reset the Status. * when @see #status==DOWNLOADING or animators are running,the call will be invalid. */ private void resetStatus() { if (status == STATUS.DOWNLOADING || mPrepareAniRun || mIfShowError || mMarkOkAniRun) return; status = STATUS.PREPARE; mArrowStart = startingArrow; updateArrow(); postInvalidate(); this.mProgress = 0; mMarkOkdegree = 0f; mMarkArcAngle = 0f; mMarkOkstart = 0f; mUsing = false; mErrorLeftDegree = 0f; mErrorRightDegree = 0f; } /** * get Use Status * * @return if use by some task. */ public boolean using() { return mUsing; } synchronized void setProgressInternal(float progressInternal) { this.mProgress = progressInternal; if (status == STATUS.PREPARE) { startDownload(); } invalidate(); if (progressInternal >= 1) { showDownloadOk(); } } /** * showDownLoadOK */ public void showDownloadOk() { status = STATUS.DOWNLOADED; makeOkPath(); if (mOkAnimatorSet != null && mMarkOkAniRun) { return; } if (mOkAnimatorSet == null) { mOkAnimatorSet = getDownloadOkAnimator(); } mOkAnimatorSet.start(); } /** * make the Path to show */ private void makeOkPath() { path1.reset(); int w2 = getMeasuredWidth() / 2; int h2 = getMeasuredHeight() / 2; double a = Math.cos(Math.toRadians(25)) * getRadius(); double c = Math.sin(Math.toRadians(25)) * getRadius(); double l = Math.cos(Math.toRadians(53)) * 2 * a; double b = Math.sin(Math.toRadians(53)) * l; double m = Math.cos(Math.toRadians(53)) * l; path1.moveTo((float) (w2 - a), (float) (h2 - c)); path1.lineTo((float) (w2 - a + m), (float) (h2 - c + Math.sin(Math.toRadians(53)) * l)); path1.lineTo((float) (w2 + a), (float) (h2 - c)); pathMeasure1.setPath(path1, false); mMarkOklength = pathMeasure1.getLength(); } /** * create a new DownLoadOkAnimator * * @return a new Animatorset for DownloadOk. */ private AnimatorSet getDownloadOkAnimator() { AnimatorSet animatorSet = new AnimatorSet(); ValueAnimator roundAnimator = ValueAnimator.ofFloat(0f, MARK_START_ANGLE).setDuration(100); roundAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mMarkArcAngle = (float) animation.getAnimatedValue(); invalidate(); } }); roundAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { status_mark = STATUS_MARK.DRAW_ARC; } @Override public void onAnimationEnd(Animator animation) { status_mark = STATUS_MARK.DRAW_MARK; } }); ValueAnimator firstAnimator = ValueAnimator.ofFloat(0f, 0.7f).setDuration(200); firstAnimator.setInterpolator(new AccelerateInterpolator()); firstAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mMarkOkdegree = (float) animation.getAnimatedValue(); invalidate(); } }); ValueAnimator secondAnimator = ValueAnimator.ofFloat(0f, 0.35f, 0.2f).setDuration(500); secondAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mMarkOkstart = (float) animation.getAnimatedValue(); invalidate(); } }); secondAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationCancel(Animator animation) { mIfShowMarkRun = false; } @Override public void onAnimationEnd(Animator animation) { mIfShowMarkRun = false; } @Override public void onAnimationStart(Animator animation) { mIfShowMarkRun = true; } }); animatorSet.play(firstAnimator).after(roundAnimator); animatorSet.play(secondAnimator).after(200); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mMarkOkAniRun = true; } @Override public void onAnimationEnd(Animator animation) { mMarkOkAniRun = false; } @Override public void onAnimationCancel(Animator animation) { mMarkOkAniRun = false; } }); return animatorSet; } public void showDownloadError() { status = STATUS.ERROR; makeErrorPath(); invalidate(); if (mErrorAnimatorSet != null && mIfShowError) { return; } if (mErrorAnimatorSet == null) { mErrorAnimatorSet = getDownLoadErrorAnimator(); } mErrorAnimatorSet.start(); } private void makeErrorPath() { final Rect bounds = this.bounds; final int w2 = bounds.centerX(); final int h2 = bounds.centerY(); path1.reset(); path1.moveTo((float) (w2 - Math.cos(Math.toRadians(45)) * getRadius()), (float) (h2 - Math.sin(Math.toRadians(45)) * getRadius())); path1.lineTo((float) (w2 + Math.cos(Math.toRadians(45)) * getRadius()), (float) (h2 + Math.sin(Math.toRadians(45)) * getRadius())); pathMeasure1.setPath(path1, false); mErrorPathLengthLeft = pathMeasure1.getLength(); path1.reset(); path1.moveTo((float) (w2 + Math.cos(Math.toRadians(45)) * getRadius()), (float) (h2 - Math.sin(Math.toRadians(45)) * getRadius())); path1.lineTo((float) (w2 - Math.cos(Math.toRadians(45)) * getRadius()), (float) (h2 + Math.sin(Math.toRadians(45)) * getRadius())); pathMeasure2.setPath(path1, false); mErrorPathLengthRight = pathMeasure2.getLength(); } private AnimatorSet getDownLoadErrorAnimator() { final AnimatorSet errorSet = new AnimatorSet(); ValueAnimator animator1 = ValueAnimator.ofFloat(0.2f, 0.8f).setDuration(300); animator1.setInterpolator(new OvershootInterpolator()); animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (float) animation.getAnimatedValue(); mErrorLeftDegree = value; invalidate(); } }); ValueAnimator animator2 = ValueAnimator.ofFloat(0.2f, 0.8f).setDuration(300); animator2.setInterpolator(new OvershootInterpolator()); animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mErrorRightDegree = (float) animation.getAnimatedValue(); invalidate(); } }); errorSet.play(animator1); errorSet.play(animator2).after(100); errorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationCancel(Animator animation) { mIfShowError = false; } @Override public void onAnimationEnd(Animator animation) { mIfShowError = false; } @Override public void onAnimationStart(Animator animation) { mIfShowError = true; } }); return errorSet; } public float getProgressTextSize() { return mProgressTextSize; } public void setProgressTextSize(float mProgressTextSize) { this.mProgressTextSize = mProgressTextSize; } public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; } public int getCircularColor() { return circular_color; } public void setCircularColor(int circular_color) { this.circular_color = circular_color; } public int getProgressColor() { return circular_progress_color; } public void setProgressColor(int circular_progress_color) { this.circular_progress_color = circular_progress_color; } public float getCircularWidth() { return circular_width; } public void setCircularWidth(float circular_width) { this.circular_width = circular_width; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mAttached = true; } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mAttached = false; } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); FreshDownloadStatus fds = new FreshDownloadStatus(superState); fds.circular_color = this.circular_color; fds.circular_progress_color = this.circular_progress_color; fds.circular_width = this.circular_width; fds.progress = this.mProgress; fds.radius = this.radius; fds.status = this.status; fds.mProgressTextSize = this.mProgressTextSize; return fds; } @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof FreshDownloadStatus)) { super.onRestoreInstanceState(state); return; } FreshDownloadStatus fds = (FreshDownloadStatus) state; this.circular_color = fds.circular_color; this.circular_progress_color = fds.circular_progress_color; this.circular_width = fds.circular_width; this.mProgress = fds.progress; this.radius = fds.radius; this.status = fds.status; this.mProgressTextSize = fds.mProgressTextSize; } static class FreshDownloadStatus extends AbsSavedState { public STATUS status; public float progress; public float radius; public int circular_color; public int circular_progress_color; public float circular_width; public float mProgressTextSize; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.status == null ? -1 : this.status.ordinal()); dest.writeFloat(this.progress); dest.writeFloat(this.radius); dest.writeInt(this.circular_color); dest.writeInt(this.circular_progress_color); dest.writeFloat(this.circular_width); dest.writeFloat(this.mProgressTextSize); } public FreshDownloadStatus(Parcelable superState) { super(superState); } protected FreshDownloadStatus(Parcel in) { super(in); int tmpStatus = in.readInt(); this.status = tmpStatus == -1 ? null : STATUS.values()[tmpStatus]; this.progress = in.readFloat(); this.radius = in.readFloat(); this.circular_color = in.readInt(); this.circular_progress_color = in.readInt(); this.circular_width = in.readFloat(); this.mProgressTextSize = in.readFloat(); } public static final Creator<FreshDownloadStatus> CREATOR = new Creator<FreshDownloadStatus>() { @Override public FreshDownloadStatus createFromParcel(Parcel source) { return new FreshDownloadStatus(source); } @Override public FreshDownloadStatus[] newArray(int size) { return new FreshDownloadStatus[size]; } }; } }