为啥说是通用的呢?因为你可以随便放几条折线都行,随便几个说明背景都可以。。。颜色神马都可设置。。。
为啥这么随便?因为公司业务需要,有的折线图是2条折线、2个说明背景色块,有的需要1条折线、3个说明背景,还有个奇葩的是1条折线、4个说明背景,总不能每个都要写一个自定义View吧~
先看效果图:这是两条折线,两个背景图的(拍照不是截图,所以看起来没那么工整,数据我都对比过了,准的一塌糊涂!!!)
先想想思路,一个趋势图需要有以下6个元素:
1,横轴集合 ; 2,纵轴集合; 3,横轴的单位;4,纵轴的单位;5,数据点集合,多少条折线就多少集合;6,说明背景集合。
然后慢慢实现:
写一个表示折线的类:
public class CurveModel {
//坐标点列表
private List<Value> curveLineDataList;
//折线颜色
private int curveColor;
//折线描述语句
private String curveDesc;
public CurveModel(List<Value> curveLineDataList, int curveColor, String curveDesc) {
this.curveLineDataList = curveLineDataList;
this.curveColor = curveColor;
this.curveDesc = curveDesc;
}
...这儿是get方法...
}
再写一个背景的类:
public class BkgModel {
//背景的最小值和最大值
private Value value;
//背景颜色
private int color;
//背景说明文字
private String desc;
public BkgModel(Value value, int color, String desc) {
this.value = value;
this.color = color;
this.desc = desc;
}
...这儿是get方法...
}
Value 类在CurveModel 类中表示x、y的数据,在BkgModel 类表示最小值和最大值
public class Value {
private int x;
private int y;
public Value(int x, int y) {
this.x = x;
this.y = y;
}
...这儿是get方法...
}
好了,所有Model都写好了,接下来,就是重头戏:绘制VIew
这儿用到了建造者模式:
先看下使用吧,看,是不是非常简单,这样构造一个View真是太方便了:
curveTrendChartView.Builder(getApplicationContext())
//设置Y轴的数据
.setY("mmHg", xList, 100.00)
//设置X轴的数据
.setX("日期", yList, 10.0)
//添加背景说明色
.addBKG(new BkgModel(new Value(8000, 9000), Color.parseColor("#FFF5EE"), "低压正常范围"))
//再添加背景说明色,如果你还想添加,那就继续add
.addBKG(new BkgModel(new Value(12000, 14000), Color.parseColor("#E0FFFF"), "高压正常范围"))
//添加折线
.addLine(new CurveModel(lowList, Color.parseColor("#006400"), "低压"))
//我还想添加
.addLine(new CurveModel(highList, Color.RED,"高压"))
.build();
接下来是CurveTrendChartView了:
1,主要重写了onDraw方法:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制背景和说明文字的方法,在该方法初始化了高度的一些变量,所以首先执行该方法
drawDesc(canvas);
//绘制Y坐标
drawYLineText(canvas);
//绘制X坐标
drawXLineText(canvas);
//绘制背景虚线
drawDottedLine(canvas);
//绘制每条折线
drawCurceLine(canvas);
}
2,接下来就是绘制各个模块了,里面对于坐标的绘制真是让我头疼,调了好多次,各种加减乘除,注意,高度是和趋势图相反的:咱们的趋势图是左图,原点在左下。找XY坐标时,要把思想转换为右图,原点在左上。
举例说明:
虚线的高度是这样的:因为要把虚线绘制在中间,所以得加上mYItemHeight / 2,再加上y轴单位和描述框的高度
i * mYItemHeight + mYItemHeight / 2 + mYUnitHeight + descHeight;
点的高度是这样的:因为虚线是在中间绘制的,所以总高度得减去最上虚线的上半部分和最下虚线的下半部分,然后根据比例求坐标点的位置,最后加上那一大串高度
(mYHeight - mYItemHeight)* ((curveLineDataList.get(i).getY() - maxData) / (minData - maxData)) +
mYItemHeight / 2 + mYUnitHeight + descHeight
但这样还是不能使坐标点完全正确,这个问题困扰了我好几个小时,各种debug啊,问题在于mYItemHeight:
//注意,就是这个float,我之前没加,结果精度变了,小数点后的都为0了,怪自己太粗心啊
mYItemHeight = mYHeight / (float) mYDataList.size();
其实也没啥了,上完整代码:
public class CurveTrendChartView extends View {
/**
* 先想想思路~
* 一个曲线图有以下6个要素
* 1,需要有横轴集合 List<T> xDataList
* 2,需要有Y轴集合
* 3,需要有横轴单位
* 4,需要有Y轴单位
* 5,需要有数据点集合,可能是多条折线,以及折现颜色,折线含义
* 6,需要有正常范围、异常范围,以及范围背景色,范围含义
*
* 需要的画笔有
* 1,虚线画笔
* 2,文字(横坐标文字,纵坐标文字,描述含义的文字)
* 3,折线的画笔
*
*
* 需要的类:
* 1,表示X数据和Y数据的
* 2,背景
* 3,每个折线
*/
private Context mContext;
//总宽高
private int mWidth;
private int mHeight;
//Y轴宽高
private int mYWidth = 100, mYHeight;
//X轴宽高
private int mXWidth, mXHeight = 50;
//虚线的画笔
private Paint mDottedLinePaint;
private Path mDottedLinePath;
private int mDottedLineColor = Color.BLUE;
//曲线的画笔
private Paint mCurvePaint;
//曲线圆点的画笔
private Paint mPointPaint;
private int mPointColor = Color.BLACK;
private int pointSize = 8;
//文字的画笔
private Paint mTextPaint;
private int mTextColor = Color.BLACK;
private int mTextSize = 30;
//X轴文字大小,颜色
private int mXTextSize, mXTextColor;
//Y轴文字大小,颜色
private int mYTextSize, mYTextColor;
// Y轴的数据源
private List<Integer> mYDataList;
private double minData, maxData;
// Y轴数据的单位
private String mYDataUnit = "mmHg";
private int mYUnitHeight = 30;
//y轴每个item的高度
private float mYItemHeight;
//X轴的数据源
private List<Integer> mXDataList;
//X轴数据的单位
private String mXDataUnit = "日期";
private float mXUnitHeight = 70;
//X轴每个item的宽度
private float mXItemWidth;
//曲线的数据源
private List<CurveModel> mLineList;
private List<BkgModel> mBkgList;
//正常范围/异常范围背景
private Paint bkgPaint;
//顶部描述文字
//描述框的高度
private int descHeight = 100;
//X轴和Y轴保留的小数点
private double mXNum,mYNum;
public CurveTrendChartView(Context context) {
this(context, null);
}
public CurveTrendChartView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public CurveTrendChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
//获取属性文件
TypedArray array = mContext.obtainStyledAttributes(attrs, R.styleable.CurveTrendChartView);
mXTextSize = array.getDimensionPixelSize(R.styleable.CurveTrendChartView_xTextSize,mTextSize);
mYTextSize = array.getDimensionPixelSize(R.styleable.CurveTrendChartView_yTextSize,mTextSize);
mXTextColor = array.getColor(R.styleable.CurveTrendChartView_xTextColor,mTextColor);
mYTextColor = array.getColor(R.styleable.CurveTrendChartView_yTextColor,mTextColor);
//TypedArray使用完一定要回收,否则会造成内存泄漏
array.recycle();
}
/**
* 初始化画笔和路径
*/
private void initPaint() {
mDottedLinePaint = new Paint();
mDottedLinePaint.setAntiAlias(true);//抗锯齿效果
mDottedLinePaint.setStyle(Paint.Style.STROKE);
mDottedLinePaint.setColor(mDottedLineColor);
mDottedLinePaint.setStrokeWidth(2);
mDottedLinePath = new Path();
mCurvePaint = new Paint();
mCurvePaint.setAntiAlias(true);
mCurvePaint.setStyle(Paint.Style.STROKE);
mPointPaint = new Paint();
mPointPaint.setAntiAlias(true);
mPointPaint.setStyle(Paint.Style.FILL);
mPointPaint.setColor(mPointColor);
mPointPaint.setStrokeWidth(pointSize);
mPointPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setColor(mTextColor);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setTextAlign(Paint.Align.CENTER);
bkgPaint = new Paint();
bkgPaint.setAntiAlias(true);
bkgPaint.setStyle(Paint.Style.FILL);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mXWidth = mWidth - mYWidth;
mXItemWidth = (mWidth - mYWidth - mXUnitHeight) / (float) mXDataList.size();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制背景和说明文字的方法,在该方法初始化了高度的一些变量,所以首先执行该方法
drawDesc(canvas);
//绘制Y坐标
drawYLineText(canvas);
//绘制X坐标
drawXLineText(canvas);
//绘制背景虚线
drawDottedLine(canvas);
//绘制每条折线
drawCurceLine(canvas);
}
/**
* 绘制描述语句部分
* @param canvas
* 初始化了全局变量@{mYHeight}
*/
private void drawDesc(Canvas canvas) {
if(null != mBkgList && mBkgList.size()>0){
//设置对齐方式为左对齐
mTextPaint.setTextAlign(Paint.Align.LEFT);
int bkgHeight = 40;
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
//绘制背景说明颜色和说明文字
//定义框的左边距,定义每个说明背景颜色块宽度
int left = mWidth / 2 + 20, width = 80;
//说明框的高度
descHeight = 10+bkgHeight/2;
for(BkgModel item : mBkgList){
canvas.save();
bkgPaint.setStrokeWidth(bkgHeight);
bkgPaint.setColor(item.getColor());
//画背景框
canvas.drawLine(left, descHeight+10 , left + width,descHeight+10 , bkgPaint);
//画描述语句
canvas.drawText(item.getDesc(), left + width + 20, descHeight+10+mTextSize/2, mTextPaint);
canvas.restore();
descHeight = descHeight+10+bkgHeight;
}
//绘制折线说明颜色和说明文字
left = left + width + 230;
int height =10+ bkgHeight/2;
for(CurveModel item:mLineList){
canvas.save();
mCurvePaint.setColor(item.getCurveColor());
//画折线颜色
canvas.drawLine(left, height+10, left + width, height+10, mCurvePaint);
//画描述语句
canvas.drawText(item.getCurveDesc(), left + width + 20, height+10+mTextSize/2, mTextPaint);
canvas.restore();
height = height+10+bkgHeight;
}
//画描述语句的外框
canvas.drawRect(new Rect(mWidth / 2, 10, mWidth - 10, descHeight + 10 - bkgHeight/2), mDottedLinePaint);
//初始化y轴高度和每行的高度
// descHeight -= 10;
mYHeight = mHeight - mXHeight -mYUnitHeight- descHeight;
//注意,这儿最好转为float,不然会导致高度计算不太准,我找位置不准的原因找了很久很久,终于找到了
mYItemHeight = mYHeight / (float) mYDataList.size();
//在趋势表中绘制背景色
for(BkgModel item : mBkgList){
canvas.save();
//获取最低值和最高值的下标
int bottom = mYDataList.indexOf(item.getValue().getX()),high = mYDataList.indexOf(item.getValue().getY());
//计算背景的高度
bkgPaint.setStrokeWidth((bottom - high) * mYItemHeight);
bkgPaint.setColor(item.getColor());
//计算背景的起始位置
float diastolicStart = (float) ((high +(bottom - high)/2.0 +0.5 ) * mYItemHeight + mYUnitHeight + descHeight);
canvas.drawLine(mYWidth, diastolicStart, mWidth, diastolicStart, bkgPaint);
canvas.restore();
}
}
}
/**
* 对应Y轴的虚线
* @param canvas
*/
private void drawDottedLine(Canvas canvas) {
int count = mYDataList.size();
if (count > 0) {
canvas.save();
//虚线效果:先画5的实线,再画5的空白,开始绘制的偏移值为0
mDottedLinePaint.setPathEffect(new DashPathEffect(new float[]{5, 5}, 0));
mDottedLinePaint.setStrokeWidth(1f);
float startY;
for (int i = 0; i < count; i++) {
//因为要绘制在中间,所以得加上mYItemHeight / 2,再加上mYUnitHeight + descHeight
startY = i * mYItemHeight + mYItemHeight / 2 + mYUnitHeight + descHeight;
mDottedLinePath.reset();
mDottedLinePath.moveTo(mYWidth, startY);
mDottedLinePath.lineTo(mWidth, startY);
canvas.drawPath(mDottedLinePath, mDottedLinePaint);
}
canvas.restore();
}
}
/**
* Y轴的文字
* @param canvas
*/
private void drawYLineText(Canvas canvas) {
int count = mYDataList.size();
if (count > 0) {
canvas.save();
mTextPaint.setTextSize(mYTextSize);
mTextPaint.setColor(mYTextColor);
float startY;
float baseline;
Paint.FontMetricsInt metrics = mTextPaint.getFontMetricsInt();
mTextPaint.setTextAlign(Paint.Align.CENTER);
for (int i = 0; i < count; i++) {
startY = (i + 1) * mYItemHeight;
baseline = (startY * 2 - mYItemHeight - metrics.bottom - metrics.top) / 2 + mYUnitHeight + descHeight;
canvas.drawText(String.valueOf(mYDataList.get(i)/mYNum), mYWidth / 2, baseline, mTextPaint);
}
canvas.drawText(mYDataUnit, mYWidth / 2, descHeight, mTextPaint);
canvas.restore();
}
}
/**
* X轴的文字
* @param canvas
*/
private void drawXLineText(Canvas canvas) {
int count = mXDataList.size();
if (count > 0) {
canvas.save();
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setTextSize(mXTextSize);
mTextPaint.setColor(mXTextColor);
float startX;
for (int i = 0; i < count; i++) {
startX = mYWidth + i * mXItemWidth + mXItemWidth / 2;
canvas.drawText(String.valueOf((mXDataList.get(i)/mXNum)), startX, mHeight-mXHeight/2, mTextPaint);
}
canvas.restore();
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
canvas.drawText(mXDataUnit, mWidth - mXItemWidth / 2, mHeight-mXHeight/2, mTextPaint);
}
}
/**
* 绘制曲线
* @param canvas
*/
private void drawCurceLine(Canvas canvas) {
for(CurveModel item:mLineList) {
canvas.save();
mCurvePaint.setColor(item.getCurveColor());
List<Value> curveLineDataList = item.getCurveLineDataList();
int count = curveLineDataList.size();
if (count > 0) {
mDottedLinePath.reset();
float stopX, stopY;
float baseHeight = mYItemHeight / 2 + mYUnitHeight + descHeight;
//因为虚线是在中间绘制的,所以得减去最上虚线的上半部分和最下虚线的下半部分
float totalHeight = mYHeight - mYItemHeight;
float totalWidth = mWidth - mYWidth - mXUnitHeight - mXItemWidth;
mDottedLinePath.moveTo(mXItemWidth / 2 + mYWidth, (float) (totalHeight * ((curveLineDataList.get(0).getY() - maxData) / (minData - maxData))) + baseHeight);
canvas.drawPoint(mXItemWidth / 2 + mYWidth, (float) (totalHeight * ((curveLineDataList.get(0).getY() - maxData) / (minData - maxData))) + baseHeight, mPointPaint);
for (int i = 1; i < count; i++) {
stopX = (float) (totalWidth * (curveLineDataList.get(i).getX() - mXDataList.get(0)) / (mXDataList.get(mXDataList.size() - 1) - mXDataList.get(0)) + mYWidth + mXItemWidth / 2);
//根据比例求得点的坐标
stopY = (float) (totalHeight * ((curveLineDataList.get(i).getY() - maxData) / (minData - maxData)) + baseHeight);
mDottedLinePath.lineTo(stopX, stopY);
canvas.drawPoint(stopX, stopY, mPointPaint);
}
canvas.drawPath(mDottedLinePath, mCurvePaint);
}
canvas.restore();
}
}
/**
* 初始化全局变量
* @param context 上下文
* @param xUnit x轴的单位
* @param xDataList X轴数据源
* @param xNum x轴保留的小数点位数
* @param yUnit y轴的单位
* @param yDataList y轴的数据源
* @param yNum y轴保留的小数点位数
* @param lineList 曲线列表
* @param bkgItemList 说明背景列表
* @return
*/
private CurveTrendChartView init(Context context, String xUnit, List<Integer> xDataList, double xNum, String yUnit, List<Integer> yDataList, double yNum, List<CurveModel> lineList, List<BkgModel> bkgItemList){
this.mContext = context;
this.mXDataUnit = xUnit;
this.mXDataList = xDataList;
this.mXNum = xNum;
this.mYDataUnit = yUnit;
this.mYDataList = yDataList;
this.mYNum = yNum;
this.mLineList = lineList;
this.mBkgList = bkgItemList;
this.minData = mYDataList.get(mYDataList.size() - 1);
this.maxData = mYDataList.get(0);
initPaint();
invalidate();
return this;
}
public Builder Builder(Context context){
return new Builder(context);
}
//利用建造者模式构造一个数据表
public class Builder{
private Context mContext;
//Y轴数据
private List<Integer> mYDataList;
//Y轴单位
private String mYUnit;
//X轴数据
private List<Integer> mXDataList;
//X轴单位
private String mXUnit;
private double mXNum,mYNum;
//每条数据源集合
private List<CurveModel> mLineList = new ArrayList<>();
//每个背景集合
private List<BkgModel> mBkgItemList = new ArrayList<>();
private Builder(Context context){
this.mContext = context;
}
public Builder setX(String xUnit,List<Integer> xDataList,double xNum){
this.mXUnit = xUnit;
this.mXDataList = xDataList;
this.mXNum = xNum;
return this;
}
public Builder setY(String yUnit,List<Integer> yDataList,double yNum){
this.mYUnit = yUnit;
this.mYDataList = yDataList;
this.mYNum = yNum;
return this;
}
public Builder addLine(CurveModel item){
this.mLineList.add(item);
return this;
}
public Builder addBKG(BkgModel item){
mBkgItemList.add(item);
return this;
}
public CurveTrendChartView build(){
return init(mContext,mXUnit,mXDataList,mXNum,mYUnit,mYDataList,mYNum,mLineList,mBkgItemList);
}
}
public void destory(){
mBkgList = null;
mLineList = null;
mXDataList = null;
mYDataList = null;
mContext = null;
}
}