package net.bohush.geometricprogressview; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Path; import android.graphics.PointF; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.View; import android.view.animation.LinearInterpolator; import java.util.ArrayList; import java.util.List; public class GeometricProgressView extends View { private static final int DEFAULT_NUMBER_OF_ANGLES = 6; private static final int DEFAULT_SIZE = 64; private static final int DEFAULT_DURATION = 1500; private static final int DEFAULT_FIGURE_PADDING = 2; private static final String DEFAULT_COLOR = "#00897b"; private static final TYPE DEFAULT_TYPE = TYPE.KITE; private int color; private int width, height; private int desiredWidth; private int desiredHeight; private int figurePadding; private int duration; private PointF center; private int numberOfAngles; private List<Figure> figures; private List<ValueAnimator> animators; private TYPE type; public GeometricProgressView(Context context) { this(context, null); } public GeometricProgressView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public GeometricProgressView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialize(context, attrs); } private void initialize(Context context, AttributeSet attrs) { desiredWidth = dpToPx(DEFAULT_SIZE); desiredHeight = dpToPx(DEFAULT_SIZE); center = new PointF(0, 0); if (attrs != null) { final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.GeometricProgressView); figurePadding = array.getDimensionPixelSize(R.styleable.GeometricProgressView_gp_figure_padding, DEFAULT_FIGURE_PADDING); numberOfAngles = array.getInteger(R.styleable.GeometricProgressView_gp_number_of_angles, DEFAULT_NUMBER_OF_ANGLES); setColor(array.getColor(R.styleable.GeometricProgressView_gp_color, Color.parseColor(DEFAULT_COLOR))); duration = array.getInteger(R.styleable.GeometricProgressView_gp_duration, DEFAULT_DURATION); int typeInt = array.getInt(R.styleable.GeometricProgressView_gp_type, 0); switch (typeInt) { case 0: type = TYPE.KITE; break; case 1: type = TYPE.TRIANGLE; break; } array.recycle(); } else { figurePadding = dpToPx(DEFAULT_FIGURE_PADDING); numberOfAngles = DEFAULT_NUMBER_OF_ANGLES; setColor(Color.parseColor(DEFAULT_COLOR)); duration = DEFAULT_DURATION; type = DEFAULT_TYPE; } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int measuredWidth = resolveSize(desiredWidth, widthMeasureSpec); final int measuredHeight = resolveSize(desiredHeight, heightMeasureSpec); setMeasuredDimension(measuredWidth, measuredHeight); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); this.width = getWidth(); this.height = getHeight(); this.center.set(width / 2.0f, height / 2.0f); initializeFigures(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (Figure figure : figures) { figure.draw(canvas); } } public void setNumberOfAngles(int numberOfAngles) { this.numberOfAngles = numberOfAngles; initializeFigures(); } public void setType(TYPE type) { this.type = type; initializeFigures(); } public void setFigurePadding(int figurePadding) { this.figurePadding = figurePadding; initializeFigures(); } public void setFigurePaddingInDp(int figurePadding) { setFigurePadding(dpToPx(figurePadding)); } public void setColor(int color) { this.color = color; if (figures != null) { for (Figure figure : figures) { figure.setColor(color); } } invalidate(); } public void setDuration(int duration) { this.duration = duration; if (figures != null) { setupAnimation(); } } private void initializeFigures() { if (!isInEditMode()) cancelAnimation(); int size = Math.min(width, height); double circumference = numberOfAngles * figurePadding; double distanceFromCenter = circumference / (Math.PI * 2); int radius = size / 2 - (int) (distanceFromCenter); double startAngle = 90 + (360.0 / numberOfAngles) / 2; List<PointF> angles = new ArrayList<>(); for (int i = 0; i < numberOfAngles; i++) { double angle = startAngle + i * (360.0 / numberOfAngles); angles.add(new PointF( (float) (center.x + radius * Math.cos(Math.toRadians(angle))), (float) (center.y + radius * Math.sin(Math.toRadians(angle)))) ); } figures = new ArrayList<>(); if (TYPE.KITE.equals(type)) { buildFiguresUsingKites(angles, startAngle, distanceFromCenter); } else { buildFiguresUsingTriangles(angles, startAngle, distanceFromCenter); } setupAnimation(); } private void buildFiguresUsingKites(List<PointF> angles, double startAngle, double distanceFromCenter) { for (int i = 0; i < angles.size(); i++) { double angle = startAngle + i * (360.0 / numberOfAngles); float newCenterX = (float) (distanceFromCenter * Math.cos(Math.toRadians(angle))); float newCenterY = (float) (distanceFromCenter * Math.sin(Math.toRadians(angle))); Path path = new Path(); path.moveTo(newCenterX + center.x, newCenterY + center.y); PointF point2; if (i <= 0) { point2 = getPointBetweenPoints(angles.get(i), angles.get(angles.size() - 1)); } else { point2 = getPointBetweenPoints(angles.get(i), angles.get(i - 1)); } path.lineTo(newCenterX + point2.x, newCenterY + point2.y); path.lineTo(newCenterX + angles.get(i).x, newCenterY + angles.get(i).y); PointF point3; if (i >= (angles.size() - 1)) { point3 = getPointBetweenPoints(angles.get(i), angles.get(0)); } else { point3 = getPointBetweenPoints(angles.get(i), angles.get(i + 1)); } path.lineTo(newCenterX + point3.x, newCenterY + point3.y); path.lineTo(newCenterX + center.x, newCenterY + center.y); path.close(); figures.add(new Figure(path, color, isInEditMode() ? (int) (25.0 + i * (230.0 / numberOfAngles)) : 0)); } } private void buildFiguresUsingTriangles(List<PointF> angles, double startAngle, double distanceFromCenter) { for (int i = 0; i < angles.size(); i++) { double angle1 = startAngle + i * (360.0 / numberOfAngles); double angle2 = startAngle + (i + 1) * (360.0 / numberOfAngles); double angle = (angle1 + angle2) / 2; float newCenterX = (float) (distanceFromCenter * Math.cos(Math.toRadians(angle))); float newCenterY = (float) (distanceFromCenter * Math.sin(Math.toRadians(angle))); Path path = new Path(); path.moveTo(newCenterX + center.x, newCenterY + center.y); path.lineTo(newCenterX + angles.get(i).x, newCenterY + angles.get(i).y); if (i >= (angles.size() - 1)) { path.lineTo(newCenterX + angles.get(0).x, newCenterY + angles.get(0).y); } else { path.lineTo(newCenterX + angles.get(i + 1).x, newCenterY + angles.get(i + 1).y); } path.lineTo(newCenterX + center.x, newCenterY + center.y); path.close(); figures.add(new Figure(path, color, isInEditMode() ? (int) (25.0 + i * (230.0 / numberOfAngles)) : 0)); } } private PointF getPointBetweenPoints(PointF point1, PointF point2) { float x = (point1.x + point2.x) / 2; float y = (point1.y + point2.y) / 2; return new PointF(x, y); } private void setupAnimation() { if (isInEditMode()) return; cancelAnimation(); animators = new ArrayList<>(); for (int i = 0; i < figures.size(); i++) { final Figure figure = figures.get(i); if (i != 0) { ValueAnimator startingFadeAnimator = ValueAnimator.ofInt((int) (i * (255.0 / numberOfAngles)), 0); startingFadeAnimator.setRepeatCount(1); startingFadeAnimator.setDuration((int) (i * (((double) duration) / numberOfAngles))); startingFadeAnimator.setInterpolator(new LinearInterpolator()); startingFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { figure.setAlpha((int) animation.getAnimatedValue()); invalidate(); } }); startingFadeAnimator.start(); animators.add(startingFadeAnimator); } ValueAnimator fadeAnimator = ValueAnimator.ofInt(255, 0); fadeAnimator.setRepeatCount(ValueAnimator.INFINITE); fadeAnimator.setDuration(duration); fadeAnimator.setInterpolator(new LinearInterpolator()); fadeAnimator.setStartDelay((int) (i * (((double) duration) / numberOfAngles))); fadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { figure.setAlpha((int) animation.getAnimatedValue()); invalidate(); } }); fadeAnimator.start(); animators.add(fadeAnimator); } } private void cancelAnimation() { if (isInEditMode()) return; if (animators != null) { for (ValueAnimator valueAnimator : animators) { valueAnimator.cancel(); } animators.clear(); animators = null; } } private int dpToPx(int dp) { DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); return (int) ((dp * displayMetrics.density) + 0.5); } }