自定义柱状图、折线图、饼状图,先上效果图。
上面是柱状图和折线图混合图。
需要代码的直接上链接拿走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;
}
}
}