package com.dacer.androidcharts; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathEffect; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.NinePatchDrawable; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; import java.util.ArrayList; import java.util.Collections; import java.util.NoSuchElementException; /** * Created by Dacer on 11/4/13. * Edited by Lee youngchan 21/1/14 * Edited by dector 30-Jun-2014 */ public class LineView extends View { public static final int SHOW_POPUPS_All = 1; public static final int SHOW_POPUPS_MAXMIN_ONLY = 2; public static final int SHOW_POPUPS_NONE = 3; private final int bottomTriangleHeight = 12; //Constants private final int popupTopPadding = ChartUtils.dip2px(getContext(), 2); private final int popupBottomMargin = ChartUtils.dip2px(getContext(), 5); private final int bottomTextTopMargin = ChartUtils.sp2px(getContext(), 5); private final int bottomLineLength = ChartUtils.sp2px(getContext(), 22); private final int DOT_INNER_CIR_RADIUS = ChartUtils.dip2px(getContext(), 2); private final int DOT_OUTER_CIR_RADIUS = ChartUtils.dip2px(getContext(), 5); private final int MIN_TOP_LINE_LENGTH = ChartUtils.dip2px(getContext(), 12); private final int MIN_VERTICAL_GRID_NUM = 4; private final int MIN_HORIZONTAL_GRID_NUM = 1; private final int BACKGROUND_LINE_COLOR = Color.parseColor("#EEEEEE"); private final int BOTTOM_TEXT_COLOR = Color.parseColor("#9B9A9B"); // onDraw optimisations private final Point tmpPoint = new Point(); private final Paint textPaint; public boolean showPopup = true; //drawBackground private boolean autoSetDataOfGird = true; private boolean autoSetGridWidth = true; // ↑this private int backgroundGridWidth = ChartUtils.dip2px(getContext(), 45); private int bottomTextDescent; private int bottomTextHeight = 0; private ArrayList<String> bottomTextList = new ArrayList<String>(); private Paint bottomTextPaint = new Paint(); //라인컬러 private String[] colorArray = {"#cce74c3c", "#cc2980b9", "#cccccccc"}; private ArrayList<Integer> dataList; private ArrayList<ArrayList<Integer>> dataLists; private int dataOfAGird = 10; //점선표시 private Boolean drawDotLine = false; private ArrayList<Dot> drawDotList = new ArrayList<Dot>(); private ArrayList<ArrayList<Dot>> drawDotLists = new ArrayList<ArrayList<Dot>>(); private Runnable animator = new Runnable() { @Override public void run() { boolean needNewFrame = false; for (ArrayList<Dot> data : drawDotLists) { for (Dot dot : data) { dot.update(); if (!dot.isAtRest()) { needNewFrame = true; } } } if (needNewFrame) { postDelayed(this, 25); } invalidate(); } }; private int mViewHeight; private Dot pointToSelect; //popup 컬러 private int[] popupColorArray = {R.drawable.popup_red, R.drawable.popup_blue, R.drawable.popup_green}; //popup private Paint popupTextPaint = new Paint(); private Dot selectedDot; private int showPopupType = SHOW_POPUPS_NONE; //-+-+- private int sideLineLength = ChartUtils.dip2px(getContext(), 45) / 3 * 2;// --+--+--+--+--+--+-- private int topLineLength = ChartUtils.dip2px(getContext(), 12); // | | ←this private ArrayList<Integer> xCoordinateList = new ArrayList<Integer>(); private ArrayList<Integer> yCoordinateList = new ArrayList<Integer>(); public LineView(Context context) { this(context, null); } public LineView(Context context, AttributeSet attrs) { super(context, attrs); textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); popupTextPaint.setAntiAlias(true); popupTextPaint.setColor(Color.WHITE); popupTextPaint.setTextSize(ChartUtils.sp2px(getContext(), 13)); popupTextPaint.setStrokeWidth(5); popupTextPaint.setTextAlign(Paint.Align.CENTER); bottomTextPaint.setAntiAlias(true); bottomTextPaint.setTextSize(ChartUtils.sp2px(getContext(), 12)); bottomTextPaint.setTextAlign(Paint.Align.CENTER); bottomTextPaint.setStyle(Paint.Style.FILL); bottomTextPaint.setColor(BOTTOM_TEXT_COLOR); } private void drawBackgroundLines(Canvas canvas) { Paint paint = new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(ChartUtils.dip2px(getContext(), 1f)); paint.setColor(BACKGROUND_LINE_COLOR); PathEffect effects = new DashPathEffect( new float[]{10, 5, 10, 5}, 1); //draw vertical lines for (int i = 0; i < xCoordinateList.size(); i++) { canvas.drawLine(xCoordinateList.get(i), 0, xCoordinateList.get(i), mViewHeight - bottomTextTopMargin - bottomTextHeight - bottomTextDescent, paint); } //draw dotted lines paint.setPathEffect(effects); Path dottedPath = new Path(); for (int i = 0; i < yCoordinateList.size(); i++) { if ((yCoordinateList.size() - 1 - i) % dataOfAGird == 0) { dottedPath.moveTo(0, yCoordinateList.get(i)); dottedPath.lineTo(getWidth(), yCoordinateList.get(i)); canvas.drawPath(dottedPath, paint); } } //draw bottom text if (bottomTextList != null) { for (int i = 0; i < bottomTextList.size(); i++) { canvas.drawText(bottomTextList.get(i), sideLineLength + backgroundGridWidth * i, mViewHeight - bottomTextDescent, bottomTextPaint); } } if (!drawDotLine) { //draw solid lines for (int i = 0; i < yCoordinateList.size(); i++) { if ((yCoordinateList.size() - 1 - i) % dataOfAGird == 0) { canvas.drawLine(0, yCoordinateList.get(i), getWidth(), yCoordinateList.get(i), paint); } } } } //도트그리기 private void drawDots(Canvas canvas) { Paint bigCirPaint = new Paint(); bigCirPaint.setAntiAlias(true); Paint smallCirPaint = new Paint(bigCirPaint); smallCirPaint.setColor(Color.parseColor("#FFFFFF")); if (drawDotLists != null && !drawDotLists.isEmpty()) { for (int k = 0; k < drawDotLists.size(); k++) { bigCirPaint.setColor(Color.parseColor(colorArray[k % 3])); for (Dot dot : drawDotLists.get(k)) { canvas.drawCircle(dot.x, dot.y, DOT_OUTER_CIR_RADIUS, bigCirPaint); canvas.drawCircle(dot.x, dot.y, DOT_INNER_CIR_RADIUS, smallCirPaint); } } } } //선그리기 private void drawLines(Canvas canvas) { Paint linePaint = new Paint(); linePaint.setAntiAlias(true); linePaint.setStrokeWidth(ChartUtils.dip2px(getContext(), 2)); for (int k = 0; k < drawDotLists.size(); k++) { linePaint.setColor(Color.parseColor(colorArray[k % 3])); for (int i = 0; i < drawDotLists.get(k).size() - 1; i++) { canvas.drawLine(drawDotLists.get(k).get(i).x, drawDotLists.get(k).get(i).y, drawDotLists.get(k).get(i + 1).x, drawDotLists.get(k).get(i + 1).y, linePaint); } } } /** * @param canvas The canvas you need to draw on. * @param point The Point consists of the x y coordinates from left bottom to right top. * Like is * <p/> * 3 * 2 * 1 * 0 1 2 3 4 5 */ private void drawPopup(Canvas canvas, String num, Point point, int PopupColor) { boolean singularNum = (num.length() == 1); int sidePadding = ChartUtils.dip2px(getContext(), singularNum ? 8 : 5); int x = point.x; int y = point.y - ChartUtils.dip2px(getContext(), 5); Rect popupTextRect = new Rect(); popupTextPaint.getTextBounds(num, 0, num.length(), popupTextRect); Rect r = new Rect(x - popupTextRect.width() / 2 - sidePadding, y - popupTextRect.height() - bottomTriangleHeight - popupTopPadding * 2 - popupBottomMargin, x + popupTextRect.width() / 2 + sidePadding, y + popupTopPadding - popupBottomMargin); NinePatchDrawable popup = (NinePatchDrawable) getResources().getDrawable(PopupColor); popup.setBounds(r); popup.draw(canvas); canvas.drawText(num, x, y - bottomTriangleHeight - popupBottomMargin, popupTextPaint); } private Dot findPointAt(int x, int y) { if (drawDotLists.isEmpty()) { return null; } final int width = backgroundGridWidth / 2; final Region r = new Region(); for (ArrayList<Dot> data : drawDotLists) { for (Dot dot : data) { final int pointX = dot.x; final int pointY = dot.y; r.set(pointX - width, pointY - width, pointX + width, pointY + width); if (r.contains(x, y)) { return dot; } } } return null; } private int getHorizontalGridNum() { int horizontalGridNum = bottomTextList.size() - 1; if (horizontalGridNum < MIN_HORIZONTAL_GRID_NUM) { horizontalGridNum = MIN_HORIZONTAL_GRID_NUM; } return horizontalGridNum; } private int getMeasurement(int measureSpec, int preferred) { int specSize = MeasureSpec.getSize(measureSpec); int measurement; switch (MeasureSpec.getMode(measureSpec)) { case MeasureSpec.EXACTLY: measurement = specSize; break; case MeasureSpec.AT_MOST: measurement = Math.min(preferred, specSize); break; default: measurement = preferred; break; } return measurement; } private int getPopupHeight() { Rect popupTextRect = new Rect(); popupTextPaint.getTextBounds("9", 0, 1, popupTextRect); Rect r = new Rect(-popupTextRect.width() / 2, -popupTextRect.height() - bottomTriangleHeight - popupTopPadding * 2 - popupBottomMargin, +popupTextRect.width() / 2, +popupTopPadding - popupBottomMargin); return r.height(); } private int getVerticalGridlNum() { int verticalGridNum = MIN_VERTICAL_GRID_NUM; if (dataLists != null && !dataLists.isEmpty()) { for (ArrayList<Integer> list : dataLists) { for (Integer integer : list) { if (verticalGridNum < (integer + 1)) { verticalGridNum = integer + 1; } } } } return verticalGridNum; } private int measureHeight(int measureSpec) { int preferred = 0; return getMeasurement(measureSpec, preferred); } private int measureWidth(int measureSpec) { int horizontalGridNum = getHorizontalGridNum(); int preferred = backgroundGridWidth * horizontalGridNum + sideLineLength * 2; return getMeasurement(measureSpec, preferred); } @Override protected void onDraw(Canvas canvas) { drawBackgroundLines(canvas); drawLines(canvas); drawDots(canvas); try { for (int k = 0; k < drawDotLists.size(); k++) { int MaxValue = Collections.max(dataLists.get(k)); int MinValue = Collections.min(dataLists.get(k)); for (Dot d : drawDotLists.get(k)) { if (showPopupType == SHOW_POPUPS_All) drawPopup(canvas, String.valueOf(d.data), d.setupPoint(tmpPoint), popupColorArray[k % 3]); else if (showPopupType == SHOW_POPUPS_MAXMIN_ONLY) { if (d.data == MaxValue) drawPopup(canvas, String.valueOf(d.data), d.setupPoint(tmpPoint), popupColorArray[k % 3]); if (d.data == MinValue) drawPopup(canvas, String.valueOf(d.data), d.setupPoint(tmpPoint), popupColorArray[k % 3]); } } } // 선택한 dot 만 popup 이 뜨게 한다. if (showPopup && selectedDot != null) { drawPopup(canvas, String.valueOf(selectedDot.data), selectedDot.setupPoint(tmpPoint), popupColorArray[selectedDot.linenumber % 3]); } } catch (NoSuchElementException nsu) { Log.w(ChartUtils.LOG_TAG, "Skipping draw"); textPaint.setColor(Color.GRAY); textPaint.setTextSize((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 18, getResources().getDisplayMetrics())); textPaint.setTextAlign(Paint.Align.LEFT); Paint.FontMetrics metric = textPaint.getFontMetrics(); int textHeight = (int) Math.ceil(metric.descent - metric.ascent); int y = (int) (textHeight - metric.descent); canvas.drawText("NO DATA", 0, y, textPaint); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int mViewWidth = measureWidth(widthMeasureSpec); mViewHeight = measureHeight(heightMeasureSpec); // mViewHeight = MeasureSpec.getSize(measureSpec); refreshAfterDataChanged(); setMeasuredDimension(mViewWidth, mViewHeight); } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { pointToSelect = findPointAt((int) event.getX(), (int) event.getY()); } else if (event.getAction() == MotionEvent.ACTION_UP) { if (pointToSelect != null) { selectedDot = pointToSelect; pointToSelect = null; postInvalidate(); } } return true; } private void refreshAfterDataChanged() { int verticalGridNum = getVerticalGridlNum(); refreshTopLineLength(verticalGridNum); refreshYCoordinateList(verticalGridNum); refreshDrawDotList(verticalGridNum); } private void refreshDrawDotList(int verticalGridNum) { if (dataLists != null && !dataLists.isEmpty()) { if (drawDotLists.size() == 0) { for (int k = 0; k < dataLists.size(); k++) { drawDotLists.add(new ArrayList<LineView.Dot>()); } } for (int k = 0; k < dataLists.size(); k++) { int drawDotSize = drawDotLists.get(k).isEmpty() ? 0 : drawDotLists.get(k).size(); for (int i = 0; i < dataLists.get(k).size(); i++) { int x = xCoordinateList.get(i); int y; try { y = yCoordinateList.get(verticalGridNum - dataLists.get(k).get(i)); } catch (IndexOutOfBoundsException boh) { y = 0; } if (i > drawDotSize - 1) { //도트리스트를 추가한다. drawDotLists.get(k).add(new Dot(x, 0, x, y, dataLists.get(k).get(i), k)); } else { //도트리스트에 타겟을 설정한다. drawDotLists.get(k).set(i, drawDotLists.get(k).get(i).setTargetData(x, y, dataLists.get(k).get(i), k)); } } int temp = drawDotLists.get(k).size() - dataLists.get(k).size(); for (int i = 0; i < temp; i++) { drawDotLists.get(k).remove(drawDotLists.get(k).size() - 1); } } } removeCallbacks(animator); post(animator); } private void refreshTopLineLength(int verticalGridNum) { // For prevent popup can't be completely showed when backgroundGridHeight is too small. // But this code not so good. if ((mViewHeight - topLineLength - bottomTextHeight - bottomTextTopMargin) / (verticalGridNum + 2) < getPopupHeight()) { topLineLength = getPopupHeight() + DOT_OUTER_CIR_RADIUS + DOT_INNER_CIR_RADIUS + 2; } else { topLineLength = MIN_TOP_LINE_LENGTH; } } private void refreshXCoordinateList(int horizontalGridNum) { xCoordinateList.clear(); for (int i = 0; i < (horizontalGridNum + 1); i++) { xCoordinateList.add(sideLineLength + backgroundGridWidth * i); } } private void refreshYCoordinateList(int verticalGridNum) { yCoordinateList.clear(); for (int i = 0; i < (verticalGridNum + 1); i++) { yCoordinateList.add(topLineLength + ((mViewHeight - topLineLength - bottomTextHeight - bottomTextTopMargin - bottomLineLength - bottomTextDescent) * i / (verticalGridNum))); } } /** * dataList will be reset when called is method. * * @param bottomTextList The String ArrayList in the bottom. */ public void setBottomTextList(ArrayList<String> bottomTextList) { this.dataList = null; this.bottomTextList = bottomTextList; Rect r = new Rect(); int longestWidth = 0; String longestStr = ""; bottomTextDescent = 0; for (String s : bottomTextList) { bottomTextPaint.getTextBounds(s, 0, s.length(), r); if (bottomTextHeight < r.height()) { bottomTextHeight = r.height(); } if (autoSetGridWidth && (longestWidth < r.width())) { longestWidth = r.width(); longestStr = s; } if (bottomTextDescent < (Math.abs(r.bottom))) { bottomTextDescent = Math.abs(r.bottom); } } if (autoSetGridWidth) { if (backgroundGridWidth < longestWidth) { backgroundGridWidth = longestWidth + (int) bottomTextPaint.measureText(longestStr, 0, 1); } if (sideLineLength < longestWidth / 2) { sideLineLength = longestWidth / 2; } } refreshXCoordinateList(getHorizontalGridNum()); } /** * @param dataLists The Integer ArrayLists for showing, * dataList.size() must < bottomTextList.size() */ public void setDataList(ArrayList<ArrayList<Integer>> dataLists) { selectedDot = null; this.dataLists = dataLists; for (ArrayList<Integer> list : dataLists) { if (list.size() > bottomTextList.size()) { throw new RuntimeException("dacer.LineView error:" + " dataList.size() > bottomTextList.size() !!!"); } } int biggestData = 0; for (ArrayList<Integer> list : dataLists) { if (autoSetDataOfGird) { for (Integer i : list) { if (biggestData < i) { biggestData = i; } } } dataOfAGird = 1; while (biggestData / 10 > dataOfAGird) { dataOfAGird *= 10; } } refreshAfterDataChanged(); showPopup = true; setMinimumWidth(0); // It can help the LineView reset the Width, // I don't know the better way.. postInvalidate(); } public void setDrawDotLine(Boolean drawDotLine) { this.drawDotLine = drawDotLine; } public void setShowPopup(int popupType) { this.showPopupType = popupType; } class Dot { int x; int y; int data; int targetX; int targetY; int linenumber; int velocity = ChartUtils.dip2px(getContext(), 18); Dot(int x, int y, int targetX, int targetY, Integer data, int linenumber) { this.x = x; this.y = y; this.linenumber = linenumber; setTargetData(targetX, targetY, data, linenumber); } boolean isAtRest() { return (x == targetX) && (y == targetY); } Dot setTargetData(int targetX, int targetY, Integer data, int linenumber) { this.targetX = targetX; this.targetY = targetY; this.data = data; this.linenumber = linenumber; return this; } Point setupPoint(Point point) { point.set(x, y); return point; } void update() { x = updateSelf(x, targetX, velocity); y = updateSelf(y, targetY, velocity); } private int updateSelf(int origin, int target, int velocity) { if (origin < target) { origin += velocity; } else if (origin > target) { origin -= velocity; } if (Math.abs(target - origin) < velocity) { origin = target; } return origin; } } }