Android自定义折线图,可设置基准线,不同点颜色
2017-08-16 11:11
309 查看
最近项目中需要用到折线图,发现了一款比较好用的折线图hellocharts,做了炫酷的动画以及折线图,柱状图,混合图都有涉及到,非常的棒,在此推荐一下。
但是使用过程中碰到这样的需求:
①:不同数据点可以显示不同的颜色
②:还要有基准线
这可把我难坏了,看了下hellocharts的源码后,没有看到提供设置不同数据点的颜色以及基准线的接口(可能也是我看得不仔细吧)算了,不管这些了,项目马上要上线了,得赶紧做出来,主要的是去实现功能满足需求,当然了,满足了需求之后还得调整界面以及样式。话不多说,我们开始吧~ Σ(っ°Д°;)っ
来看看我做的效果吧:
接下来结合代码为大家分析一下大致流程(今天我就不直接放代码了,我要一步一步的介绍一下我的流程 ヾ(゚∀゚ゞ) )(可爱的源码君依然是在文章最后):
一、首先将坐标轴X轴Y轴绘制出来
①、大部分的Y轴都用来展示数字类型的数据,X轴可数字类型也可文字类型,这就好办了,定义两个List,一个存Y轴的数字,一个存X轴的字符(数字的话可以转成字符串)
定义一个AxisValue类,用来存放轴上单个点的数据:
②、关于轴相关的类已经建好,接下来就是绘制轴上的数据。绘制Y轴的数据需要注意一点,由于数据的宽度不一样,我们需要保证数据在Y轴能够右对齐,要先遍历每一个数据,然后测量其宽度,取出最大的宽度,为了保证高度上能够均分,需要预先算出X轴上文字的高度,这样就能保证X轴和Y轴上下都能留出一定的距离给数据展示。
设置Y轴的数据:
设置X轴上的数据,在这个地方会出现新的问题,为保证文字能够垂直居中,所以我们需要减去测量文字的高度的一半,但是!!!文字的高度不一样的话,就不会显示在中间。比如文字 一 和 日。咦,我为什么要说日。ノ´▽`)ノ♪ 后来我想到一种做法,在底部放一个矩形,每个矩形的高度一样,然后将文字设置在矩形的中间。并且,宽度还要减去每个文字的宽度的一半。简直完美!
二、坐标轴已经绘制好了,我们开始绘制基准线,定义了一个BaseLine类,这个类里主要放了value和color,就不展示出来了,画线那里用到了DashPathEffect类。这个类的作用就是将Path的线段虚线化。构造函数为DashPathEffect(float[] intervals, float offset),其中intervals为虚线的ON和OFF数组,该数组的length必须大于等于2,offset为绘制时的偏移量。
三、基准线绘制好了,接下来就是绘制点和线了。这里也得注意,先要绘制线,再绘制点,因为点的颜色会不一样,再绘制线的话会盖在点的上面。我的做法是先将每个点在屏幕的坐标位置根据点的X轴Y轴的值计算出来。
①、首先我们需要一个PointValue类来存储点的位置,这里没什么好说的,看名字就能明白:
②、其次我们需要把每个点串起来的Line类,这里开放了几个属性,如,线的颜色,点的颜色,宽度,是否是平滑曲线还是直线,是否显示当前值等,当然了你也可以拿过去定制你需要的属性:
③、然后我们需要一个类用来存放我们所有的数据,这样在代码中我们只需要几行代码就能显示出折线图,
④、最后再来根据线的个数去绘制点和线,通过Y的最大值来计算点的Y轴的位置
最关键的绘制线的地方在这里,由于比较复杂,我就单独封装了一个方法,记得绘制之前将path和paint给reset一下。(这里可得注意点,一不留心就会出问题)
最后再放上完整的LineChartView吧,好方便大家对应着去看:
好了,基本上主要的内容就是以上的这些了。来看看在界面中如何使用:
好啦,基本上就是这些了,各位老铁觉得不错的话不妨点个赞? 罒ω罒
源码点我点我
但是使用过程中碰到这样的需求:
①:不同数据点可以显示不同的颜色
②:还要有基准线
这可把我难坏了,看了下hellocharts的源码后,没有看到提供设置不同数据点的颜色以及基准线的接口(可能也是我看得不仔细吧)算了,不管这些了,项目马上要上线了,得赶紧做出来,主要的是去实现功能满足需求,当然了,满足了需求之后还得调整界面以及样式。话不多说,我们开始吧~ Σ(っ°Д°;)っ
来看看我做的效果吧:
接下来结合代码为大家分析一下大致流程(今天我就不直接放代码了,我要一步一步的介绍一下我的流程 ヾ(゚∀゚ゞ) )(可爱的源码君依然是在文章最后):
一、首先将坐标轴X轴Y轴绘制出来
①、大部分的Y轴都用来展示数字类型的数据,X轴可数字类型也可文字类型,这就好办了,定义两个List,一个存Y轴的数字,一个存X轴的字符(数字的话可以转成字符串)
定义一个AxisValue类,用来存放轴上单个点的数据:
/** * Created by fySpring * Date : 2017/8/10 * To do :x或y轴上的值 */ public class AxisValue { private float value; //值 private String label; //文字 public AxisValue(float value) { setValue(value); } public AxisValue(float value, String label) { this.value = value; this.label = label; } public AxisValue(AxisValue axisValue) { this.value = axisValue.value; this.label = axisValue.label; } public float getValue() { return value; } public AxisValue setValue(float value) { this.value = value; return this; } public AxisValue setLabel(String label) { this.label = label; return this; } public String getLabel() { return label; } }定义一个Axis类,用来存放坐标轴的数据list,即我们的轴。在这个类中我们设置了一些属性,包括是否显示刻度线、刻度线的颜色、轴上文字的颜色、文字的大小、以及坐标值的List。这里得说一下spacingNum,当X轴上的坐标很多的情况下,全部显示很明显是不可能的,设置spacingNum后,即每隔spacingNum个点显示一个,由于X轴的数据可以为字符串,所以目前只针对X轴有效。
/** * Created by fySpring * Date : 2017/8/10 * To do :x或y轴 */ public class Axis { public static final int DEFAULT_TEXT_SIZE_SP = 12; /** * X轴间隔,每隔多少显示一个值,默认为1 */ private int spacingNum = 1; /** * 是否显示轴上的刻度线 */ private boolean hasLines = false; /** * 值的颜色 */ private int textColor = Color.LTGRAY; /** * 值的字体大小 */ private int textSize = DEFAULT_TEXT_SIZE_SP; /** * 刻度线的颜色 */ private int lineColor = Color.LTGRAY; /** * 坐标值list */ private List<AxisValue> values = new ArrayList<>(); public Axis() { } public Axis(List<AxisValue> values) { this.values = values; } public int getSpacingNum() { return spacingNum; } public void setSpacingNum(int spacingNum) { this.spacingNum = spacingNum; } public boolean isHasLines() { return hasLines; } public Axis setHasLines(boolean hasLines) { this.hasLines = hasLines; return this; } public int getTextColor() { return textColor; } public void setTextColor(int textColor) { this.textColor = textColor; } public int getTextSize() { return textSize; } public void setTextSize(int textSize) { this.textSize = textSize; } public int getLineColor() { return lineColor; } public void setLineColor(int lineColor) { this.lineColor = lineColor; } public List<AxisValue> getValues() { return values; } public void setValues(List<AxisValue> values) { this.values = values; } }
②、关于轴相关的类已经建好,接下来就是绘制轴上的数据。绘制Y轴的数据需要注意一点,由于数据的宽度不一样,我们需要保证数据在Y轴能够右对齐,要先遍历每一个数据,然后测量其宽度,取出最大的宽度,为了保证高度上能够均分,需要预先算出X轴上文字的高度,这样就能保证X轴和Y轴上下都能留出一定的距离给数据展示。
设置Y轴的数据:
//获取数据中的X轴和Y轴 Axis xAxis = data.getAxisX(); Axis yAxis = data.getAxisY(); //设置轴的颜色和字体大小 paint.setColor(xAxis.getTextColor()); paint.setTextSize(sp2px(xAxis.getTextSize())); //从X轴的数据中取出一个来测量底部需要的高度 String firstStr = "测试"; paint.getTextBounds(firstStr, 0, firstStr.length(), fontRect); // X轴的起始值,表示距离底部的Y的值 int bottomY = getHeight() - fontRect.height() * 2 - paddingLeft; //Y轴上每一项的高度,用来均分,一定要用float来计算除法,切记!!!! float itemHeight = (float) bottomY / yAxis.getValues().size(); //遍历Y轴的数据,取出占用宽度最大的数值,保持Y轴的数据能够右对齐 widthList.clear(); int maxWidth = 0; for (int i = 0; i < yAxis.getValues().size(); i++) { String contentStr = yAxis.getValues().get(i).getLabel(); //测量字体宽度,取最大宽度 paint.getTextBounds(contentStr, 0, contentStr.length(), fontRect); widthList.add(fontRect.width()); if (fontRect.width() > maxWidth) maxWidth = fontRect.width(); } //Y轴的起始值,表示距离左边的X的值 int leftX = maxWidth + paddingLeft * 2; //绘制Y轴上的值及刻度 for (int i = 0; i < yAxis.getValues().size(); i++) { String contentStr = yAxis.getValues().get(i).getLabel(); float curY = itemHeight * (yAxis.getValues().size() - i); //绘制值 canvas.drawText(contentStr, paddingLeft + maxWidth - widthList.get(i), curY + fontRect.height() / 2, paint); //绘制刻度线,0的时候不需要画刻度 if (i != 0) { if (yAxis.isHasLines()) canvas.drawLine(leftX, curY, getWidth() - paddingRight, curY, paint); else canvas.drawLine(leftX, curY, leftX + scaleHeight, curY, paint); } }
设置X轴上的数据,在这个地方会出现新的问题,为保证文字能够垂直居中,所以我们需要减去测量文字的高度的一半,但是!!!文字的高度不一样的话,就不会显示在中间。比如文字 一 和 日。咦,我为什么要说日。ノ´▽`)ノ♪ 后来我想到一种做法,在底部放一个矩形,每个矩形的高度一样,然后将文字设置在矩形的中间。并且,宽度还要减去每个文字的宽度的一半。简直完美!
//计算X轴要显示的数据个数 int xUnit = (int) Math.ceil((double) xAxis.getValues().size() / xAxis.getSpacingNum()); //X轴上每一项的宽度,切记,右边一定要记得转float float itemWidth = (float) (getWidth() - leftX - paddingRight) / xUnit; int index = 0; for (int i = 0; i < xAxis.getValues().size(); i += xAxis.getSpacingNum()) { String contentStr = xAxis.getValues().get(i).getLabel(); //测量字体宽度 paint.getTextBounds(contentStr, 0, contentStr.length(), fontRect); float curX = leftX + index * itemWidth; //绘制坐标轴上的值,减掉字体宽度的一半是为了对齐刻度线 //设置一个矩形格子,保证文字能够居中 Rect targetRect = new Rect((int) curX - fontRect.width() / 2, bottomY, (int) curX + fontRect.width() / 2, getHeight()); canvas.drawText(contentStr, targetRect.left, targetRect.centerY(), paint); //绘制坐标轴刻度线 if (i != 0) { if (xAxis.isHasLines()) canvas.drawLine(curX, 0, curX, bottomY, paint); else canvas.drawLine(curX, bottomY - scaleHeight, curX, bottomY, paint); } index++; } //绘制Y轴的线 canvas.drawLine(leftX, 0, leftX, bottomY, paint); //绘制X轴的线 canvas.drawLine(leftX, bottomY, getWidth() - paddingRight, bottomY, paint);
二、坐标轴已经绘制好了,我们开始绘制基准线,定义了一个BaseLine类,这个类里主要放了value和color,就不展示出来了,画线那里用到了DashPathEffect类。这个类的作用就是将Path的线段虚线化。构造函数为DashPathEffect(float[] intervals, float offset),其中intervals为虚线的ON和OFF数组,该数组的length必须大于等于2,offset为绘制时的偏移量。
effects = new DashPathEffect(new float[]{5, 5, 5, 5}, 1); //绘制基准线 if (data.getBaseLines().size() != 0) { float maxY = yAxis.getValues().get(yAxis.getValues().size() - 1).getValue(); float minY = yAxis.getValues().get(0).getValue(); for (BaseLine line : data.getBaseLines()) { path.reset(); //基准值只能在最大值和最小值之间 if (line.getBaseValue() < maxY && line.getBaseValue() > minY) { String contentStr = String.valueOf(line.getBaseValue()); //测量字体宽度 paint.getTextBounds(contentStr, 0, contentStr.length(), fontRect); //计算基准线的Y轴的位置 float progress = 1 - (line.getBaseValue() / maxY); float curY = (bottomY - itemHeight) * progress + itemHeight; float toX = getWidth() - paddingRight - fontRect.width(); paint.setColor(line.getBaseColor()); paint.setStyle(Paint.Style.STROKE); paint.setPathEffect(effects); path.moveTo(leftX, curY); path.lineTo(toX, curY); canvas.drawPath(path, paint); paint.setStyle(Paint.Style.FILL); paint.setAntiAlias(true); canvas.drawText(contentStr, toX + paddingRight / 2, curY + fontRect.height() / 2, paint); } } }
三、基准线绘制好了,接下来就是绘制点和线了。这里也得注意,先要绘制线,再绘制点,因为点的颜色会不一样,再绘制线的话会盖在点的上面。我的做法是先将每个点在屏幕的坐标位置根据点的X轴Y轴的值计算出来。
①、首先我们需要一个PointValue类来存储点的位置,这里没什么好说的,看名字就能明白:
/** * Created by fySpring * Date : 2017/8/10 * To do :点的坐标值及颜色 */ public class PointValue { private float xValue; //点在X轴上的值 private float yValue; //点在Y轴上的值 private float xPosition; //点在X轴上的位置 private float yPosition; //点在Y轴上的位置 private String label; private int pointColor; public PointValue(float xValue, float yValue) { setData(xValue, yValue); } public PointValue(PointValue pointValue) { setData(pointValue.xValue, pointValue.yValue); this.label = pointValue.label; } public PointValue setData(float xValue, float yValue) { this.xValue = xValue; this.yValue = yValue; return this; } public float getxValue() { return xValue; } public void setxValue(float xValue) { this.xValue = xValue; } public float getyValue() { return yValue; } public void setyValue(float yValue) { this.yValue = yValue; } public float getxPosition() { return xPosition; } public void setxPosition(float xPosition) { this.xPosition = xPosition; } public float getyPosition() { return yPosition; } public void setyPosition(float yPosition) { this.yPosition = yPosition; } public String getLabel() { return TextUtils.isEmpty(label) ? String.valueOf(yValue) : label; } public void setLabel(String label) { this.label = label; } public int getPointColor() { return pointColor; } public PointValue setPointColor(int pointColor) { this.pointColor = pointColor; return this; } }
②、其次我们需要把每个点串起来的Line类,这里开放了几个属性,如,线的颜色,点的颜色,宽度,是否是平滑曲线还是直线,是否显示当前值等,当然了你也可以拿过去定制你需要的属性:
/** * Created by fySpring * Date : 2017/8/10 * To do :折线图中每条线的数据 */ public class Line { private static final int DEFAULT_LINE_STROKE_WIDTH_DP = 1; private static final int DEFAULT_POINT_RADIUS_DP = 3; /** * 线的颜色,默认红色 */ private int lineColor = Color.RED; /** * 圆点的颜色 */ private int pointColor; /** * 线的宽度 */ private int strokeWidth = DEFAULT_LINE_STROKE_WIDTH_DP; /** * 圆点的半径 */ private int pointRadius = DEFAULT_POINT_RADIUS_DP; /** * 是否显示小圆点 */ private boolean hasPoints = true; /** * 是否显示值 */ private boolean hasLabels = false; /** * 折线是否圆滑 */ private boolean isSmooth = false; private List<PointValue> values = new ArrayList<>(); public Line(List<PointValue> values) { this.values = values; } public int getLineColor() { return lineColor; } public void setLineColor(int lineColor) { this.lineColor = lineColor; } public int getPointColor() { //如果没有设置则返回线的颜色 return pointColor == 0 ? lineColor : pointColor; } public void setPointColor(int pointColor) { this.pointColor = pointColor; } public int getStrokeWidth() { return strokeWidth; } public void setStrokeWidth(int strokeWidth) { this.strokeWidth = strokeWidth; } public int getPointRadius() { return pointRadius; } public void setPointRadius(int pointRadius) { this.pointRadius = pointRadius; } public boolean isHasPoints() { return hasPoints; } public void setHasPoints(boolean hasPoints) { this.hasPoints = hasPoints; } public boolean isHasLabels() { return hasLabels; } public void setHasLabels(boolean hasLabels) { this.hasLabels = hasLabels; } public boolean isSmooth() { return isSmooth; } public void setSmooth(boolean smooth) { isSmooth = smooth; } public List<PointValue> getValues() { return values; } public void setValues(List<PointValue> values) { this.values = values; } }
③、然后我们需要一个类用来存放我们所有的数据,这样在代码中我们只需要几行代码就能显示出折线图,
/** * Created by fySpring * Date : 2017/8/10 * To do :折线图数据类 */ public class LineChartData implements ChartData{ private static final int DEFAULT_TEXT_SIZE_SP = 12; private Axis axisX; private Axis axisY; private int valueLabelTextColor = Color.WHITE; private int valueLabelTextSize = DEFAULT_TEXT_SIZE_SP; private List<BaseLine> baseLines = new ArrayList<>(); private List<Line> lines = new ArrayList<>(); public LineChartData(List<Line> lines) { this.lines = lines; } @Override public Axis getAxisX() { return axisX; } @Override public void setAxisX(Axis axisX) { this.axisX = axisX; } @Override public Axis getAxisY() { return axisY; } @Override public void setAxisY(Axis axisY) { this.axisY = axisY; } @Override public int getValueLabelTextColor() { return valueLabelTextColor; } @Override public void setValueLabelsTextColor(int valueLabelTextColor) { this.valueLabelTextColor = valueLabelTextColor; } @Override public int getValueLabelTextSize() { return valueLabelTextSize; } @Override public void setValueLabelTextSize(int valueLabelTextSize) { this.valueLabelTextSize = valueLabelTextSize; } public List<Line> getLines() { return lines; } public List<BaseLine> getBaseLines() { return baseLines; } public void setBaseLines(List<BaseLine> baseLines) { this.baseLines = baseLines; } }
④、最后再来根据线的个数去绘制点和线,通过Y的最大值来计算点的Y轴的位置
//计算点在屏幕中的位置 if (data.getLines().size() != 0) { float maxY = yAxis.getValues().get(yAxis.getValues().size() - 1).getValue(); for (Line line : data.getLines()) { for (PointValue value : line.getValues()) { //根据横纵坐标计算点在屏幕中的位置,如果没有线的话可以在这里画点 float xProgress = value.getxValue() / xAxis.getValues().size(); float yProgress = 1 - (value.getyValue() / maxY); //点的坐标 float centerX = leftX + (getWidth() - leftX - paddingRight) * xProgress; float centerY = (bottomY - itemHeight) * yProgress + itemHeight; value.setxPosition(centerX); value.setyPosition(centerY); } //根据数据值画线与点 drawLineAndPoint(canvas, line); } }
最关键的绘制线的地方在这里,由于比较复杂,我就单独封装了一个方法,记得绘制之前将path和paint给reset一下。(这里可得注意点,一不留心就会出问题)
/** * 根据点绘制贝赛尔曲线和点,绘制贝赛尔曲线每次需要三个点 * * @param canvas * @param line */ private void drawLineAndPoint(Canvas canvas, Line line) { path.reset(); paint.reset(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.STROKE); paint.setColor(line.getLineColor()); paint.setStrokeWidth(dip2px(line.getStrokeWidth())); if (line.isSmooth()) { //绘制圆滑曲线 float prePreviousPointX = Float.NaN; float prePreviousPointY = Float.NaN; float previousPointX = Float.NaN; float previousPointY = Float.NaN; float currentPointX = Float.NaN; float currentPointY = Float.NaN; float nextPointX = Float.NaN; float nextPointY = Float.NaN; int valueSize = line.getValues().size(); for (int valueIndex = 0; valueIndex < valueSize; valueIndex++) { if (Float.isNaN(currentPointX)) { PointValue linePoint = line.getValues().get(valueIndex); currentPointX = linePoint.getxPosition(); currentPointY = linePoint.getyPosition(); } if (Float.isNaN(previousPointX)) { if (valueIndex > 0) { PointValue linePoint = line.getValues().get(valueIndex - 1); previousPointX = linePoint.getxPosition(); previousPointY = linePoint.getyPosition(); } else { previousPointX = currentPointX; previousPointY = currentPointY; } } if (Float.isNaN(prePreviousPointX)) { if (valueIndex > 1) { PointValue linePoint = line.getValues().get(valueIndex - 2); prePreviousPointX = linePoint.getxPosition(); prePreviousPointY = linePoint.getyPosition(); } else { prePreviousPointX = previousPointX; prePreviousPointY = previousPointY; } } // 设置当前点的下一个点的坐标 if (valueIndex < valueSize - 1) { PointValue linePoint = line.getValues().get(valueIndex + 1); nextPointX = linePoint.getxPosition(); nextPointY = linePoint.getyPosition(); } else { nextPointX = currentPointX; nextPointY = currentPointY; } if (valueIndex == 0) { // 将第一个点设为起始点 path.moveTo(currentPointX, currentPointY); } else { // 计算出每个点的控制点 final float firstDiffX = (currentPointX - prePreviousPointX); final float firstDiffY = (currentPointY - prePreviousPointY); final float secondDiffX = (nextPointX - previousPointX); final float secondDiffY = (nextPointY - previousPointY); final float firstControlPointX = previousPointX + (LINE_SMOOTHNESS * firstDiffX); final float firstControlPointY = previousPointY + (LINE_SMOOTHNESS * firstDiffY); final float secondControlPointX = currentPointX - (LINE_SMOOTHNESS * secondDiffX); final float secondControlPointY = currentPointY - (LINE_SMOOTHNESS * secondDiffY); path.cubicTo(firstControlPointX, firstControlPointY, secondControlPointX, secondControlPointY, currentPointX, currentPointY); } // 将值后移 prePreviousPointX = previousPointX; prePreviousPointY = previousPointY; previousPointX = currentPointX; previousPointY = currentPointY; currentPointX = nextPointX; currentPointY = nextPointY; } } else { //绘制直线 for (int i = 0; i < line.getValues().size(); i++) { PointValue value = line.getValues().get(i); //第一个点不需要连接 if (i == 0) { path.moveTo(value.getxPosition(), value.getyPosition()); } else { path.lineTo(value.getxPosition(), value.getyPosition()); } } } canvas.drawPath(path, paint); //如果有点的话就进行画圆点 if (line.isHasPoints()) { paint.setTextSize(defaultTextSize); paint.setStyle(Paint.Style.FILL); for (PointValue pointValue : line.getValues()) { if (pointValue.getPointColor() != 0) { paint.setColor(pointValue.getPointColor()); } else { paint.setColor(line.getPointColor()); } //画圆点 canvas.drawCircle(pointValue.getxPosition(), pointValue.getyPosition(), dip2px(line.getPointRadius()), paint); if (line.isHasLabels()) { String contentStr = pointValue.getLabel(); //测量字体宽高 paint.getTextBounds(contentStr, 0, contentStr.length(), fontRect); //绘制点的顶部数据 canvas.drawText(contentStr, pointValue.getxPosition() - fontRect.width() / 2, pointValue.getyPosition() - 20, paint); } } } }
最后再放上完整的LineChartView吧,好方便大家对应着去看:
public class LineChartView extends View {
private static final float LINE_SMOOTHNESS = 0.16f;
private Context context;
private LineChartData data;
private int paddingLeft;
private int paddingRight;
//刻度的高度,只在hasLine为false的情况下显示
private int scaleHeight;
//绘制虚线
private PathEffect effects;
private Paint paint;
private Path path;//路径
private Rect fontRect; //测量字体
private List<Integer> widthList;
private int defaultTextSize;
public LineChartView(Context context) {
super(context, null, 0);
this.context = context;
init();
}
public LineChartView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
this.context = context;
init();
}
public LineChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
init();
}
private void init() {
paint = new Paint();
path = new Path();
paint.setAntiAlias(true);
fontRect = new Rect();
widthList = new ArrayList<>();
effects = new DashPathEffect(new float[]{5, 5, 5, 5}, 1);
paddingLeft = dip2px(10);
paddingRight = dip2px(10);
scaleHeight = dip2px(5);
defaultTextSize = sp2px(12);
}
public void setData(LineChartData data) {
this.data = data;
invalidate();
}
public LineChartData getData() {
return data;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (data != null && data.getAxisY().getValues().size() != 0 && data.getAxisX().getValues().size() != 0) {
path.reset();
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.FILL);
//获取数据中的X轴和Y轴 Axis xAxis = data.getAxisX(); Axis yAxis = data.getAxisY(); //设置轴的颜色和字体大小 paint.setColor(xAxis.getTextColor()); paint.setTextSize(sp2px(xAxis.getTextSize())); //从X轴的数据中取出一个来测量底部需要的高度 String firstStr = "测试"; paint.getTextBounds(firstStr, 0, firstStr.length(), fontRect); // X轴的起始值,表示距离底部的Y的值 int bottomY = getHeight() - fontRect.height() * 2 - paddingLeft; //Y轴上每一项的高度,用来均分,一定要用float来计算除法,切记!!!! float itemHeight = (float) bottomY / yAxis.getValues().size(); //遍历Y轴的数据,取出占用宽度最大的数值,保持Y轴的数据能够右对齐 widthList.clear(); int maxWidth = 0; for (int i = 0; i < yAxis.getValues().size(); i++) { String contentStr = yAxis.getValues().get(i).getLabel(); //测量字体宽度,取最大宽度 paint.getTextBounds(contentStr, 0, contentStr.length(), fontRect); widthList.add(fontRect.width()); if (fontRect.width() > maxWidth) maxWidth = fontRect.width(); } //Y轴的起始值,表示距离左边的X的值 int leftX = maxWidth + paddingLeft * 2; //绘制Y轴上的值及刻度 for (int i = 0; i < yAxis.getValues().size(); i++) { String contentStr = yAxis.getValues().get(i).getLabel(); float curY = itemHeight * (yAxis.getValues().size() - i); //绘制值 canvas.drawText(contentStr, paddingLeft + maxWidth - widthList.get(i), curY + fontRect.height() / 2, paint); //绘制刻度线,0的时候不需要画刻度 if (i != 0) { if (yAxis.isHasLines()) canvas.drawLine(leftX, curY, getWidth() - paddingRight, curY, paint); else canvas.drawLine(leftX, curY, leftX + scaleHeight, curY, paint); } }
//计算X轴要显示的数据个数
int xUnit = (int) Math.ceil((double) xAxis.getValues().size() / xAxis.getSpacingNum());
//X轴上每一项的宽度,切记,右边一定要记得转float
float itemWidth = (float) (getWidth() - leftX - paddingRight) / xUnit;
int index = 0;
for (int i = 0; i < xAxis.getValues().size(); i += xAxis.getSpacingNum()) {
String contentStr = xAxis.getValues().get(i).getLabel();
//测量字体宽度
paint.getTextBounds(contentStr, 0, contentStr.length(), fontRect);
float curX = leftX + index * itemWidth;
//绘制坐标轴上的值,减掉字体宽度的一半是为了对齐刻度线
//设置一个矩形格子,保证文字能够居中
Rect targetRect = new Rect((int) curX - fontRect.width() / 2, bottomY, (int) curX + fontRect.width() / 2, getHeight());
canvas.drawText(contentStr, targetRect.left, targetRect.centerY(), paint);
//绘制坐标轴刻度线
if (i != 0) {
if (xAxis.isHasLines())
canvas.drawLine(curX, 0, curX, bottomY, paint);
else
canvas.drawLine(curX, bottomY - scaleHeight, curX, bottomY, paint);
}
index++;
}
//绘制Y轴的线
canvas.drawLine(leftX, 0, leftX, bottomY, paint);
//绘制X轴的线
canvas.drawLine(leftX, bottomY, getWidth() - paddingRight, bottomY, paint);
//绘制基准线
if (data.getBaseLines().size() != 0) {
float maxY = yAxis.getValues().get(yAxis.getValues().size() - 1).getValue();
float minY = yAxis.getValues().get(0).getValue();
for (BaseLine line : data.getBaseLines()) {
path.reset();
//基准值只能在最大值和最小值之间
if (line.getBaseValue() < maxY && line.getBaseValue() > minY) {
String contentStr = String.valueOf(line.getBaseValue());
//测量字体宽度
paint.getTextBounds(contentStr, 0, contentStr.length(), fontRect);
//计算基准线的Y轴的位置
float progress = 1 - (line.getBaseValue() / maxY);
float curY = (bottomY - itemHeight) * progress + itemHeight;
float toX = getWidth() - paddingRight - fontRect.width();
paint.setColor(line.getBaseColor());
paint.setStyle(Paint.Style.STROKE);
paint.setPathEffect(effects);
path.moveTo(leftX, curY);
path.lineTo(toX, curY);
canvas.drawPath(path, paint);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
canvas.drawText(contentStr, toX + paddingRight / 2, curY + fontRect.height() / 2, paint);
}
}
}
//计算点在屏幕中的位置
if (data.getLines().size() != 0) {
float maxY = yAxis.getValues().get(yAxis.getValues().size() - 1).getValue();
for (Line line : data.getLines()) {
for (PointValue value : line.getValues()) {
//根据横纵坐标计算点在屏幕中的位置,如果没有线的话可以在这里画点
float xProgress = value.getxValue() / xAxis.getValues().size();
float yProgress = 1 - (value.getyValue() / maxY);
//点的坐标
float centerX = leftX + (getWidth() - leftX - paddingRight) * xProgress;
float centerY = (bottomY - itemHeight) * yProgress + itemHeight;
value.setxPosition(centerX);
value.setyPosition(centerY);
}
//根据数据值画线与点
drawLineAndPoint(canvas, line);
}
}
}
}
/** * 根据点绘制贝赛尔曲线和点,绘制贝赛尔曲线每次需要三个点 * * @param canvas * @param line */ private void drawLineAndPoint(Canvas canvas, Line line) { path.reset(); paint.reset(); paint.setAntiAlias(true); paint.setStyle(Paint.Style.STROKE); paint.setColor(line.getLineColor()); paint.setStrokeWidth(dip2px(line.getStrokeWidth())); if (line.isSmooth()) { //绘制圆滑曲线 float prePreviousPointX = Float.NaN; float prePreviousPointY = Float.NaN; float previousPointX = Float.NaN; float previousPointY = Float.NaN; float currentPointX = Float.NaN; float currentPointY = Float.NaN; float nextPointX = Float.NaN; float nextPointY = Float.NaN; int valueSize = line.getValues().size(); for (int valueIndex = 0; valueIndex < valueSize; valueIndex++) { if (Float.isNaN(currentPointX)) { PointValue linePoint = line.getValues().get(valueIndex); currentPointX = linePoint.getxPosition(); currentPointY = linePoint.getyPosition(); } if (Float.isNaN(previousPointX)) { if (valueIndex > 0) { PointValue linePoint = line.getValues().get(valueIndex - 1); previousPointX = linePoint.getxPosition(); previousPointY = linePoint.getyPosition(); } else { previousPointX = currentPointX; previousPointY = currentPointY; } } if (Float.isNaN(prePreviousPointX)) { if (valueIndex > 1) { PointValue linePoint = line.getValues().get(valueIndex - 2); prePreviousPointX = linePoint.getxPosition(); prePreviousPointY = linePoint.getyPosition(); } else { prePreviousPointX = previousPointX; prePreviousPointY = previousPointY; } } // 设置当前点的下一个点的坐标 if (valueIndex < valueSize - 1) { PointValue linePoint = line.getValues().get(valueIndex + 1); nextPointX = linePoint.getxPosition(); nextPointY = linePoint.getyPosition(); } else { nextPointX = currentPointX; nextPointY = currentPointY; } if (valueIndex == 0) { // 将第一个点设为起始点 path.moveTo(currentPointX, currentPointY); } else { // 计算出每个点的控制点 final float firstDiffX = (currentPointX - prePreviousPointX); final float firstDiffY = (currentPointY - prePreviousPointY); final float secondDiffX = (nextPointX - previousPointX); final float secondDiffY = (nextPointY - previousPointY); final float firstControlPointX = previousPointX + (LINE_SMOOTHNESS * firstDiffX); final float firstControlPointY = previousPointY + (LINE_SMOOTHNESS * firstDiffY); final float secondControlPointX = currentPointX - (LINE_SMOOTHNESS * secondDiffX); final float secondControlPointY = currentPointY - (LINE_SMOOTHNESS * secondDiffY); path.cubicTo(firstControlPointX, firstControlPointY, secondControlPointX, secondControlPointY, currentPointX, currentPointY); } // 将值后移 prePreviousPointX = previousPointX; prePreviousPointY = previousPointY; previousPointX = currentPointX; previousPointY = currentPointY; currentPointX = nextPointX; currentPointY = nextPointY; } } else { //绘制直线 for (int i = 0; i < line.getValues().size(); i++) { PointValue value = line.getValues().get(i); //第一个点不需要连接 if (i == 0) { path.moveTo(value.getxPosition(), value.getyPosition()); } else { path.lineTo(value.getxPosition(), value.getyPosition()); } } } canvas.drawPath(path, paint); //如果有点的话就进行画圆点 if (line.isHasPoints()) { paint.setTextSize(defaultTextSize); paint.setStyle(Paint.Style.FILL); for (PointValue pointValue : line.getValues()) { if (pointValue.getPointColor() != 0) { paint.setColor(pointValue.getPointColor()); } else { paint.setColor(line.getPointColor()); } //画圆点 canvas.drawCircle(pointValue.getxPosition(), pointValue.getyPosition(), dip2px(line.getPointRadius()), paint); if (line.isHasLabels()) { String contentStr = pointValue.getLabel(); //测量字体宽高 paint.getTextBounds(contentStr, 0, contentStr.length(), fontRect); //绘制点的顶部数据 canvas.drawText(contentStr, pointValue.getxPosition() - fontRect.width() / 2, pointValue.getyPosition() - 20, paint); } } } }
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
private int dip2px(float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
/**
* 将sp值转换为px值,保证文字大小不变
*
* @param spValue (DisplayMetrics类中属性scaledDensity)
* @return
*/
public int sp2px(float spValue) {
float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
}
好了,基本上主要的内容就是以上的这些了。来看看在界面中如何使用:
public class MainActivity extends AppCompatActivity { private String rankColors[] = new String[]{"#98D675", "#8DD0EA", "#F6CE6E", "#FF7077", "#C15667"}; private LineChartView chartView; private Button addPointBtn; private List<String> xValueList = new ArrayList<>(); private List<PointValue> pointValues = new ArrayList<>(); private List<Line> lineList = new ArrayList<>(); private Random random = new Random(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); chartView = (LineChartView) findViewById(R.id.line_chart_view); addPointBtn = (Button) findViewById(R.id.add_new_point_btn); for (int i = 1; i < 16; i++) { xValueList.add("测试"+i); } pointValues.add(new PointValue(0f,15f)); lineList = initDataLine(pointValues); chartView.setData(initData(lineList,xValueList)); addPointBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { addNewPoint(); } }); } private void addNewPoint(){ float x = random.nextInt(15); float y = random.nextInt(20); int colorInt = random.nextInt(5); //生成随机颜色,随机位置的点在坐标轴上 pointValues.add(new PointValue(x,y).setPointColor(Color.parseColor(rankColors[colorInt]))); lineList = initDataLine(pointValues); chartView.setData(initData(lineList,xValueList)); } /** * 初始化线属性 * * @return */ private List<Line> initDataLine(List<PointValue> pointValueList) { List<Line> lineList = new ArrayList<>(); //这里可以设置多条数据线,目前我这里展示只有一条 Line bgLine = new Line(pointValueList); bgLine.setSmooth(true); bgLine.setHasLabels(true); bgLine.setPointColor(Color.RED); bgLine.setLineColor(Color.GREEN); bgLine.setHasPoints(true); lineList.add(bgLine); return lineList; } /** * 初始化记录曲线 * * @return */ private LineChartData initData(List<Line> lines, List<String> xDatas) { LineChartData data = new LineChartData(lines); Axis axisY = new Axis(); Axis axisX = new Axis(); axisX.setSpacingNum(3); //设置x轴数值 List<AxisValue> xValues = new ArrayList<>(); for (int i = 0; i < xDatas.size(); i++) { AxisValue value = new AxisValue(i); String label = xDatas.get(i); value.setLabel(label); xValues.add(value); } axisX.setValues(xValues); //设置y轴数值 List<AxisValue> yValues = new ArrayList<>(); for (int i = 0; i <= 20; i += 5) { AxisValue value = new AxisValue(i); String label = String.valueOf(i); value.setLabel(label); yValues.add(value); } axisY.setValues(yValues); //设置X轴Y轴 data.setAxisY(axisY); data.setAxisX(axisX); //设置基准线 List<BaseLine> baseLineList = new ArrayList<>(); BaseLine baseLine1 = new BaseLine().setBaseValue(17.5f).setBaseColor(Color.RED); BaseLine baseLine2 = new BaseLine().setBaseValue(6.89f).setBaseColor(Color.BLUE); baseLineList.add(baseLine1); baseLineList.add(baseLine2); data.setBaseLines(baseLineList); return data; } }
好啦,基本上就是这些了,各位老铁觉得不错的话不妨点个赞? 罒ω罒
源码点我点我
相关文章推荐
- Android textview 设置不同的字体大小和颜色
- Android设置Selector不同状态下颜色及图片
- Android改自定义背景颜色+分隔线设置
- Android之TextView设置字体不同颜色大小
- Android用代码给TextView中间部分设置不同的颜色
- Android之自定义背景Button按钮、自定义形状Button的全攻略、设置字体颜色
- Android textview设置不同的颜色和大小
- android 通过html设置textview的内容不同种颜色
- Android——LinearLayout按下(state_pressed)或获取焦点时(state_focused)背景设置不同颜色或图片
- Android中TextView、EditText对部分内容设置不同格式(颜色、字体、超链接、图片。。)
- Android TextView设置不同的颜色字体
- Android 一个TextView中设置文字不同字体大小和颜色的最完整方法
- 【android】 使用一个shape.xml文件,使用代码设置不同圆角背景颜色
- iOS 设置一行颜色不同的NSString 、剪切图片、设置TableView的自定义header
- Android ImageView设置图片的可见性,设置颜色的不同方法,设置图片指定大小
- android 一个 textview 设置不同的字体大小和颜色
- android设置TextView中文字不同的颜色
- 一起学android之如何设置TextView中不同字段的字体颜色(22)
- android 一个 textview 设置不同的字体大小和颜色
- Android设置十六进制颜色不同透明度对应的值