一. 设计
- 上次写的七日化利率表设计很粗糙,也没有体现面向对象的思想。数据处理都在控件中,而且表格列数比较固定,对于数据和列数不匹配的情况,只能从左向右依次显示数据。另外扩展性很差。
- 根据图表的属性,分别抽象出坐标轴、坐标刻度、点、线等类。所以在使用的时候初始化这些数据就可以控制图表的展示。
- 抽象出折线统计图和条形统计图公共部分AbsLeafChart,这个类主要处理了控件的有关尺寸、初始化坐标轴和坐标轴刻度、计算每个点在控件的坐标。
- 折线统计图LeafLineChart继承AbsLeafChart,只负责画折线图。
二.类介绍
AxisValue 坐标轴刻度
类型 | 属性 | 介绍 |
---|---|---|
String | label | 刻度值 |
float | pointX | x 坐标 |
float | pointY | y 坐标 |
Axis 坐标轴
类型 | 属性 | 介绍 |
---|---|---|
List | values | 刻度集合 |
boolean | hasLines | 是否连线 |
boolean | isShowText | 是否显示坐标轴刻度值 |
Typeface | typeface | 刻度字体 |
int | textSize | 刻度字体大小 |
int | textColor | 刻度字体颜色 |
int | axisColor | x/y轴颜色 |
float | axisWidth | x/y轴宽度 |
int | axisLineColor | 平行于x/y轴的坐标轴颜色 |
float | axisLineWidth | 平行于x/y轴的坐标轴宽度 |
float | startX | 坐标轴起点x坐标 |
float | startY | 坐标轴起点y坐标 |
float | stopX | 坐标轴终点x坐标 |
float | stopY | 坐标轴终点y坐标 |
PointValue 点
类型 | 属性 | 介绍 |
---|---|---|
float | x | 占x轴总长度权重 |
float | y | 占x轴总长度权重 |
float | diffX | 距离原点长度 |
float | diffY | 距离原点长度 |
float | originX | x坐标 |
float | originY | y坐标 |
String | label | 标签 |
ChartData 图表数据
类型 | 属性 | 介绍 |
---|---|---|
List | values | 点集合 |
boolean | hasLabels | 是否显示标签 |
int | labelColor | 标签背景色 |
float | labelRadius | 标签背景弧度 |
Line extends ChartData 折线
类型 | 属性 | 介绍 |
---|---|---|
int | lineColor | 折线颜色 |
float | lineWidth | 折线的宽度 |
boolean | hasPoints | 是否画圆点 |
boolean | hasLines | 是否画线条 |
int | pointColor | 圆点颜色 |
float | pointRadius | 圆点半径 |
boolean | isCubic | 是否是曲线 |
boolean | isFill | 是否填充 |
int | fillColr | 填充色 |
三、实现
暂时只实现了折线图,这里只介绍折线图实现
AbsLeafChart中的相关方法:
尺寸控制
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
initViewSize();
resetAsixSize();
resetLineSize();
}
控件宽高和控件内间距初始化
protected void initViewSize() {
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
leftPadding = LeafUtil.dp2px(mContext, 20);
rightPadding = LeafUtil.dp2px(mContext, 10);
topPadding = LeafUtil.dp2px(mContext, 15);
bottomPadding = LeafUtil.dp2px(mContext, 20);
}
设置坐标轴位置
protected void resetAsixSize() {
if(axisX != null){
List<AxisValue> values = axisX.getValues();
int sizeX = values.size(); //几条y轴
float xStep = (mWidth - leftPadding) / sizeX;
axisX.setStepSize(xStep);
for (int i = 0; i < sizeX; i++) {
AxisValue axisValue = values.get(i);
axisValue.setPointY(mHeight);
if(i == 0){
axisValue.setPointX(leftPadding);
} else {
axisValue.setPointX(leftPadding + xStep * i);
}
}
axisX.setStartX(0).setStartY(mHeight - bottomPadding)
.setStopX(mWidth).setStopY(mHeight - bottomPadding);
}
if(axisY != null){
List<AxisValue> values = axisY.getValues();
int sizeY = values.size(); //几条x轴
float yStep = (mHeight - topPadding - bottomPadding) / sizeY;
axisY.setStepSize(yStep);
for (int i = 0; i < sizeY; i++) {
AxisValue axisValue = values.get(i);
axisValue.setPointX(leftPadding);
if(i == 0){
axisValue.setPointY(mHeight - bottomPadding );
} else {
axisValue.setPointY(mHeight - bottomPadding - yStep * i);
}
}
axisY.setStartX(leftPadding).setStartY(mHeight - bottomPadding)
.setStopX(leftPadding).setStopY(0);
}
}
设置点所占比重
protected void resetLineSize() {
if(line != null && axisX != null && axisY != null){
List<PointValue> values = line.getValues();
int size = values.size();
List<AxisValue> axisValuesX = axisX.getValues();
List<AxisValue> axisValuesY = axisY .getValues();
float totalWidth = Math.abs(axisValuesX.get(0).getPointX() - axisValuesX.get(axisValuesX.size() - 1).getPointX());
float totalHeight = Math.abs(axisValuesY.get(0).getPointY() - axisValuesY.get(axisValuesY.size() - 1).getPointY());
// Log.e("=====", "totalWidth:" + totalWidth + ", totalHeight:" + totalHeight);
for (int i = 0; i < size; i++) {
PointValue pointValue = values.get(i);
float diffX = pointValue.getX() * totalWidth;
pointValue.setDiffX(diffX);
float diffY = pointValue.getY() * totalHeight;
pointValue.setDiffY(diffY);
// Log.e("====", "x:" + pointValue.getX() + ", y:" + pointValue.getY());
// Log.e("=======", "diffX:" + pointValue.getDiffX() + ", diffY:" + pointValue.getDiffY());
}
}
}
画坐标轴 刻度值
protected void drawCoordinateText(Canvas canvas) {
if(axisX != null && axisY != null){
X 轴
// 1.刻度
paint.setColor(axisX.getTextColor());
paint.setTextSize(LeafUtil.sp2px(mContext, axisX.getTextSize()));
Paint.FontMetrics fontMetrics = paint.getFontMetrics(); // 获取标题文字的高度(fontMetrics.descent - fontMetrics.ascent)
float textH = fontMetrics.descent - fontMetrics.ascent;
List<AxisValue> valuesX = axisX.getValues();
if(axisX.isShowText()){
for (int i = 0; i < valuesX.size(); i++) {
AxisValue value = valuesX.get(i);
float textW = paint.measureText(value.getLabel());
canvas.drawText(value.getLabel(), value.getPointX() - textW / 2, value.getPointY() - textH / 2,paint);
}
}
/// Y 轴
paint.setColor(axisY.getTextColor());
paint.setTextSize(LeafUtil.sp2px(mContext, axisY.getTextSize()));
List<AxisValue> valuesY = axisY.getValues();
if(axisY.isShowText()){
for (AxisValue value : valuesY){
float textW = paint.measureText(value.getLabel());
float pointx = value.getPointX() - 1.1f * textW;
canvas.drawText(value.getLabel(), pointx , value.getPointY(),paint);
}
}
}
}
确定每个点所在位置:根据之前计算的diff计算得到每个点的x、y坐标
protected void setPointsLoc(){
if(line != null){
linePaint.setStrokeWidth(LeafUtil.dp2px(mContext, line.getLineWidth()));
List<PointValue> values = line.getValues();
int size = values.size();
for (int i = 0; i < size; i++) {
PointValue point1 = values.get(i);
float originX1 = point1.getDiffX() + leftPadding;
float originY1 = mHeight - bottomPadding - point1.getDiffY();
point1.setOriginX(originX1).setOriginY(originY1);
}
}
}
画圆点
protected void drawPoints(Canvas canvas) {
if (line != null) {
if(line.isHasPoints()){
labelPaint.setColor(line.getPointColor());
List<PointValue> values = line.getValues();
for (PointValue point: values) {
float radius = LeafUtil.dp2px(mContext, line.getPointRadius());
canvas.drawCircle(point.getOriginX(), point.getOriginY(),
radius , labelPaint);
}
}
}
}
画每一个点上的标签 : 需要注意的是画标签的时候需要得到标签字符串的高和宽,另外需要控制的是标签边界不要超出控件边界
protected void drawLabels(Canvas canvas) {
if (line != null) {
if(line.isHasLabels()){
labelPaint.setTextSize(LeafUtil.sp2px(mContext, 12));
Paint.FontMetrics fontMetrics = labelPaint.getFontMetrics();
List<PointValue> values = line.getValues();
int size = values.size();
for (int i = 0; i < size; i++) {
PointValue point = values.get(i);
String label = point.getLabel();
Rect bounds = new Rect();
int length = label.length();
labelPaint.getTextBounds(label, 0, length, bounds);
float textW = bounds.width();
float textH = bounds.height();
float left, top, right, bottom;
if(length == 1){
left = point.getOriginX() - textW * 2.2f;
right = point.getOriginX() + textW * 2.2f;
} else if(length == 2){
left = point.getOriginX() - textW * 1.0f;
right = point.getOriginX() + textW * 1.0f;
} else {
left = point.getOriginX() - textW * 0.6f;
right = point.getOriginX() + textW * 0.6f;
}
top = point.getOriginY() - 2.5f*textH;
bottom = point.getOriginY() - 0.5f*textH;
// if(i > 0){
// PointValue prePoint = values.get(i - 1);
// RectF rectF = prePoint.getRectF();
// if(left <= rectF.right){
// // 左边与上一个标签重叠
// top = point.getOriginY() + 1.7f*textH;
// bottom = point.getOriginY() + 0.5f*textH;
// }
// }
//控制位置
if(left < 0){
left = leftPadding;
right += leftPadding;
}
if(top < 0){
top = topPadding;
bottom += topPadding;
}
if(right > mWidth){
right -= rightPadding;
left -= rightPadding;
}
RectF rectF = new RectF(left, top, right, bottom);
float labelRadius = LeafUtil.dp2px(mContext,line.getLabelRadius());
labelPaint.setColor(line.getLabelColor());
canvas.drawRoundRect(rectF, labelRadius, labelRadius, labelPaint);
//drawText
labelPaint.setColor(Color.WHITE);
float xCoordinate = left + (right - left - textW) / 2;
float yCoordinate = bottom - (bottom - top - textH) / 2 ;
canvas.drawText(point.getLabel(), xCoordinate, yCoordinate, labelPaint);
}
}
}
}
LeafLineChart中的方法:
画折线:记得之前话折线的方法是点与点之间连线。这里的做法是吧每个点用Path记录,最后通过drawPath来画折线
protected void drawLines(Canvas canvas) {
if(line != null){
linePaint.setColor(line.getLineColor());
linePaint.setStrokeWidth(LeafUtil.dp2px(mContext, line.getLineWidth()));
linePaint.setStyle(Paint.Style.STROKE);
List<PointValue> values = line.getValues();
int size = values.size();
for (int i = 0; i < size; i++) {
PointValue point = values.get(i);
if(i == 0) path.moveTo(point.getOriginX(), point.getOriginY());
else path.lineTo(point.getOriginX(), point.getOriginY());
}
canvas.drawPath(path, linePaint);
}
}
画曲线
private void drawCubicPath(Canvas canvas) {
if(line != null){
linePaint.setColor(line.getLineColor());
linePaint.setStrokeWidth(LeafUtil.dp2px(mContext, line.getLineWidth()));
linePaint.setStyle(Paint.Style.STROKE);
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;
List<PointValue> values = line.getValues();
final int lineSize = values.size();
for (int valueIndex = 0; valueIndex < lineSize; ++valueIndex) {
if (Float.isNaN(currentPointX)) {
PointValue linePoint = (PointValue) values.get(valueIndex);
currentPointX = linePoint.getOriginX();
currentPointY = linePoint.getOriginY();
}
if (Float.isNaN(previousPointX)) {
if (valueIndex > 0) {
PointValue linePoint = values.get(valueIndex - 1);
previousPointX = linePoint.getOriginX();
previousPointY = linePoint.getOriginY();
} else {
previousPointX = currentPointX;
previousPointY = currentPointY;
}
}
if (Float.isNaN(prePreviousPointX)) {
if (valueIndex > 1) {
PointValue linePoint = values.get(valueIndex - 2);
prePreviousPointX = linePoint.getOriginX();
prePreviousPointY = linePoint.getOriginY();
} else {
prePreviousPointX = previousPointX;
prePreviousPointY = previousPointY;
}
}
// nextPoint is always new one or it is equal currentPoint.
if (valueIndex < lineSize - 1) {
PointValue linePoint = values.get(valueIndex + 1);
nextPointX = linePoint.getOriginX();
nextPointY = linePoint.getOriginY();
} else {
nextPointX = currentPointX;
nextPointY = currentPointY;
}
if (valueIndex == 0) {
// Move to start point.
path.moveTo(currentPointX, currentPointY);
} else {
// Calculate control points.
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);
}
// Shift values by one back to prevent recalculation of values that have
// been already calculated.
prePreviousPointX = previousPointX;
prePreviousPointY = previousPointY;
previousPointX = currentPointX;
previousPointY = currentPointY;
currentPointX = nextPointX;
currentPointY = nextPointY;
}
canvas.drawPath(path, linePaint);
}
}
用的请看源码。
GitHub:LeafChart