自定义View-柱状图和折线图的合体
前端时间,工作中有个需求,柱状图和折线图合体,左右两个坐标分别表示柱状图和折线图,是不是看的有点抽象?没关系,先上个效果图来看一下
左边坐标用来表示柱状图的高度,右边坐标用来表示折线图的点的位置。
然后触摸柱子需要出现一条标识线和一个小卡片,如下图:
图中柱子中间有一根白色的标识线,手势滑动标识线和小卡片需要跟着改变。
虽然网上很多强大的图表库,不过由于时间关系,需要去改别人代码,不如自己动手来的快,记录下来,也给需要的朋友作个小参考。
属性动态设置
各种属性还是建议动态赋值,以后修改起来也方便很多
class ChartParam {
private int verNumberSpace;
private int horNumberSpace;
private int numberSize;
private int rectangleWidth;
private int circleRadius; // 小圆半径
private int baseLineWidth;
private int screenWidth;
private int sideSpace; // 距两边边距
private int leftNumSpace;// 左边数字终点距y轴的距离
private int leftToBaseLine; // 左边数字终点距基线的距离
private int baseLineToYear; // 基线到年份的距离
private int baseLineToLeft;
private int firstRectCenterX; // 第一个矩形中点距边界距离
private int firstRectLeft; // 第一个矩形left属性
private int firstRextRight;
private int rectSpace;
private int overRectHeight; // 高出相对柱状图的高度
private int cardToBaseLine; // 提示相对基线的高度
private int cardToRect; // 提示相对柱状图的边距
private int roundRectAngle; // card圆角大小
private int cardWidth;// card宽
private int cardHeight;// card高
private int card_textMarginTop;
private int card_textMarginLeft;
private int card_text_1to2;// 第一行文字距第二行文字距离
private int card_text_2to3; // 第二行文字距第三行文字距离
private int card_text_1toRect;// 第一行文字的x距圆角矩形的距离
private int card_text_2toRect;//
private int card_text_3toRect;//
}
然后在构造函数中进行初始化赋值
private void init() {
mContext = getContext();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mChartParam = new ChartParam();
mChartParam.baseLineWidth = getWidth(mContext) - dpToPx(mContext, 38 * 2);
mChartParam.screenWidth = dpToPx(mContext, 375);
mChartParam.horNumberSpace = dpToPx(mContext, 28);
mChartParam.verNumberSpace = dpToPx(mContext, 23);
mChartParam.rectangleWidth = dpToPx(mContext, 12);
mChartParam.circleRadius = dpToPx(mContext, 2);
mChartParam.numberSize = dpToPx(mContext, 11);
mChartParam.sideSpace = dpToPx(mContext, 15);
mChartParam.leftNumSpace = dpToPx(mContext, 30);
mChartParam.leftToBaseLine = dpToPx(mContext, 8);
mChartParam.baseLineToYear = dpToPx(mContext, 5);
mChartParam.baseLineToLeft = dpToPx(mContext, 38);
mChartParam.firstRectCenterX = dpToPx(mContext, 79);
mChartParam.firstRectLeft = dpToPx(mContext, 73);
mChartParam.firstRextRight = dpToPx(mContext, 85);
mChartParam.rectSpace = dpToPx(mContext, 40);
mChartParam.overRectHeight = dpToPx(mContext, 20);
mChartParam.cardToBaseLine = dpToPx(mContext, 58);
mChartParam.cardToRect = dpToPx(mContext, 8);
mChartParam.roundRectAngle = dpToPx(mContext, 2);
mChartParam.cardWidth = dpToPx(mContext, 127);
mChartParam.cardHeight = dpToPx(mContext, 63);
mChartParam.card_textMarginTop = dpToPx(mContext, 8);
mChartParam.card_textMarginLeft = dpToPx(mContext, 8);
mChartParam.card_text_1to2 = dpToPx(mContext, 8);
mChartParam.card_text_2to3 = dpToPx(mContext, 6);
mChartParam.card_text_1toRect = dpToPx(mContext,20);
mChartParam.card_text_2toRect = dpToPx(mContext,38);
mChartParam.card_text_3toRect = dpToPx(mContext,55);
rectStartX = new float[5];
rectStopX = new float[5];
rectStopY = new float[5];
touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
}
具体的值看大家的需求自己定义啦。
下面开始画具体的图了。
左侧坐标栏
private void drawLeftNumber(Canvas canvas) {
for (int i = 0; i < 6; i++) {
setPaint(NUMBER_TEXT);
String text = ((5.0 - i) + "").equals("0.0") ? "0" : 5.0 - i + "";
float number = Float.parseFloat(text);
int textWidth = (int) mPaint.measureText(text);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
canvas.drawText(text, mChartParam.leftNumSpace - textWidth,
mChartParam.verNumberSpace * i + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) * (i + 1),
mPaint);
if (number == 0 || Math.abs(number) <= 0.2) {
baseNumber = number + "";
// 该值是基线相关数字最上面的值
baseLineY = mChartParam.verNumberSpace * i + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) * i;
}
if (i == 5) {
yearY = mChartParam.verNumberSpace * i + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) * (i + 1);
}
}
}
这里需要注意一点的是drawText()绘制文字的起点是差不多文字左下角的位置(这里只是差不多,其实不完全准确,这里我忽略了那小差别)。由于需求是数字靠右对齐,所以在计算文字的起点坐标的时候,需要用文字靠右的距离减去文字的宽度,也就是x轴坐标:
mChartParam.leftNumSpace - textWidth,
文字宽度的计算如下:
int textWidth = (int) mPaint.measureText(text);
然后是文字y轴计算,这里需要计算文字的高度,关于文字的高度问题参考下面这张图:
(注:copy from http://hencoder.com/ui-1-3/)
基线
基线也就是底部的那根线了 ,这个就比较简单了
/**
* 基线
*
* @param canvas
*/
private void drawBaseLine(Canvas canvas) {
setPaint(BASE_LINE);
canvas.drawLine(mChartParam.baseLineToLeft, baseLineY - mChartParam.baseLineToYear, mChartParam.baseLineToLeft + mChartParam.baseLineWidth, baseLineY - mChartParam.baseLineToYear, mPaint);
}
右侧坐标栏
右侧坐标栏的绘制和左侧是一样的,值得注意的点也只有是文字的高度
/**
* 右边数字栏
*
* @param canvas
*/
private void drawRightNumber(Canvas canvas) {
for (int i = 0; i < 6; i++) {
setPaint(NUMBER_TEXT);
String text = ((100 - 20 * i) + "").equals("0") ? "0" : (100 - 20 * i + "%");
int textWidth = (int) mPaint.measureText(text);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
canvas.drawText(text, getWidth(mContext) - mChartParam.sideSpace - textWidth,
mChartParam.verNumberSpace * i + (Math.abs(fontMetrics.ascent) + fontMetrics.descent) * (i + 1),
mPaint);
}
}
年份
年份的绘制也很简单,也就是文字绘制,由于标注图有给出各种距离,所以绘制起来也很顺利
/**
* 年份
*
* @param canvas
*/
private void drawYear(Canvas canvas) {
setPaint(NUMBER_TEXT);
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
for (int i = 0; i < 5; i++) {
String text = year - (4 - i) + "";
int textWidth = (int) mPaint.measureText(text);
canvas.drawText(text, mChartParam.firstRectCenterX - textWidth / 2 + (textWidth + mChartParam.horNumberSpace) * i, yearY, mPaint);
rectLeft = mChartParam.firstRectCenterX - textWidth / 2 + (textWidth + mChartParam.horNumberSpace) * i + textWidth / 2 - dpToPx(mContext, 6);
}
}
柱状图
柱状图的话,只要你知道第一个柱形图的x轴的位置,柱形的宽度,和个柱形之间的距离,也和很顺利
/**
* 柱状图
*
* @param canvas
*/
private void drawRectangle(Canvas canvas) {
setPaint(RECTANGLE);
for (int i = 0; i < 5; i++) {
canvas.drawRect(mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i,
dpToPx(mContext, 60) - i * 2,
mChartParam.firstRextRight + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i,
baseLineY - mChartParam.baseLineToYear, mPaint);
rectStartX[i] = mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i;
rectStopX[i] = mChartParam.firstRextRight + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i;
rectStopY[i] = dpToPx(mContext, 60) - i * 2;
}
}
这里的:
rectStartX[i] = mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i;
rectStopX[i] = mChartParam.firstRextRight + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i;
rectStopY[i] = dpToPx(mContext, 60) - i * 2;
是为了记录柱形的坐标,因为后面需要在触摸柱形图范围内显示标识线。
点和折线
接下里是先把点绘制出来,然后再连接起来,形成折线
/**
* 点和折线
*
* @param canvas
*/
private void drawPointAndLine(Canvas canvas) {
setPaint(BROKE_LINE);
for (int i = 0; i < 5; i++) {
canvas.drawCircle(mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i + mChartParam.rectangleWidth / 2,
50 * i + 20, mChartParam.circleRadius, mPaint);
if (i != 4) {
float startX = mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * i + mChartParam.rectangleWidth / 2;
float stopX = mChartParam.firstRectLeft + (mChartParam.rectangleWidth + mChartParam.rectSpace) * (i + 1) + mChartParam.rectangleWidth / 2;
float startY = 50 * i + 20;
float stopY = 50 * (i + 1) + 20;
canvas.drawLine(startX, startY, stopX, stopY, mPaint);
}
}
}
两个点作为一条线的起点和终点。
标志线
前面在绘制矩形图的时候已经记录了各个矩形的坐标,所以这里标志线在每个柱形图的中间位置
/**
* 触摸柱状图时中间的竖线
* @param canvas
*/
private void drawVerLine(Canvas canvas) {
if (touchRectPoi != -1) {
setPaint(CARD_LINE);
float lineX = rectStartX[touchRectPoi] + (rectStopX[touchRectPoi] - rectStartX[touchRectPoi]) / 2;
canvas.drawLine(lineX, baseLineY - mChartParam.baseLineToYear, lineX, rectStopY[touchRectPoi] - mChartParam.overRectHeight, mPaint);
}
}
小卡片
接下来是小卡片的展示,我这里的小卡片的位置是根据触摸 的柱状图的位置来确定的,前面三个是显示在柱状图的右边,后面两个是显示在柱状图的左边,但是卡片相对柱状图的距离和高度是确定的,小卡片的背景是用的一张图片。
/**
* 卡片
* @param canvas
*/
private void drawTipsCard(Canvas canvas) {
// 圆角矩形与其中的文字
float roundRectLeft = 0, roundRectTop = 0;
if (touchRectPoi != -1) {
setPaint(ROUND_RECT);
float lineX = rectStartX[touchRectPoi] + (rectStopX[touchRectPoi] - rectStartX[touchRectPoi]) / 2;
if (touchRectPoi >= 0 && touchRectPoi < 3) {
// 显示在柱状图右边
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.roundrect), lineX + mChartParam.cardToRect, baseLineY - mChartParam.cardToBaseLine - mChartParam.cardHeight,mPaint);
roundRectLeft = lineX + mChartParam.cardToRect;
roundRectTop = baseLineY - mChartParam.cardToBaseLine - mChartParam.cardHeight;
} else {
// 显示在柱状图左边
canvas.drawBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.roundrect), lineX - mChartParam.cardToRect - mChartParam.cardWidth, baseLineY - mChartParam.cardToBaseLine - mChartParam.cardHeight,mPaint);
roundRectLeft = lineX - mChartParam.cardToRect - mChartParam.cardWidth;
roundRectTop = baseLineY - mChartParam.cardToBaseLine - mChartParam.cardHeight;
}
// 文字
setPaint(CARD_TEXT);
String text1 = "2017年年报";
String text2 = "每股收益:1.83元";
String text3 = "同比增长率:-12.00%";
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
canvas.drawText(text1, roundRectLeft + mChartParam.card_textMarginLeft,
roundRectTop + mChartParam.card_text_1toRect, mPaint);
canvas.drawText(text2, roundRectLeft + mChartParam.card_textMarginLeft,
roundRectTop + mChartParam.card_text_2toRect, mPaint);
canvas.drawText(text3, roundRectLeft + mChartParam.card_textMarginLeft,
roundRectTop + mChartParam.card_text_3toRect, mPaint);
}
}
手势识别
标识线和小卡片是需要根据手势移动来改变展示的,所以这里需要监听onTouchEvent事件:
@Override
public boolean onTouchEvent(MotionEvent event) {
float touchX = event.getX();
float touchY = event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
touchToRect(touchX);
lastX = touchX;
return true;
case MotionEvent.ACTION_MOVE:
float moveX = event.getX();
if (Math.abs(moveX - lastX) > touchSlop) {
lastX = moveX;
touchToRect(moveX);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}
这里记录下手指触摸和手指滑动时的x值,然后判断是否在对应的柱状图的范围之内,来展示标识线和小卡片:
/**
* 在柱状图范围
* @param x
*/
private void touchToRect(float x) {
if (x >= rectStartX[0] && x <= rectStopX[0]) {
touchRectPoi = 0;
Log.d("wyk", "第一个柱状图");
} else if (x >= rectStartX[1] && x <= rectStopX[1]) {
touchRectPoi = 1;
Log.d("wyk", "第二个柱状图");
} else if (x >= rectStartX[2] && x <= rectStopX[2]) {
touchRectPoi = 2;
Log.d("wyk", "第三个柱状图");
} else if (x >= rectStartX[3] && x <= rectStopX[3]) {
touchRectPoi = 3;
Log.d("wyk", "第四个柱状图");
} else if (x >= rectStartX[4] && x <= rectStopX[4]) {
touchRectPoi = 4;
Log.d("wyk", "第五个柱状图");
}
invalidate();
}
最后记得要调用invalidate()方法进行重绘
写的比较匆忙,有问题可以在下面评论或者私我。
最后附上完整的项目地址:
https://github.com/upperLucky/CombineChartDemo
推荐阅读
-
Python读取Excel表格,并同时画折线图和柱状图的方法
-
jQuery插件HighCharts绘制2D柱状图、折线图和饼图的组合图效果示例【附demo源码下载】
-
自定义View-柱状图和折线图的合体
-
Python实现双轴组合图表柱状图和折线图的具体流程
-
Android自定义可左右滑动和点击的折线图
-
Python读取Excel表格,并同时画折线图和柱状图的方法
-
自定义view-仿qq侧滑菜单的显示和删除
-
jQuery插件FusionCharts绘制2D柱状图和折线图的组合图效果示例【附demo源码】
-
jQuery插件HighCharts绘制2D柱状图、折线图和饼图的组合图效果示例【附demo源码下载】
-
FusionCharts 2D柱状图和折线图的组合图调试错误