package com.code44.finance.graphs.pie; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View; import com.code44.finance.R; import com.code44.finance.ui.common.ViewBackgroundTheme; import java.util.Arrays; import java.util.List; public class PieChartView extends View { private final RectF rect = new RectF(); private final RectF outlineRect = new RectF(); private final RectF inlineRect = new RectF(); private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint outlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint inlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Type type; private SizeBasedOn sizeBasedOn; private float donutWidthRatio; private int emptyColor; private PieChartData pieChartData; public PieChartView(Context context) { this(context, null); } public PieChartView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public PieChartView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // Init outlinePaint.setStyle(Paint.Style.STROKE); inlinePaint.setStyle(Paint.Style.STROKE); applyStyle(context, attrs); if (isInEditMode()) { setPieChartData(PieChartData.builder().setValues(Arrays.asList(new PieChartValue(15, 0xffe51c23), new PieChartValue(25, 0xff5677fc))).build()); } else { setPieChartData(null); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); switch (sizeBasedOn) { case Min: { final int size = Math.min(getMeasuredWidth(), getMeasuredHeight()); setMeasuredDimension(size, size); break; } case Width: { final int size = getMeasuredWidth(); setMeasuredDimension(size, size); break; } case Height: { final int size = getMeasuredHeight(); setMeasuredDimension(size, size); break; } } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); final int contentWidth = w - getPaddingLeft() - getPaddingRight(); final int contentHeight = h - getPaddingTop() - getPaddingBottom(); int size = Math.min(contentWidth, contentHeight); float leftOffset = getPaddingLeft() + Math.max((contentWidth - contentHeight) / 2.0f, 0.0f); float topOffset = getPaddingTop() + Math.max((contentHeight - contentWidth) / 2.0f, 0.0f); if (hasOutline()) { size -= outlinePaint.getStrokeWidth() * 2; leftOffset += outlinePaint.getStrokeWidth(); topOffset += outlinePaint.getStrokeWidth(); } if (type == Type.DONUT) { final float strokeWidth = size / 2 * donutWidthRatio; final float halfStrokeSize = strokeWidth / 2.0f; paint.setStrokeWidth(strokeWidth); size -= strokeWidth; leftOffset += halfStrokeSize; topOffset += halfStrokeSize; } rect.set(leftOffset, topOffset, leftOffset + size, topOffset + size); if (hasOutline()) { final float outlineSize = size + outlinePaint.getStrokeWidth() + (type == Type.DONUT ? paint.getStrokeWidth() : 0); final float outlineLeftOffset = leftOffset - (outlinePaint.getStrokeWidth() / 2.0f) - (type == Type.DONUT ? paint.getStrokeWidth() / 2.0f : 0); final float outlineTopOffset = topOffset - (outlinePaint.getStrokeWidth() / 2.0f) - (type == Type.DONUT ? paint.getStrokeWidth() / 2.0f : 0); outlineRect.set(outlineLeftOffset, outlineTopOffset, outlineLeftOffset + outlineSize, outlineTopOffset + outlineSize); } if (hasInline()) { final float inlineSize = size - inlinePaint.getStrokeWidth() - paint.getStrokeWidth(); final float inlineLeftOffset = leftOffset + (paint.getStrokeWidth() / 2.0f) + (inlinePaint.getStrokeWidth() / 2.0f); final float inlineTopOffset = topOffset + (paint.getStrokeWidth() / 2.0f) + (inlinePaint.getStrokeWidth() / 2.0f); inlineRect.set(inlineLeftOffset, inlineTopOffset, inlineLeftOffset + inlineSize, inlineTopOffset + inlineSize); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final List<PieChartValue> values = pieChartData.getValues(); final long total = pieChartData.getTotalValue(); final boolean useCenter = paint.getStyle() == Paint.Style.FILL; float startAngle = -90.0f; if (values.size() > 0) { for (PieChartValue value : values) { final float sweepAngle = 360.0f * value.getValue() / total; paint.setColor(value.getColor()); canvas.drawArc(rect, startAngle, sweepAngle + 1, useCenter, paint); startAngle += sweepAngle; } } else { paint.setColor(emptyColor); canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2.0f, paint); } if (hasOutline()) { canvas.drawCircle(outlineRect.centerX(), outlineRect.centerY(), outlineRect.width() / 2.0f, outlinePaint); } if (hasInline()) { canvas.drawCircle(inlineRect.centerX(), inlineRect.centerY(), inlineRect.width() / 2.0f, inlinePaint); } } public void setType(Type type) { if (type == null) { throw new NullPointerException("Type cannot be null."); } this.type = type; switch (type) { case PIE: paint.setStyle(Paint.Style.FILL); break; case DONUT: paint.setStyle(Paint.Style.STROKE); break; } requestLayout(); } public void setSizeBasedOn(SizeBasedOn sizeBasedOn) { this.sizeBasedOn = sizeBasedOn; requestLayout(); } public void setDonutWidthRatio(float donutWidthRatio) { if (Float.compare(donutWidthRatio, 0) < 0 || Float.compare(donutWidthRatio, 1) > 0) { throw new IllegalArgumentException("Donut width ratio must be [0, 1]."); } this.donutWidthRatio = donutWidthRatio; requestLayout(); } public void setOutlineWidth(float outlineWidth) { if (Float.compare(outlineWidth, 0) < 0) { throw new IllegalArgumentException("Outline width must be >= 0."); } outlinePaint.setStrokeWidth(outlineWidth); requestLayout(); } public void setOutlineColor(int outlineColor) { outlinePaint.setColor(outlineColor); invalidate(); } public void setInlineWidth(float inlineWidth) { if (Float.compare(inlineWidth, 0) < 0) { throw new IllegalArgumentException("Inline width must be >= 0."); } inlinePaint.setStrokeWidth(inlineWidth); requestLayout(); } public void setInlineColor(int inlineColor) { inlinePaint.setColor(inlineColor); invalidate(); } public void setPieChartData(PieChartData pieChartData) { if (pieChartData == null) { this.pieChartData = PieChartData.builder().build(); } else { this.pieChartData = pieChartData; } requestLayout(); } public void setEmptyColor(int emptyColor) { this.emptyColor = emptyColor; invalidate(); } public void setViewBackgroundTheme(ViewBackgroundTheme viewBackgroundTheme) { final int color = getResources().getColor(viewBackgroundTheme == ViewBackgroundTheme.Light ? R.color.dark : R.color.bg_primary); setOutlineColor(color); setInlineColor(color); setEmptyColor(color); invalidate(); } private boolean hasOutline() { return Float.compare(outlinePaint.getStrokeWidth(), 0) > 0; } private boolean hasInline() { return type == Type.DONUT && Float.compare(inlinePaint.getStrokeWidth(), 0) > 0; } private void applyStyle(Context context, AttributeSet attrs) { final TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PieChartView, 0, 0); try { final ViewBackgroundTheme viewBackgroundTheme = ViewBackgroundTheme.from(a.getInteger(R.styleable.PieChartView_viewBackgroundTheme, 0)); final int color = getResources().getColor(viewBackgroundTheme == ViewBackgroundTheme.Light ? R.color.dark : R.color.bg_primary); setType(Type.from(a.getInteger(R.styleable.PieChartView_type, 0))); setSizeBasedOn(SizeBasedOn.from(a.getInteger(R.styleable.PieChartView_sizeBasedOn, 0))); setDonutWidthRatio(a.getFloat(R.styleable.PieChartView_donutWidthRatio, 0.3f)); setOutlineWidth(a.getDimension(R.styleable.PieChartView_outlineWidth, getResources().getDimension(R.dimen.divider))); setOutlineColor(a.getColor(R.styleable.PieChartView_outlineColor, color)); setInlineWidth(a.getDimension(R.styleable.PieChartView_inlineWidth, outlinePaint.getStrokeWidth())); setInlineColor(a.getColor(R.styleable.PieChartView_inlineColor, color)); setEmptyColor(a.getColor(R.styleable.PieChartView_emptyColor, color)); } finally { a.recycle(); } } public static enum Type { PIE, DONUT; public static Type from(int value) { if (value == 1) { return DONUT; } return PIE; } } public static enum SizeBasedOn { Default, Min, Width, Height; public static SizeBasedOn from(int value) { switch (value) { case 1: return Min; case 2: return Width; case 3: return Height; default: return Default; } } } }