package com.code44.finance.graphs.line; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PointF; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import com.code44.finance.R; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; public class LineGraphView extends View { private final List<LineGraphData> lineGraphDataList = new ArrayList<>(); private final Map<LineGraphData, LineData> lineDataCache = new HashMap<>(); private double maxValue; private double minValue; public LineGraphView(Context context) { this(context, null); } public LineGraphView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public LineGraphView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); if (isInEditMode()) { final LineGraphData.Builder builder = new LineGraphData.Builder() .setColor(getResources().getColor(R.color.text_negative)) .setLineWidth(getResources().getDimension(R.dimen.divider)) .setSmooth(true) .setUseGlobalMinMax(true); final Random random = new Random(); for (int i = 0; i < 30; i++) { builder.addValue(new LineGraphValue(random.nextFloat() * 1000)); } setLineGraphData(builder.build()); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); invalidateGraphs(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (LineGraphData lineGraphData : lineGraphDataList) { // Check if we have something to draw final boolean hasItems = lineGraphData.size() > 0; final boolean hasLineWidth = Float.compare(lineGraphData.getLineWidth(), 0) > 0; final boolean hasDividers = lineGraphData.getDividerDrawable() != null; final boolean hasSomethingToDraw = hasItems && (hasLineWidth || hasDividers); if (!hasSomethingToDraw) { continue; } final LineData lineData = lineDataCache.get(lineGraphData); // Draw line if we have line width if (hasLineWidth) { canvas.drawPath(lineData.getPath(), lineData.getPaint()); } // Draw dividers if we have them if (hasDividers) { final Drawable dividerDrawable = lineGraphData.getDividerDrawable(); final int drawableHalfWidth = dividerDrawable.getIntrinsicWidth() / 2; final int drawableHalfHeight = dividerDrawable.getIntrinsicHeight() / 2; for (PointF point : lineData.getPoints()) { if (point == null) { continue; } dividerDrawable.setBounds((int) point.x - drawableHalfWidth, (int) point.y - drawableHalfHeight, (int) point.x + drawableHalfWidth, (int) point.y + drawableHalfHeight); dividerDrawable.draw(canvas); } } } } public void setLineGraphData(LineGraphData... lineGraphData) { this.lineGraphDataList.clear(); addLineGraphData(lineGraphData); } public void addLineGraphData(LineGraphData... lineGraphData) { if (lineGraphData != null && lineGraphData.length > 0) { this.lineGraphDataList.addAll(Arrays.asList(lineGraphData)); } invalidateGraphs(); } private void invalidateGraphs() { lineDataCache.clear(); if (getMeasuredHeight() == 0 || getMeasuredWidth() == 0) { invalidate(); return; } invalidateMinMax(); final GraphsInfo graphsInfo = invalidateLineDataCache(0, 0); // Check if paths are out of bounds final float[] topBottomDelta = getTopBottomOffsetToFitGraphs(graphsInfo); final float topDelta = topBottomDelta[0]; final float bottomDelta = topBottomDelta[1]; if (Float.compare(topDelta, 0) > 0 || Float.compare(bottomDelta, 0) > 0) { lineDataCache.clear(); invalidateLineDataCache(topDelta, bottomDelta); } invalidate(); } private void invalidateMinMax() { minValue = Double.MAX_VALUE; maxValue = Double.MIN_VALUE; for (LineGraphData lineGraphData : lineGraphDataList) { if (!lineGraphData.isUsingGlobalMinMax()) { continue; } if (Double.compare(lineGraphData.getMinValue(), minValue) < 0) { minValue = lineGraphData.getMinValue(); } if (Double.compare(lineGraphData.getMaxValue(), maxValue) > 0) { maxValue = lineGraphData.getMaxValue(); } } } private GraphsInfo invalidateLineDataCache(float topDelta, float bottomDelta) { final GraphsInfo graphsInfo = getGraphsInfo(topDelta, bottomDelta); for (LineGraphData lineGraphData : lineGraphDataList) { lineDataCache.put(lineGraphData, getLineData(lineGraphData, graphsInfo)); } return graphsInfo; } private float[] getTopBottomOffsetToFitGraphs(GraphsInfo graphsInfo) { final RectF bounds = new RectF(); float topDelta = 0; float bottomDelta = 0; for (LineData line : lineDataCache.values()) { line.getPath().computeBounds(bounds, true); final float lineHalfWidth = line.getPaint().getStrokeWidth() / 2; final float currentTopDelta = Math.max(0, graphsInfo.getBounds().top - bounds.top + lineHalfWidth); final float currentBottomDelta = Math.max(0, bounds.bottom - graphsInfo.getBounds().bottom + lineHalfWidth); topDelta = Math.max(currentTopDelta, topDelta); bottomDelta = Math.max(currentBottomDelta, bottomDelta); } return new float[]{topDelta, bottomDelta}; } private GraphsInfo getGraphsInfo(float extraTopSpace, float extraBottomSpace) { float paddingHorizontal = 0; float paddingVertical = 0; for (LineGraphData lineGraphData : lineGraphDataList) { final Drawable dividerDrawable = lineGraphData.getDividerDrawable(); paddingHorizontal = Math.max(paddingHorizontal, Math.max(dividerDrawable != null ? dividerDrawable.getIntrinsicWidth() : 0, lineGraphData.getLineWidth())); paddingVertical = Math.max(paddingVertical, Math.max(dividerDrawable != null ? dividerDrawable.getIntrinsicHeight() : 0, lineGraphData.getLineWidth())); } paddingHorizontal /= 2; paddingVertical /= 2; final RectF bounds = new RectF(paddingHorizontal, paddingVertical + extraTopSpace, getMeasuredWidth() - paddingHorizontal, getMeasuredHeight() - paddingVertical - extraBottomSpace); return new GraphsInfo(bounds); } private LineData getLineData(LineGraphData lineGraphData, GraphsInfo graphsInfo) { final List<PointF> points = getPoints(lineGraphData, graphsInfo); final PathMaker pathMaker = lineGraphData.isSmooth() ? new SmoothPathMaker() : new SharpPathMaker(); final Path path = pathMaker.makePath(points); final Paint paint = createLinePaint(lineGraphData); return new LineData(points, path, paint); } private List<PointF> getPoints(LineGraphData lineGraphData, GraphsInfo graphsInfo) { final List<PointF> points = new ArrayList<>(); for (int i = 0, size = lineGraphData.size(); i < size; i++) { final LineGraphValue value = lineGraphData.getValue(i); if (value == null) { points.add(null); } else { final PointF point = getPoint(i, lineGraphData, graphsInfo, value); points.add(point); } } return points; } private PointF getPoint(int index, LineGraphData lineGraphData, GraphsInfo graphsInfo, LineGraphValue value) { if (value == null) { return null; } final float x; if (index == 0) { x = graphsInfo.getBounds().left; } else if (index == lineGraphData.size() - 1) { x = graphsInfo.getBounds().right; } else { final float step = graphsInfo.getBounds().width() / (lineGraphData.size() - 1); x = graphsInfo.getBounds().left + (step * index); } final double minValue; final double maxValue; if (lineGraphData.isUsingGlobalMinMax()) { minValue = this.minValue; maxValue = this.maxValue; } else { minValue = lineGraphData.getMinValue(); maxValue = lineGraphData.getMaxValue(); } final float ratio; if (Double.compare(minValue, maxValue) == 0) { ratio = 0.5f; } else { ratio = (float) ((value.getValue() - minValue) / (maxValue - minValue)); } final float height = graphsInfo.getBounds().height(); final float y = graphsInfo.getBounds().bottom - (height * ratio); return new PointF(x, y); } private Paint createLinePaint(LineGraphData lineGraphData) { final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(lineGraphData.getColor()); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(lineGraphData.getLineWidth()); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStrokeJoin(Paint.Join.ROUND); return paint; } private static class LineData { private final List<PointF> points; private final Path path; private final Paint paint; private LineData(List<PointF> points, Path path, Paint paint) { this.points = points; this.path = path; this.paint = paint; } public List<PointF> getPoints() { return points; } public Path getPath() { return path; } public Paint getPaint() { return paint; } } private static class GraphsInfo { final RectF bounds; private GraphsInfo(RectF bounds) { this.bounds = bounds; } public RectF getBounds() { return bounds; } } }