自定義柱狀圖、折線圖、餅狀圖,先上效果圖。
上面是柱狀圖和折線圖混合圖。
需要代碼的直接上連結拿走https://github.com/yangxiaohan2014/ChartView
分析圖表
-
X軸分析
X軸顯示日期,并且是可左右滑動的
-
Y軸分析
Y軸顯示資料,比較簡單
-
圖表分析
圖表分為兩部分:折線圖和柱狀圖
自定義View
1.分析ChartView需要的坐标資料
原點坐标ox、oy,
X軸步長Xstep,
X軸第一個坐标值
X軸滑動時需要的兩個變量:
X軸第一個坐标的最小值minXInit,
X軸第一個坐标的最大值maxXInit
如下圖:
滑動時需要計算圖表拖到最左邊的值:
2.初始化需要的變量和自定義屬性
public ChartView(Context context) {
super(context);
init(context);
}
public ChartView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initCustomAttrs(context, attrs);
init(context);
}
public ChartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initCustomAttrs(context, attrs);
init(context);
}
private void init(Context context) {
WindowManager wm = (WindowManager) context.getSystemService(WINDOW_SERVICE);
screenWidth = wm.getDefaultDisplay().getWidth();
mPaint = new Paint();
mPaint.setColor(Color.parseColor("#FF2741"));
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setStrokeWidth(3);
}
private void initCustomAttrs(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ChartView);
xStep = typedArray.getDimension(R.styleable.ChartView_x_step, 45);
yStep = typedArray.getDimension(R.styleable.ChartView_y_step, 35);
xTextColor = typedArray.getColor(R.styleable.ChartView_x_text_color, Color.BLACK);
yTextColor = typedArray.getColor(R.styleable.ChartView_y_text_color, Color.BLACK);
linColor = typedArray.getColor(R.styleable.ChartView_x_line_color, Color.BLUE);
columnWidth = typedArray.getDimension(R.styleable.ChartView_column_width, 20);
columnColor = typedArray.getColor(R.styleable.ChartView_column_color, Color.BLUE);
isColumnGradient = typedArray.getBoolean(R.styleable.ChartView_column_color_gradient, false);
columnStartColor = typedArray.getColor(R.styleable.ChartView_column_start_color, Color.BLUE);
columnEndColor = typedArray.getColor(R.styleable.ChartView_column_end_color, Color.BLUE);
columnTextColor = typedArray.getColor(R.styleable.ChartView_column_text_color, Color.BLUE);
startX = (int) typedArray.getDimension(R.styleable.ChartView_x_padding, DensityUtil.dp2px(10));
startY = (int) typedArray.getDimension(R.styleable.ChartView_y_padding, DensityUtil.dp2px(10));
linePointColor = typedArray.getColor(R.styleable.ChartView_line_point_color, Color.BLUE);
lineChartColor = typedArray.getColor(R.styleable.ChartView_line_chart_color, Color.BLUE);
lineTextColor = typedArray.getColor(R.styleable.ChartView_line_text_color, Color.BLUE);
isChartDataVisible = typedArray.getBoolean(R.styleable.ChartView_is_chart_data_visible, false);
typedArray.recycle();
}
3.重寫onLayout、onDraw方法
onlayout方法中擷取圖表的原點O的位置分别為ox、oy,xInit為x軸第一個刻度位置,minXInit為X軸最小值,maxXInit為滑動時X軸的最大值
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (changed) {
height = getHeight();
width = getWidth();
ox = startX;
oy = startY + height - paddingBottom;
xInit = startX + xStep;
minXInit = width - ox - xStep * (xPoints.size() - 1);
maxXInit = xInit;
}
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(bgcolor);
drawOXYPoint(canvas);
drawYPoints(canvas);
// drawXPoints(canvas);
drawColumnsAndLines(canvas);
}
4.開始繪制
首先繪制原點O和X軸、Y軸
private void drawOXYPoint(Canvas canvas) {
mPaint.setColor(yTextColor);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(DensityUtil.dp2px(14));
String o = "0";
Rect rect = new Rect();
mPaint.getTextBounds("0", 0, o.length(), rect);
int bottom = rect.bottom;
int top = rect.top;
int left = rect.left;
int right = rect.right;
int oWidth = right - left;
int oHeight = bottom - top;
canvas.drawText(o, ox - (oWidth + columnTextPadding), oy + oHeight / 2, mPaint);
// mPaint.setColor(linColor);
// //橫線
// canvas.drawLine(startX, startY + height - paddingBottom, startX + width, startY + height - paddingBottom, mPaint);
// //豎線
// canvas.drawLine(startX, startY, startX, height + startY - paddingBottom, mPaint);
canvas.drawText(o, startX - (oWidth + columnTextPadding), oy + oHeight / 2, mPaint);
mPaint.setColor(linColor);
//橫線
canvas.drawLine(startX, startY + height - paddingBottom, startX + width, startY + height - paddingBottom, mPaint);
//豎線
canvas.drawLine(startX, startY, startX, height + startY - paddingBottom, mPaint);
}
繪制Y軸坐标
/**
* 繪制縱坐标
*
* @param canvas
*/
private void drawYPoints(Canvas canvas) {
mPaint.setStyle(Paint.Style.FILL);
for (int i = 0; i < yPoints.length; i++) {
mPaint.setColor(linColor);
float yLength = (i + 1) * yStep;
float pointY = oy - yLength;
canvas.drawLine(startX, pointY, startX + pointWidth, pointY, mPaint);
mPaint.setColor(yTextColor);
String pointStr = yPoints[i];
Rect rect = new Rect();
mPaint.getTextBounds(pointStr, 0, pointStr.length(), rect);
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
canvas.drawText(pointStr, ox - (width + yTextPadding), pointY + height / 2, mPaint);
}
}
繪制折線圖和柱狀圖
private void drawColumnsAndLines(Canvas canvas) {
//??
int layerId = canvas.saveLayer(0, 0, width, height, null, Canvas.ALL_SAVE_FLAG);
drawColumns(canvas);
drawLines(canvas);
// 将折線超出x軸坐标的部分截取掉
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.BLUE);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
RectF rectF = new RectF(0, 0, ox, height);
canvas.drawRect(rectF, mPaint);
mPaint.setXfermode(null);
//儲存圖層
canvas.restoreToCount(layerId);
}
/**
* 繪制柱狀圖
*
* @param canvas
*/
private void drawColumns(Canvas canvas) {
for (int i = 0; i < xPoints.size(); i++) {
float xLength = i * xStep;
float pointX = xInit + xLength;
float centerX = pointX;
float columnHeight = yStep * xPoints.get(i).getxDecimal1() / stepYNumber;
Shader shader = new LinearGradient(centerX, oy, centerX, oy - columnHeight, columnStartColor,
columnEndColor, Shader.TileMode.CLAMP);
if (isColumnGradient) {
mPaint.setShader(shader);
} else {
mPaint.setColor(columnColor);
}
//畫柱形圖
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
canvas.drawRoundRect(centerX - columnWidth / 2, oy - columnHeight, centerX + columnWidth / 2, oy, 0, 0, mPaint);
} else {
canvas.drawRect(centerX - columnWidth / 2, oy - columnHeight, centerX + columnWidth / 2, oy, mPaint);
}
mPaint.setShader(null);
if (xPoints.get(i).isDataVisible) {
//繪制柱狀圖資料
String pointNum = xPoints.get(i).getxDecimal1() + "";
Rect rectNum = new Rect();
mPaint.getTextBounds(pointNum, 0, pointNum.length(), rectNum);
int numRectHeight = rectNum.bottom - rectNum.top;
int numRectWidth = rectNum.right - rectNum.left;
mPaint.setColor(columnTextColor);
canvas.drawText(pointNum, centerX - numRectWidth / 2, oy - columnHeight - columnTextPadding, mPaint);
mPaint.setColor(selectedXTitleColor);
}
//繪制橫坐标标題
mPaint.setColor(xTextColor);
String title = xPoints.get(i).getTitle();
Rect rect = new Rect();
mPaint.getTextBounds(title, 0, title.length(), rect);
int rectHeight = rect.bottom - rect.top;
int rectWidth = rect.right - rect.left;
canvas.drawText(title, centerX - rectWidth / 2, oy + rectHeight + columnTextPadding, mPaint);
}
}
繪制折線圖:
為了使折線看起來順滑流暢,使用了二次貝塞爾曲線來繪制
/**
* 繪制折線圖
*
* @param canvas
*/
private void drawLines(Canvas canvas) {
if (xPoints.size() == 0) {
return;
}
Point[] mPoints = new Point[xPoints.size()];
// float[] pointsy = new float[xPoints.size()];
for (int i = 0; i < xPoints.size(); i++) {
float xLength = i * xStep;
float pointX = xInit + xLength;
//計算圓圈坐标
float pointY = yStep * xPoints.get(i).getxDecimal2() / stepYNumber;
mPaint.setColor(linePointColor);
mPaint.setStyle(Paint.Style.FILL);
//繪制圓圈
canvas.drawCircle(pointX, oy - pointY, DensityUtil.dp2px(3), mPaint);
//繪制圓圈上的數值
if (xPoints.get(i).isDataVisible) {
String pointNum = xPoints.get(i).getxDecimal2() + "";
Rect rectNum = new Rect();
mPaint.getTextBounds(pointNum, 0, pointNum.length(), rectNum);
int numRectWidth = rectNum.right - rectNum.left;
mPaint.setColor(lineTextColor);
canvas.drawText(pointNum, pointX - numRectWidth / 2, oy - pointY - columnTextPadding, mPaint);
}
//繪制貝塞爾曲線
Point point = new Point((int) (0.5f + pointX), (int) (0.5f + oy - pointY));
mPoints[i] = point;
if (i > 0 && i <= xPoints.size() - 1) {
mPaint.setStyle(Paint.Style.STROKE);
Point startp = mPoints[i - 1];
Point endp = mPoints[i];
int wt = (startp.x + endp.x) / 2;
Point p3 = new Point();
Point p4 = new Point();
p3.y = startp.y;
p3.x = wt;
p4.y = endp.y;
p4.x = wt;
Path path = new Path();
path.moveTo(startp.x, startp.y);
//二次貝塞爾曲線
// path.quadTo(p3.x,p3.y,endp.x,endp.y);
//繪制三次貝塞爾曲線
path.cubicTo(p3.x, p3.y, p4.x, p4.y, endp.x, endp.y);
canvas.drawPath(path, mPaint);
}
}
}
5.圖表上資料的點選事件處理
int lastX;
int lastY;
@Override
public boolean onTouchEvent(MotionEvent event) {
this.getParent().requestDisallowInterceptTouchEvent(true);
obtainVelocityTracker(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
if (xStep*xPoints.size() + startX > getWidth()){
int offsetx = (int) (event.getX() - lastX);
lastX = (int) event.getX();
if (xInit + offsetx < minXInit) {
xInit = (int) minXInit;
} else if (xInit + offsetx > maxXInit) {
xInit = (int) maxXInit;
} else {
xInit = xInit + offsetx;
}
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if (event.getX() - lastX < 5) {
clickAction(event);
}
// scrollAfterActionUp();
this.getParent().requestDisallowInterceptTouchEvent(false);
recycleVelocityTracker();
break;
case MotionEvent.ACTION_CANCEL:
this.getParent().requestDisallowInterceptTouchEvent(false);
recycleVelocityTracker();
break;
case MotionEvent.ACTION_POINTER_DOWN:
break;
case MotionEvent.ACTION_POINTER_UP:
break;
}
return true;
}
/**
* 點選X軸坐标或者折線節點
*
* @param event
*/
private void clickAction(MotionEvent event) {
int dp8 = DensityUtil.dp2px(10);
float eventX = event.getX();
float eventY = event.getY();
for (int i = 0; i < xPoints.size(); i++) {
//節點
float x = xInit + xStep * i;
float linePointY = yStep * xPoints.get(i).getxDecimal2() / stepYNumber;
float columnY = yStep * xPoints.get(i).getxDecimal1() / stepYNumber;
float y1 = oy - linePointY;
float y2 = oy - columnY;
if (eventX >= x - dp8 && eventX <= x + dp8 &&
eventY >= y1 - dp8 && eventY <= y1 + dp8 && selectIndex != i + 1) {//每個節點周圍8dp都是可點選區域
if (selectIndex == i) {
xPoints.get(i).isDataVisible = false;
} else {
xPoints.get(selectIndex) .isDataVisible = false;
xPoints.get(i).isDataVisible = true;
}
selectIndex = i;
invalidate();
return;
}
if (eventX >= x - dp8 && eventX <= x + dp8 &&
eventY >= y2 - dp8 && eventY <= y2 + dp8 && selectIndex != i + 1) {//每個節點周圍8dp都是可點選區域
selectIndex = i + 1;
invalidate();
return;
}
}
}