爲啥說是通用的呢?因爲你可以隨便放幾條折線都行,隨便幾個說明背景都可以。。。顏色神馬都可設置。。。
爲啥這麼隨便?因爲公司業務需要,有的折線圖是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;
}
}
這樣就結束了,很簡單,最後附上地址吧