Android自定义可左右滑动和点击的折线图
前言
前几天有小盆友让我写一个折线图,可以点击,可以左右滑动。对于折线肯定有很多项目都使用过,所以网上肯定也有很多demo,像androidchart、hellochart之类的,功能相当丰富,效果也很赞,但是太重了,其他的小demo又不符合要求,当然了,我写的自定义折线图的思想也有来自这些小demo,对他们表示感谢。
效果图
废话不多说,先上效果图:
效果是不是很赞,如果上图满足你的需求,那就继续往下看。
自定义折线图的步骤:
1、自定义view所需要的属性
确定所需要的自定义view的属性,然后在res/values目录下,新建一个attrs.xml文件,代码如下:
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- xy坐标轴颜色 --> <attr name="xylinecolor" format="color" /> <!-- xy坐标轴宽度 --> <attr name="xylinewidth" format="dimension" /> <!-- xy坐标轴文字颜色 --> <attr name="xytextcolor" format="color" /> <!-- xy坐标轴文字大小 --> <attr name="xytextsize" format="dimension" /> <!-- 折线图中折线的颜色 --> <attr name="linecolor" format="color" /> <!-- x轴各个坐标点水平间距 --> <attr name="interval" format="dimension" /> <!-- 背景颜色 --> <attr name="bgcolor" format="color" /> <!--是否在action_up时,根据速度进行自滑动,建议关闭,过于占用gpu--> <attr name="isscroll" format="boolean" /> <declare-styleable name="chartview"> <attr name="xylinecolor" /> <attr name="xylinewidth" /> <attr name="xytextcolor" /> <attr name="xytextsize" /> <attr name="linecolor" /> <attr name="interval" /> <attr name="bgcolor" /> <attr name="isscroll" /> </declare-styleable> </resources>
2、在自定义view的构造方法中获取我们的自定义属性:
public chartview(context context) { this(context, null); } public chartview(context context, attributeset attrs) { this(context, attrs, 0); } public chartview(context context, attributeset attrs, int defstyleattr) { super(context, attrs, defstyleattr); init(context, attrs, defstyleattr); initpaint(); } /** * 初始化 * * @param context * @param attrs * @param defstyleattr */ private void init(context context, attributeset attrs, int defstyleattr) { typedarray array = context.obtainstyledattributes(attrs, r.styleable.chartview, defstyleattr, 0); int count = array.getindexcount(); for (int i = 0; i < count; i++) { int attr = array.getindex(i); switch (attr) { case r.styleable.chartview_xylinecolor://xy坐标轴颜色 xylinecolor = array.getcolor(attr, xylinecolor); break; case r.styleable.chartview_xylinewidth://xy坐标轴宽度 xylinewidth = (int) array.getdimension(attr, typedvalue.applydimension(typedvalue.complex_unit_px, xylinewidth, getresources().getdisplaymetrics())); break; case r.styleable.chartview_xytextcolor://xy坐标轴文字颜色 xytextcolor = array.getcolor(attr, xytextcolor); break; case r.styleable.chartview_xytextsize://xy坐标轴文字大小 xytextsize = (int) array.getdimension(attr, typedvalue.applydimension(typedvalue.complex_unit_px, xytextsize, getresources().getdisplaymetrics())); break; case r.styleable.chartview_linecolor://折线图中折线的颜色 linecolor = array.getcolor(attr, linecolor); break; case r.styleable.chartview_interval://x轴各个坐标点水平间距 interval = (int) array.getdimension(attr, typedvalue.applydimension(typedvalue.complex_unit_px, interval, getresources().getdisplaymetrics())); break; case r.styleable.chartview_bgcolor: //背景颜色 bgcolor = array.getcolor(attr, bgcolor); break; case r.styleable.chartview_isscroll://是否在action_up时,根据速度进行自滑动 isscroll = array.getboolean(attr, isscroll); break; } } array.recycle(); } /** * 初始化畫筆 */ private void initpaint() { xypaint = new paint(); xypaint.setantialias(true); xypaint.setstrokewidth(xylinewidth); xypaint.setstrokecap(paint.cap.round); xypaint.setcolor(xylinecolor); xytextpaint = new paint(); xytextpaint.setantialias(true); xytextpaint.settextsize(xytextsize); xytextpaint.setstrokecap(paint.cap.round); xytextpaint.setcolor(xytextcolor); xytextpaint.setstyle(paint.style.stroke); linepaint = new paint(); linepaint.setantialias(true); linepaint.setstrokewidth(xylinewidth); linepaint.setstrokecap(paint.cap.round); linepaint.setcolor(linecolor); linepaint.setstyle(paint.style.stroke); }
3、获取一写基本点
这些基本点包括:xy轴的原点坐标,第一个点的x轴的初始化坐标值以及其最大值和最小值。这些参数可以在onlayout()方法里面获取。
@override protected void onlayout(boolean changed, int left, int top, int right, int bottom) { if (changed) { //这里需要确定几个基本点,只有确定了xy轴原点坐标,第一个点的x坐标值及其最大最小值 width = getwidth(); height = getheight(); //y轴文本最大宽度 float textywdith = gettextbounds("000", xytextpaint).width(); for (int i = 0; i < yvalue.size(); i++) {//求取y轴文本最大的宽度 float temp = gettextbounds(yvalue.get(i) + "", xytextpaint).width(); if (temp > textywdith) textywdith = temp; } int dp2 = dptopx(2); int dp3 = dptopx(3); xori = (int) (dp2 + textywdith + dp2 + xylinewidth);//dp2是y轴文本距离左边,以及距离y轴的距离 // //x轴文本最大高度 xvaluerect = gettextbounds("000", xytextpaint); float textxheight = xvaluerect.height(); for (int i = 0; i < xvalue.size(); i++) {//求取x轴文本最大的高度 rect rect = gettextbounds(xvalue.get(i) + "", xytextpaint); if (rect.height() > textxheight) textxheight = rect.height(); if (rect.width() > xvaluerect.width()) xvaluerect = rect; } yori = (int) (height - dp2 - textxheight - dp3 - xylinewidth);//dp3是x轴文本距离底边,dp2是x轴文本距离x轴的距离 xinit = interval + xori; minxinit = width - (width - xori) * 0.1f - interval * (xvalue.size() - 1);//减去0.1f是因为最后一个x周刻度距离右边的长度为x轴可见长度的10% maxxinit = xinit; } super.onlayout(changed, left, top, right, bottom); }
4、利用ondraw()方法进行绘制
@override protected void ondraw(canvas canvas) { // super.ondraw(canvas); canvas.drawcolor(bgcolor); drawxy(canvas); drawbrokenlineandpoint(canvas); } /** * 绘制折线和折线交点处对应的点 * * @param canvas */ private void drawbrokenlineandpoint(canvas canvas) { if (xvalue.size() <= 0) return; //重新开一个图层 int layerid = canvas.savelayer(0, 0, width, height, null, canvas.all_save_flag); drawbrokenline(canvas); drawbrokenpoint(canvas); // 将折线超出x轴坐标的部分截取掉 linepaint.setstyle(paint.style.fill); linepaint.setcolor(bgcolor); linepaint.setxfermode(new porterduffxfermode(porterduff.mode.clear)); rectf rectf = new rectf(0, 0, xori, height); canvas.drawrect(rectf, linepaint); linepaint.setxfermode(null); //保存图层 canvas.restoretocount(layerid); } /** * 绘制折线对应的点 * * @param canvas */ private void drawbrokenpoint(canvas canvas) { float dp2 = dptopx(2); float dp4 = dptopx(4); float dp7 = dptopx(7); //绘制节点对应的原点 for (int i = 0; i < xvalue.size(); i++) { float x = xinit + interval * i; float y = yori - yori * (1 - 0.1f) * value.get(xvalue.get(i)) / yvalue.get(yvalue.size() - 1); //绘制选中的点 if (i == selectindex - 1) { linepaint.setstyle(paint.style.fill); linepaint.setcolor(0xffd0f3f2); canvas.drawcircle(x, y, dp7, linepaint); linepaint.setcolor(0xff81dddb); canvas.drawcircle(x, y, dp4, linepaint); drawfloattextbox(canvas, x, y - dp7, value.get(xvalue.get(i))); } //绘制普通的节点 linepaint.setstyle(paint.style.fill); linepaint.setcolor(color.white); canvas.drawcircle(x, y, dp2, linepaint); linepaint.setstyle(paint.style.stroke); linepaint.setcolor(linecolor); canvas.drawcircle(x, y, dp2, linepaint); } } /** * 绘制显示y值的浮动框 * * @param canvas * @param x * @param y * @param text */ private void drawfloattextbox(canvas canvas, float x, float y, int text) { int dp6 = dptopx(6); int dp18 = dptopx(18); //p1 path path = new path(); path.moveto(x, y); //p2 path.lineto(x - dp6, y - dp6); //p3 path.lineto(x - dp18, y - dp6); //p4 path.lineto(x - dp18, y - dp6 - dp18); //p5 path.lineto(x + dp18, y - dp6 - dp18); //p6 path.lineto(x + dp18, y - dp6); //p7 path.lineto(x + dp6, y - dp6); //p1 path.lineto(x, y); canvas.drawpath(path, linepaint); linepaint.setcolor(color.white); linepaint.settextsize(sptopx(14)); rect rect = gettextbounds(text + "", linepaint); canvas.drawtext(text + "", x - rect.width() / 2, y - dp6 - (dp18 - rect.height()) / 2, linepaint); } /** * 绘制折线 * * @param canvas */ private void drawbrokenline(canvas canvas) { linepaint.setstyle(paint.style.stroke); linepaint.setcolor(linecolor); //绘制折线 path path = new path(); float x = xinit + interval * 0; float y = yori - yori * (1 - 0.1f) * value.get(xvalue.get(0)) / yvalue.get(yvalue.size() - 1); path.moveto(x, y); for (int i = 1; i < xvalue.size(); i++) { x = xinit + interval * i; y = yori - yori * (1 - 0.1f) * value.get(xvalue.get(i)) / yvalue.get(yvalue.size() - 1); path.lineto(x, y); } canvas.drawpath(path, linepaint); } /** * 绘制xy坐标 * * @param canvas */ private void drawxy(canvas canvas) { int length = dptopx(4);//刻度的长度 //绘制y坐标 canvas.drawline(xori - xylinewidth / 2, 0, xori - xylinewidth / 2, yori, xypaint); //绘制y轴箭头 xypaint.setstyle(paint.style.stroke); path path = new path(); path.moveto(xori - xylinewidth / 2 - dptopx(5), dptopx(12)); path.lineto(xori - xylinewidth / 2, xylinewidth / 2); path.lineto(xori - xylinewidth / 2 + dptopx(5), dptopx(12)); canvas.drawpath(path, xypaint); //绘制y轴刻度 int ylength = (int) (yori * (1 - 0.1f) / (yvalue.size() - 1));//y轴上面空出10%,计算出y轴刻度间距 for (int i = 0; i < yvalue.size(); i++) { //绘制y轴刻度 canvas.drawline(xori, yori - ylength * i + xylinewidth / 2, xori + length, yori - ylength * i + xylinewidth / 2, xypaint); xytextpaint.setcolor(xytextcolor); //绘制y轴文本 string text = yvalue.get(i) + ""; rect rect = gettextbounds(text, xytextpaint); canvas.drawtext(text, 0, text.length(), xori - xylinewidth - dptopx(2) - rect.width(), yori - ylength * i + rect.height() / 2, xytextpaint); } //绘制x轴坐标 canvas.drawline(xori, yori + xylinewidth / 2, width, yori + xylinewidth / 2, xypaint); //绘制x轴箭头 xypaint.setstyle(paint.style.stroke); path = new path(); //整个x轴的长度 float xlength = xinit + interval * (xvalue.size() - 1) + (width - xori) * 0.1f; if (xlength < width) xlength = width; path.moveto(xlength - dptopx(12), yori + xylinewidth / 2 - dptopx(5)); path.lineto(xlength - xylinewidth / 2, yori + xylinewidth / 2); path.lineto(xlength - dptopx(12), yori + xylinewidth / 2 + dptopx(5)); canvas.drawpath(path, xypaint); //绘制x轴刻度 for (int i = 0; i < xvalue.size(); i++) { float x = xinit + interval * i; if (x >= xori) {//只绘制从原点开始的区域 xytextpaint.setcolor(xytextcolor); canvas.drawline(x, yori, x, yori - length, xypaint); //绘制x轴文本 string text = xvalue.get(i); rect rect = gettextbounds(text, xytextpaint); if (i == selectindex - 1) { xytextpaint.setcolor(linecolor); canvas.drawtext(text, 0, text.length(), x - rect.width() / 2, yori + xylinewidth + dptopx(2) + rect.height(), xytextpaint); canvas.drawroundrect(x - xvaluerect.width() / 2 - dptopx(3), yori + xylinewidth + dptopx(1), x + xvaluerect.width() / 2 + dptopx(3), yori + xylinewidth + dptopx(2) + xvaluerect.height() + dptopx(2), dptopx(2), dptopx(2), xytextpaint); } else { canvas.drawtext(text, 0, text.length(), x - rect.width() / 2, yori + xylinewidth + dptopx(2) + rect.height(), xytextpaint); } } } }
5、点击的处理以及左右
重写ontoucheven()方法,来处理点击和滑动
@override public boolean ontouchevent(motionevent event) { if (isscrolling) return super.ontouchevent(event); this.getparent().requestdisallowintercepttouchevent(true);//当该view获得点击事件,就请求父控件不拦截事件 obtainvelocitytracker(event); switch (event.getaction()) { case motionevent.action_down: startx = event.getx(); break; case motionevent.action_move: if (interval * xvalue.size() > width - xori) {//当期的宽度不足以呈现全部数据 float dis = event.getx() - startx; startx = event.getx(); if (xinit + dis < minxinit) { xinit = minxinit; } else if (xinit + dis > maxxinit) { xinit = maxxinit; } else { xinit = xinit + dis; } invalidate(); } break; case motionevent.action_up: clickaction(event); scrollafteractionup(); this.getparent().requestdisallowintercepttouchevent(false); recyclevelocitytracker(); break; case motionevent.action_cancel: this.getparent().requestdisallowintercepttouchevent(false); recyclevelocitytracker(); break; } return true; }
点击的处理是计算当前点击的x、y坐标范围进行判断点击的是那个点
/** * 点击x轴坐标或者折线节点 * * @param event */ private void clickaction(motionevent event) { int dp8 = dptopx(8); float eventx = event.getx(); float eventy = event.gety(); for (int i = 0; i < xvalue.size(); i++) { //节点 float x = xinit + interval * i; float y = yori - yori * (1 - 0.1f) * value.get(xvalue.get(i)) / yvalue.get(yvalue.size() - 1); if (eventx >= x - dp8 && eventx <= x + dp8 && eventy >= y - dp8 && eventy <= y + dp8 && selectindex != i + 1) {//每个节点周围8dp都是可点击区域 selectindex = i + 1; invalidate(); return; } //x轴刻度 string text = xvalue.get(i); rect rect = gettextbounds(text, xytextpaint); x = xinit + interval * i; y = yori + xylinewidth + dptopx(2); if (eventx >= x - rect.width() / 2 - dp8 && eventx <= x + rect.width() + dp8 / 2 && eventy >= y - dp8 && eventy <= y + rect.height() + dp8 && selectindex != i + 1) { selectindex = i + 1; invalidate(); return; } } }
处理滑动的原理,就是通过改变第一个点的x坐标,通过改变这个基本点,依次改变后面的x轴的点的坐标。
最后在布局里面应用就可以啦,我就不贴代码啦!
总结:
项目还是有缺点的:
(1)左右滑动时,抬起手指仍然可以快速滑动;代码里面给出了一种解决方案,但是太过于暂用资源,没有特 殊要求不建议使用,所以给出一个boolean类型的自定义属性isscroll,true:启动,反之亦然;还有一种解决方案 就是外面再加一层横向scrollview,请读者自行解决,也很简单,只需要稍作修改即可。
(2)点击的时候忘记添加回调,只有添加了回调在可以在activity或者fragment里面获取点击的内容;代码很简 单,自行脑补。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 详解Canvas 跨域脱坑实践
下一篇: 微信小程序实战之轮播图(3)
推荐阅读
-
Android自定义可左右滑动和点击的折线图
-
Android仿微信左右滑动点击切换页面和图标
-
Android仿微信左右滑动点击切换页面和图标
-
Android使用gallery和imageSwitch制作可左右循环滑动的图片浏览器
-
实现可自定义的Android滑动删除
-
Android使用gallery和imageSwitch制作可左右循环滑动的图片浏览器
-
Android自定义可左右滑动和点击的折线图
-
Android中mpchartlib柱状图的详细属性以及实现(可左右滑动可点击)
-
Android自定义控件9----scrollTo/scrollBy实现滑动和直接绘制滑动的对比使用demo测试
-
实现可自定义的Android滑动删除