自定义View-柱状图和折线图的合体
来源:互联网 发布:linux只读文件怎么修改 编辑:程序博客网 时间:2024/05/17 01:49
前端时间,工作中有个需求,柱状图和折线图合体,左右两个坐标分别表示柱状图和折线图,是不是看的有点抽象?没关系,先上个效果图来看一下
左边坐标用来表示柱状图的高度,右边坐标用来表示折线图的点的位置。
然后触摸柱子需要出现一条标识线和一个小卡片,如下图:
图中柱子中间有一根白色的标识线,手势滑动标识线和小卡片需要跟着改变。
虽然网上很多强大的图表库,不过由于时间关系,需要去改别人代码,不如自己动手来的快,记录下来,也给需要的朋友作个小参考。
属性动态设置
各种属性还是建议动态赋值,以后修改起来也方便很多
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
- 自定义View-柱状图和折线图的合体
- Android自定义View实现简单的折线图、柱状图
- 自定义View-轻量级柱状图、饼图、折线图
- 自定义折线图/柱状图
- android自定义统计折线图和柱状图组合
- 三、关于自定义view的文章:饼状图和折线图
- 自定义View--折线图
- 自定义View折线图
- 自定义View折线图
- 折线图 自定义折线图 自定义view
- Android 自定义View -- 简约的折线图
- 自定义View:画布实现自定义View(折线图的实现)
- Android自定义统计图(柱状图,折线图,饼状图)
- Android自定义统计图(柱状图,折线图,饼状图)
- Android自定义统计图(柱状图,折线图,饼状图)
- Android自定义统计图(柱状图,折线图,饼状图)
- Android自定义统计图(柱状图,折线图,饼状图)
- flex柱状图和折线图的混合图使用
- 【微服务架构】SpringCloud之Eureka(服务注册和服务发现基础篇)(二)
- Android应用安装完成后在应用程序安装器的安装完成页面点击"打开"后进入app,再按Home键退到桌面再点击桌面app图标会重启app
- Docker之docker-compose部署django+mysql示例
- github的README.md
- 电商类-仿美团页面demo
- 自定义View-柱状图和折线图的合体
- 线程并发学习----Thread、Runnable、Callable
- spark eclipse开发调试 本地单机模式
- SpringBoot构建微服务实战 之 @Condition*
- sequence的作用和用法
- CDN在秒杀系统中的作用
- 使用AWK在shell中生成日历的小程序
- Salesforce并非唯一利用AI进行CRM和营销自动化的企业
- 使用pdf.js在线预览远程服务器上的pdf文件