package com.stanleycen.facebookanalytics.graph; /** * Created by scen on 9/1/13. */ import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; import com.stanleycen.facebookanalytics.Typefaces; import com.stanleycen.facebookanalytics.Util; import java.util.ArrayList; public class LineGraph extends View { private ArrayList<Line> lines = new ArrayList<Line>(); private Paint paint = new Paint(); private Paint ylabelPaint = new Paint(); private Paint xaxisLabelPaint = new Paint(); private float minY = 0, minX = 0; private float maxY = 0, maxX = 0; private boolean isMaxYUserSet = false; private int lineToFill = -1; private OnPointClickedListener listener; public Bitmap fullImage; private boolean shouldUpdate = false; private LabelFormatter xlabelFormatter; private LabelFormatter ylabelFormatter; private int numHorizontalGrids; private int numVerticalGrids; Rect tmpRect = new Rect(); private boolean shouldCacheToBitmap; public LineGraph(Context context) { this(context, null); } public LineGraph(Context context, AttributeSet attrs) { super(context, attrs); ylabelPaint.setColor(Color.BLACK); ylabelPaint.setAlpha(180); ylabelPaint.setTextSize(Util.dipToPixels(getContext(), 10)); ylabelPaint.setTypeface(Typefaces.get("sans-serif-thin", Typeface.NORMAL)); ylabelPaint.setAntiAlias(true); xaxisLabelPaint.setColor(Color.BLACK); xaxisLabelPaint.setTextSize(Util.dipToPixels(getContext(), 12)); xaxisLabelPaint.setTypeface(Typefaces.get("sans-serif-thin", Typeface.NORMAL)); xaxisLabelPaint.setAntiAlias(true); xaxisLabelPaint.setAlpha(200); xaxisLabelPaint.setTextAlign(Paint.Align.CENTER); } public void setTextColor(int color) { ylabelPaint.setColor(color); } public void setTextSize(float s) { ylabelPaint.setTextSize(s); } public void setMinY(float minY) { this.minY = minY; } public void update() { shouldUpdate = true; postInvalidate(); } public void removeAllLines() { while (lines.size() > 0) { lines.remove(0); } shouldUpdate = true; postInvalidate(); } public void addLine(Line line) { lines.add(line); shouldUpdate = true; postInvalidate(); } public ArrayList<Line> getLines() { return lines; } public void setLineToFill(int indexOfLine) { this.lineToFill = indexOfLine; shouldUpdate = true; postInvalidate(); } public int getLineToFill() { return lineToFill; } public void setLines(ArrayList<Line> lines) { this.lines = lines; } public Line getLine(int index) { return lines.get(index); } public int getSize() { return lines.size(); } public void setRangeY(float min, float max) { minY = min; maxY = max; isMaxYUserSet = true; } public float getMaxY() { if (isMaxYUserSet) { return maxY; } else { maxY = lines.get(0).getMaxY(); for (Line line : lines) { maxY = Math.max(maxY, line.getMaxY()); } return maxY; } } public float getMinY() { if (isMaxYUserSet) { return minY; } else { float min = lines.get(0).getMinY(); for (Line line : lines) { min = Math.min(min, line.getMinY()); } minY = min; return minY; } } public float getMaxX() { float max = lines.get(0).getMaxX(); for (Line line : lines) { max = Math.max(max, line.getMaxX()); } maxX = max; return maxX; } public float getMinX() { float min = lines.get(0).getPoint(0).getX(); for (Line line : lines) { min = Math.min(min, line.getMinX()); } minX = min; return minX; } public void onDraw(Canvas ca) { if (!shouldCacheToBitmap) { drawToCanvas(ca); return; } if (fullImage == null || shouldUpdate) { if (fullImage != null) { fullImage.recycle(); } fullImage = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(fullImage); drawToCanvas(canvas); Log.w("k", "caching"); shouldUpdate = false; } else { Log.w("k", "Drawn from cache"); } ca.drawBitmap(fullImage, 0, 0, null); } private void drawToCanvas(Canvas canvas) { if (lines == null || lines.size() == 0) return; // canvas.drawText("1400 messages", getWidth() / 2, getHeight() / 2, ylabelPaint); paint.reset(); boolean drawLegend = lines.size() > 1; final float ylabelFontSpacing = ylabelPaint.getFontSpacing() + ylabelPaint.descent() + Util.dipToPixels(getContext(), 1); final float xaxislabelFontSpacing = xaxisLabelPaint.descent() - xaxisLabelPaint.ascent();// xaxisLabelPaint.getFontSpacing(); final float paddingYLabelFromGrid = Util.dipToPixels(getContext(), 2); final float legendSize = drawLegend ? Util.dipToPixels(getContext(), 20) : 0; final float bottomPadding = xaxislabelFontSpacing + Util.dipToPixels(getContext(), 3); final float leftPadding = 0;//Util.dipToPixels(getContext(), 10); final float rightPadding = 0;//Util.dipToPixels(getContext(), 10); final float topPadding = legendSize; final float legendLeftPadding = Util.dipToPixels(getContext(), 10); final float circleRadius = Util.dipToPixels(getContext(), 5); final float lineWidth = Util.dipToPixels(getContext(), 2); final float legendLineWidth = Util.dipToPixels(getContext(), 3); final float legendLineLength = Util.dipToPixels(getContext(), 14); final float legendPaddingBetween = Util.dipToPixels(getContext(), 7); final float drawableHeight = getHeight() - bottomPadding - ylabelFontSpacing - legendSize; final float drawableWidth = getWidth() - leftPadding - rightPadding; final float xaxisY = getHeight() - bottomPadding; // Render grids paint.setStrokeWidth(1); paint.setAlpha(50); paint.setAntiAlias(true); int dataCount = lines.get(0).getPoints().size(); int ptDeltaPerLine = dataCount / (getNumVerticalGrids() - 1); // draw a vertical line per for (int i = 0; i < getNumVerticalGrids(); i++) { float vxpos = leftPadding + (float) i * ((float) ptDeltaPerLine * (drawableWidth / (float) (dataCount - 1))); if (i != 0) canvas.drawLine(vxpos, xaxisY, vxpos, xaxisY - drawableHeight, paint); String txt = getXlabelFormatter().format(i, getNumVerticalGrids(), getMinX(), getMaxX(), ptDeltaPerLine); if (txt != null) { if (i == 0) { xaxisLabelPaint.setTextAlign(Paint.Align.LEFT); } else if (i == getNumVerticalGrids() - 1) { float width = xaxisLabelPaint.measureText(txt); if (vxpos + (width / 2) > leftPadding + drawableWidth - 1) { xaxisLabelPaint.setTextAlign(Paint.Align.RIGHT); vxpos = leftPadding + drawableWidth - 1; } else xaxisLabelPaint.setTextAlign(Paint.Align.CENTER); } else { xaxisLabelPaint.setTextAlign(Paint.Align.CENTER); } canvas.drawText(txt, vxpos, xaxisY + (xaxislabelFontSpacing), xaxisLabelPaint); } } for (int i = 1; i < getNumHorizontalGrids(); i++) { float ypos = xaxisY - ((float) (i) * drawableHeight / (float) (getNumHorizontalGrids() - 1)); paint.setStrokeWidth(1); paint.setAlpha(50); canvas.drawLine(leftPadding, ypos, getWidth() - rightPadding, ypos, paint); } float maxY = getMaxY(); float minY = getMinY(); float maxX = getMaxX(); float minX = getMinX(); paint.setStrokeWidth(lineWidth); paint.setStrokeCap(Paint.Cap.ROUND); for (Line line : lines) { float oldx = 0, oldy = 0; int cnt = 0; int len = line.getPoints().size(); paint.setColor(line.getColor()); for (LinePoint lp : line.getPoints()) { float xpos = leftPadding + ((lp.getX() - minX) / (maxX - minX)) * drawableWidth; float ypos = xaxisY - ((lp.getY() - minY) / (maxY - minY)) * drawableHeight; if (cnt > 0) { canvas.drawLine(oldx, oldy, xpos, ypos, paint); } if (cnt != 0 && cnt != len - 1 && line.isShowingPoints()) { canvas.drawCircle(xpos, ypos, circleRadius, paint); } oldx = xpos; oldy = ypos; ++cnt; } } if (drawLegend) { float x = legendLeftPadding; float y = legendSize / 2f; paint.setStrokeWidth(legendLineWidth); paint.setStrokeCap(Paint.Cap.ROUND); paint.setTextSize(Util.dipToPixels(getContext(), 10)); for (Line line : lines) { paint.setColor(line.getColor()); canvas.drawLine(x, y, x + legendLineLength, y, paint); x += legendLineLength + legendPaddingBetween; paint.setColor(Color.BLACK); paint.setAlpha(200); canvas.drawText(line.getName(), x, y - (paint.descent() + paint.ascent()) / 2, paint); x += paint.measureText(line.getName()) + legendPaddingBetween; } } // to help with debugging formatting // { // Paint paint = new Paint(); // paint.setColor(Color.RED); // paint.setStrokeWidth(1); // paint.setAntiAlias(true); //// canvas.drawLine(0, 0, getWidth(), 0, paint); //// canvas.drawLine(0, getHeight(), getWidth(), getHeight(), paint); //// canvas.drawLine(getWidth(), 0, getWidth(), getHeight(), paint); //// canvas.drawLine(0, 0, 0, getHeight(), paint); //// paint.setColor(Color.BLUE); // canvas.drawLine(0, legendSize / 2f, getWidth(), legendSize / 2f, paint); // } for (int i = 0; i < getNumHorizontalGrids(); i++) { float ypos = xaxisY - ((float) (i) * drawableHeight / (float) (getNumHorizontalGrids() - 1)); String txt = getYlabelFormatter().format(i, getNumHorizontalGrids(), getMinY(), getMaxY(), ptDeltaPerLine); if (i == 0) { paint.setColor(Color.BLACK); paint.setStrokeWidth(2); paint.setAlpha(80); paint.setAntiAlias(true); canvas.drawLine(leftPadding, ypos, getWidth() - rightPadding, ypos, paint); } if (txt != null) canvas.drawText(txt, leftPadding, ypos - ylabelPaint.descent() - 1, ylabelPaint); } } @Override public boolean onTouchEvent(MotionEvent event) { // Point point = new Point(); // point.x = (int) event.getX(); // point.y = (int) event.getY(); // // int count = 0; // int lineCount = 0; // int pointCount; // // Region r = new Region(); // for (Line line : lines) { // pointCount = 0; // for (LinePoint p : line.getPoints()) { // // if (p.getPath() != null && p.getRegion() != null) { // r.setPath(p.getPath(), p.getRegion()); // if (r.contains(point.x, point.y) && event.getAction() == MotionEvent.ACTION_DOWN) { // indexSelected = count; // } else if (event.getAction() == MotionEvent.ACTION_UP) { // if (r.contains(point.x, point.y) && listener != null) { // listener.onClick(lineCount, pointCount); // } // indexSelected = -1; // } // } // // pointCount++; // count++; // } // lineCount++; // // } // // if (event.getAction() == MotionEvent.ACTION_DOWN || event.getAction() == MotionEvent.ACTION_UP) { // shouldUpdate = true; // postInvalidate(); // } return true; } public void setOnPointClickedListener(OnPointClickedListener listener) { this.listener = listener; } public LabelFormatter getXlabelFormatter() { return xlabelFormatter; } @Override public void invalidate() { if (fullImage != null) { fullImage.recycle(); fullImage = null; shouldUpdate = true; } super.invalidate(); } public void setXlabelFormatter(LabelFormatter xlabelFormatter) { this.xlabelFormatter = xlabelFormatter; } public int getNumHorizontalGrids() { return numHorizontalGrids; } public void setNumHorizontalGrids(int numHorizontalGrids) { this.numHorizontalGrids = numHorizontalGrids; } public LabelFormatter getYlabelFormatter() { return ylabelFormatter; } public void setYlabelFormatter(LabelFormatter ylabelFormatter) { this.ylabelFormatter = ylabelFormatter; } public int getNumVerticalGrids() { return numVerticalGrids; } public void setNumVerticalGrids(int numVerticalGrids) { this.numVerticalGrids = numVerticalGrids; } public boolean isShouldCacheToBitmap() { return shouldCacheToBitmap; } public void setShouldCacheToBitmap(boolean shouldCacheToBitmap) { this.shouldCacheToBitmap = shouldCacheToBitmap; } public interface OnPointClickedListener { abstract void onClick(int lineIndex, int pointIndex); } public abstract interface LabelFormatter { abstract String format(int idx, int tot, float min, float max, int ptsPerDelta); } }