package com.miris.ui.view; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.util.DisplayMetrics; import android.view.View; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Animation; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.Transformation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; public class MaterialProgressDrawable extends Drawable implements Animatable { private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); private static final Interpolator END_CURVE_INTERPOLATOR = new EndCurveInterpolator(); private static final Interpolator START_CURVE_INTERPOLATOR = new StartCurveInterpolator(); private static final Interpolator EASE_INTERPOLATOR = new AccelerateDecelerateInterpolator(); @Retention(RetentionPolicy.CLASS) @IntDef({LARGE, DEFAULT}) public @interface ProgressDrawableSize {} static final int LARGE = 0; static final int DEFAULT = 1; private static final int CIRCLE_DIAMETER = 40; private static final float CENTER_RADIUS = 8.75f; private static final float STROKE_WIDTH = 2.5f; private static final int CIRCLE_DIAMETER_LARGE = 56; private static final float CENTER_RADIUS_LARGE = 12.5f; private static final float STROKE_WIDTH_LARGE = 3f; private final int[] COLORS = new int[] { Color.BLACK }; private static final int ANIMATION_DURATION = 1000 * 80 / 60; private static final float NUM_POINTS = 5f; private final ArrayList<Animation> mAnimators = new ArrayList<Animation>(); private final Ring mRing; private float mRotation; private static final int ARROW_WIDTH = 10; private static final int ARROW_HEIGHT = 5; private static final float ARROW_OFFSET_ANGLE = 5; private static final int ARROW_WIDTH_LARGE = 12; private static final int ARROW_HEIGHT_LARGE = 6; private static final float MAX_PROGRESS_ARC = .8f; private Resources mResources; private View mParent; private Animation mAnimation; private float mRotationCount; private double mWidth; private double mHeight; boolean mFinishing; public MaterialProgressDrawable(Context context, View parent) { mParent = parent; mResources = context.getResources(); mRing = new Ring(mCallback); mRing.setColors(COLORS); updateSizes(DEFAULT); setupAnimators(); } private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { final Ring ring = mRing; final DisplayMetrics metrics = mResources.getDisplayMetrics(); final float screenDensity = metrics.density; mWidth = progressCircleWidth * screenDensity; mHeight = progressCircleHeight * screenDensity; ring.setStrokeWidth((float) strokeWidth * screenDensity); ring.setCenterRadius(centerRadius * screenDensity); ring.setColorIndex(0); ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); ring.setInsets((int) mWidth, (int) mHeight); } public void updateSizes(@ProgressDrawableSize int size) { if (size == LARGE) { setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); } else { setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, ARROW_WIDTH, ARROW_HEIGHT); } } public void showArrow(boolean show) { mRing.setShowArrow(show); } public void setArrowScale(float scale) { mRing.setArrowScale(scale); } public void setStartEndTrim(float startAngle, float endAngle) { mRing.setStartTrim(startAngle); mRing.setEndTrim(endAngle); } public void setProgressRotation(float rotation) { mRing.setRotation(rotation); } public void setBackgroundColor(int color) { mRing.setBackgroundColor(color); } public void setColorSchemeColors(int... colors) { mRing.setColors(colors); mRing.setColorIndex(0); } @Override public int getIntrinsicHeight() { return (int) mHeight; } @Override public int getIntrinsicWidth() { return (int) mWidth; } @Override public void draw(Canvas c) { final Rect bounds = getBounds(); final int saveCount = c.save(); c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); mRing.draw(c, bounds); c.restoreToCount(saveCount); } @Override public void setAlpha(int alpha) { mRing.setAlpha(alpha); } public int getAlpha() { return mRing.getAlpha(); } @Override public void setColorFilter(ColorFilter colorFilter) { mRing.setColorFilter(colorFilter); } @SuppressWarnings("unused") void setRotation(float rotation) { mRotation = rotation; invalidateSelf(); } @SuppressWarnings("unused") private float getRotation() { return mRotation; } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } @Override public boolean isRunning() { final ArrayList<Animation> animators = mAnimators; final int N = animators.size(); for (int i = 0; i < N; i++) { final Animation animator = animators.get(i); if (animator.hasStarted() && !animator.hasEnded()) { return true; } } return false; } @Override public void start() { mAnimation.reset(); mRing.storeOriginals(); if (mRing.getEndTrim() != mRing.getStartTrim()) { mFinishing = true; mAnimation.setDuration(ANIMATION_DURATION/2); mParent.startAnimation(mAnimation); } else { mRing.setColorIndex(0); mRing.resetOriginals(); mAnimation.setDuration(ANIMATION_DURATION); mParent.startAnimation(mAnimation); } } @Override public void stop() { mParent.clearAnimation(); setRotation(0); mRing.setShowArrow(false); mRing.setColorIndex(0); mRing.resetOriginals(); } private void applyFinishTranslation(float interpolatedTime, Ring ring) { float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) + 1f); final float startTrim = ring.getStartingStartTrim() + (ring.getStartingEndTrim() - ring.getStartingStartTrim()) * interpolatedTime; ring.setStartTrim(startTrim); final float rotation = ring.getStartingRotation() + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); ring.setRotation(rotation); } private void setupAnimators() { final Ring ring = mRing; final Animation animation = new Animation() { @Override public void applyTransformation(float interpolatedTime, Transformation t) { if (mFinishing) { applyFinishTranslation(interpolatedTime, ring); } else { final float minProgressArc = (float) Math.toRadians( ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); final float startingEndTrim = ring.getStartingEndTrim(); final float startingTrim = ring.getStartingStartTrim(); final float startingRotation = ring.getStartingRotation(); final float minArc = MAX_PROGRESS_ARC - minProgressArc; final float endTrim = startingEndTrim + (minArc * START_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); ring.setEndTrim(endTrim); final float startTrim = startingTrim + (MAX_PROGRESS_ARC * END_CURVE_INTERPOLATOR.getInterpolation(interpolatedTime)); ring.setStartTrim(startTrim); final float rotation = startingRotation + (0.25f * interpolatedTime); ring.setRotation(rotation); float groupRotation = ((720.0f / NUM_POINTS) * interpolatedTime) + (720.0f * (mRotationCount / NUM_POINTS)); setRotation(groupRotation); } } }; animation.setRepeatCount(Animation.INFINITE); animation.setRepeatMode(Animation.RESTART); animation.setInterpolator(LINEAR_INTERPOLATOR); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { mRotationCount = 0; } @Override public void onAnimationEnd(Animation animation) { } @Override public void onAnimationRepeat(Animation animation) { ring.storeOriginals(); ring.goToNextColor(); ring.setStartTrim(ring.getEndTrim()); if (mFinishing) { mFinishing = false; animation.setDuration(ANIMATION_DURATION); ring.setShowArrow(false); } else { mRotationCount = (mRotationCount + 1) % (NUM_POINTS); } } }); mAnimation = animation; } private final Callback mCallback = new Callback() { @Override public void invalidateDrawable(Drawable d) { invalidateSelf(); } @Override public void scheduleDrawable(Drawable d, Runnable what, long when) { scheduleSelf(what, when); } @Override public void unscheduleDrawable(Drawable d, Runnable what) { unscheduleSelf(what); } }; private static class Ring { private final RectF mTempBounds = new RectF(); private final Paint mPaint = new Paint(); private final Paint mArrowPaint = new Paint(); private final Callback mCallback; private float mStartTrim = 0.0f; private float mEndTrim = 0.0f; private float mRotation = 0.0f; private float mStrokeWidth = 5.0f; private float mStrokeInset = 2.5f; private int[] mColors; private int mColorIndex; private float mStartingStartTrim; private float mStartingEndTrim; private float mStartingRotation; private boolean mShowArrow; private Path mArrow; private float mArrowScale; private double mRingCenterRadius; private int mArrowWidth; private int mArrowHeight; private int mAlpha; private final Paint mCirclePaint = new Paint(); private int mBackgroundColor; public Ring(Callback callback) { mCallback = callback; mPaint.setStrokeCap(Paint.Cap.SQUARE); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.STROKE); mArrowPaint.setStyle(Paint.Style.FILL); mArrowPaint.setAntiAlias(true); } public void setBackgroundColor(int color) { mBackgroundColor = color; } public void setArrowDimensions(float width, float height) { mArrowWidth = (int) width; mArrowHeight = (int) height; } public void draw(Canvas c, Rect bounds) { final RectF arcBounds = mTempBounds; arcBounds.set(bounds); arcBounds.inset(mStrokeInset, mStrokeInset); final float startAngle = (mStartTrim + mRotation) * 360; final float endAngle = (mEndTrim + mRotation) * 360; float sweepAngle = endAngle - startAngle; mPaint.setColor(mColors[mColorIndex]); c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); drawTriangle(c, startAngle, sweepAngle, bounds); if (mAlpha < 255) { mCirclePaint.setColor(mBackgroundColor); mCirclePaint.setAlpha(255 - mAlpha); c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, mCirclePaint); } } private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { if (mShowArrow) { if (mArrow == null) { mArrow = new android.graphics.Path(); mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); } else { mArrow.reset(); } float inset = (int) mStrokeInset / 2 * mArrowScale; float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); mArrow.moveTo(0, 0); mArrow.lineTo(mArrowWidth * mArrowScale, 0); mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight * mArrowScale)); mArrow.offset(x - inset, y); mArrow.close(); mArrowPaint.setColor(mColors[mColorIndex]); c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), bounds.exactCenterY()); c.drawPath(mArrow, mArrowPaint); } } public void setColors(@NonNull int[] colors) { mColors = colors; setColorIndex(0); } public void setColorIndex(int index) { mColorIndex = index; } public void goToNextColor() { mColorIndex = (mColorIndex + 1) % (mColors.length); } public void setColorFilter(ColorFilter filter) { mPaint.setColorFilter(filter); invalidateSelf(); } public void setAlpha(int alpha) { mAlpha = alpha; } public int getAlpha() { return mAlpha; } public void setStrokeWidth(float strokeWidth) { mStrokeWidth = strokeWidth; mPaint.setStrokeWidth(strokeWidth); invalidateSelf(); } @SuppressWarnings("unused") public float getStrokeWidth() { return mStrokeWidth; } @SuppressWarnings("unused") public void setStartTrim(float startTrim) { mStartTrim = startTrim; invalidateSelf(); } @SuppressWarnings("unused") public float getStartTrim() { return mStartTrim; } public float getStartingStartTrim() { return mStartingStartTrim; } public float getStartingEndTrim() { return mStartingEndTrim; } @SuppressWarnings("unused") public void setEndTrim(float endTrim) { mEndTrim = endTrim; invalidateSelf(); } @SuppressWarnings("unused") public float getEndTrim() { return mEndTrim; } @SuppressWarnings("unused") public void setRotation(float rotation) { mRotation = rotation; invalidateSelf(); } @SuppressWarnings("unused") public float getRotation() { return mRotation; } public void setInsets(int width, int height) { final float minEdge = (float) Math.min(width, height); float insets; if (mRingCenterRadius <= 0 || minEdge < 0) { insets = (float) Math.ceil(mStrokeWidth / 2.0f); } else { insets = (float) (minEdge / 2.0f - mRingCenterRadius); } mStrokeInset = insets; } @SuppressWarnings("unused") public float getInsets() { return mStrokeInset; } public void setCenterRadius(double centerRadius) { mRingCenterRadius = centerRadius; } public double getCenterRadius() { return mRingCenterRadius; } public void setShowArrow(boolean show) { if (mShowArrow != show) { mShowArrow = show; invalidateSelf(); } } public void setArrowScale(float scale) { if (scale != mArrowScale) { mArrowScale = scale; invalidateSelf(); } } public float getStartingRotation() { return mStartingRotation; } public void storeOriginals() { mStartingStartTrim = mStartTrim; mStartingEndTrim = mEndTrim; mStartingRotation = mRotation; } public void resetOriginals() { mStartingStartTrim = 0; mStartingEndTrim = 0; mStartingRotation = 0; setStartTrim(0); setEndTrim(0); setRotation(0); } private void invalidateSelf() { mCallback.invalidateDrawable(null); } } private static class EndCurveInterpolator extends AccelerateDecelerateInterpolator { @Override public float getInterpolation(float input) { return super.getInterpolation(Math.max(0, (input - 0.5f) * 2.0f)); } } private static class StartCurveInterpolator extends AccelerateDecelerateInterpolator { @Override public float getInterpolation(float input) { return super.getInterpolation(Math.min(1, input * 2.0f)); } } }