Android自定义控件:图形报表的实现(折线图、曲线图、动态曲线图)(View与SurfaceView分别实现图表控件)
图形报表很常用,因为展示数据比较直观,常见的形式有很多,如:折线图、柱形图、饼图、雷达图、股票图、还有一些3d效果的图表等。
android中也有不少第三方图表库,但是很难兼容各种各样的需求。
如果第三方库不能满足我们的需要,那么就需要自己去写这么一个控件。
往往在app需求给定后,很多开发者却无从下手,不知道该如何写。
今天刚好抽出点时间,做了个小demo,给大家讲解一下。
本节,主要分享自定义图表的基本过程,不会涉及过于复杂的知识点。
咱们还是按照:需求、分析、设计、实现、总结这种方式给大家讲解吧!!!
这样大家也更容易看得懂。
***
需求
先上效果图:
需求内容:
1.数据:
-- 模拟50天的雾霾数值吧,每天的数值是一个100以内的随机数;
-- 以当前日期为最后一天,向前取50天的数据,也就是50条;
2.业务逻辑
-- 页面加载时,请求数据,展示在图表上;
-- 点击【刷新】数据,重新请求数据,展示在图表上;
3.view
-- 图表背景色为暗灰色:#343643;
-- 图表背景边框线颜色为浅蓝色:#999dd2;
-- 曲线颜色为蓝色:#7176ff;
-- 文字颜色为白色;
-- 图表可设置padding值;
-- 图表全量显示数据,即适配显示;
-- 曲线上的数值文本显示在对应的位置;
-- x坐标轴左右分别显示 开始和结束的日期,并与左右边框线对齐;
-- 图表应支持两种查看方式:整体加载(全量加载) 和 逐条加载(动态加载)
分析
1.数据比较简单,做个随机数即可,略;
2.业务逻辑,较简单,略;
3.view,本节的重点,需要详细分析一下:
3.1 这种图表控件如何实现?
一般做法:使用画布、画笔进行绘制。 如何绘制:使用画笔在画布上绘制图形 (画布类提供了很多画图的方法,画笔可以设置各种笔触效果)。 建议:大家最好提前了解一下画布和画笔的用法。
3.2 背景色如何绘制?
canvas.drawcolor(参数:颜色)即可,很简单,即:画布直接填充背景颜色,不用画笔。
3.3 背景边框线如何实现?
方案1:先定义路径path,记录每一个跟边框线的信息,再使用canvas.drawpath进行绘制; 方案2:使用canvas.drawline分别绘制每一条横线和纵线; 建议:多线条时,canvas.drawpath管理更简单,绘制会更方便一些。
3.4 曲线如何绘制?
我们可以看作二维坐标系,包含x轴和y轴;
那么,曲线的数据如何才能在坐标系中合适的显示呢? 其实不难,我们可以根据画布大小(或控件大小(如果画布尺寸等于控件尺寸)), 计算出曲线的每个数据在x轴和y轴的位置信息,然后将这些位置点连成线就可以了;
x轴应显示数据的位置: 以图表能适配全量数据为参考(也就是能显示全部的数据,本demo中就是50条雾霾数据的点): x轴的长度应与数据总条数对应,那么每一条数据在x轴的位置,应是: 每条数据在x轴的间隔 = x轴长度 / 数据条数; 每条数据在x轴的位置 = 第n条数据 * 间隔;
y轴应显示数据的位置: 以图表能适配全量数据为参考, y轴的区域应能包含所有数据大小,那么,我们需要先获得数据的最大最小值与之对应, 每一条数据num在y轴的位置,应是: 每条数据的y轴比率 = (num - min ) / (max - min); 每条数据在y轴的位置 = 比率 * y轴长度;
获得了数据在x、y轴的位置,我们就可以绘制曲线了, 此处仍然使用path收集每一个数据点的位置,同时使用曲线进行连接, 即path.quadto(x1, y1,x2,y2)(该方法后面有介绍); 然后再画布上绘制曲线路径:canvas.drawpath(path,paint);
3.5 如何绘制文本?
使用canvas.drawtext(text, x, y, paint); 不过x,y的位置的计算,稍微麻烦一些,大家可以看一下这篇文章的相关介绍: https://www.jianshu.com/p/3e48dd0547a0 文章 -- 绘图基础 -- 绘制文本
文本绘制差异:
文本绘制时并非从文本的左上角开始绘制,而是基于baseline开始绘制。 举例: 如果我们想在自定义控件左上角位置绘制文本, 可能会这么写canvas.drawtext("mfgia", 0, 0, paint); 但是这么写,等运行出来,我们发现该控件左上角只会显示baseline下面的内容, 也就只能看到字母g的下半部分, 而其他部分,因为超出了自定义控件上边界,所以没有被绘制出来。
如果不明白也不要紧,我们先学习主要的知识。
如果想把文本位置控制的特别精确,请务必参考该文章。
3.6 动态图表如何绘制?
图表的动态效果其实就是每隔一定时间重绘一次,也就是动态了(视频效果也是这么个原理);
之所以做成两种效果(非动态/动态),主要是让大家了解一下view和surfaceview的用法差异。
主要差异如下:
view -- 仅能在主线程中刷新。 缺点:如果绘制内容过多或频率过高,会影响主线程fps,造成页面卡顿 -- 使用了单缓冲; 缓冲可以理解成对处理的包装,举个简单易懂点的例子: 工人搬砖 工人有10000块砖要从a区搬到b区,他每次搬一块,要搬10000次, 为了不想来回跑这么多次,工人想了个办法,找了个筐来背砖,每筐可以背100块, 这样他就来回跑100次就行了,提高了搬砖效率。那么,这个筐呢就是一个缓冲处理。 在view的绘制上也很容易理解,例如:我们使用画笔按序(中间可有停顿)绘制多个图形, 但是view并没有一个个的去绘制,而是在一次draw方法中,全部绘制了出来。 因为,view也使用了缓冲处理。 surfaceview -- 可在子线程中刷新; 如果绘制的内容少,不建议使用,因为创建线程和缓冲区,也增加了内存。 反之,推荐使用,但是要注意线程的管控。 -- 使用了双缓冲; 继续以工人搬砖的例子讲解。 工人转身忽然看到了一辆卡车(一车能装>1万块),心想这不更省事了么, 于是他先把一框框砖搬到了车上,再把车开到b区,卸砖。 这辆车也就相当于第二次缓冲了。 在控件绘制时实现双缓冲一般可以这么做: 1.新建一个临时图片,并创建其临时画布(画布相当于那辆卡车); 2.将我们想绘制的内容,先绘制到临时图片的画布上(即图片上) 3.在控件需要绘制时,再把图片绘制到控件的真正画布上;
经过上面的对比分析,我们可以得出结论: 1.全量加载的图表(曲线图),使用view或surfaceview来绘制都是可以的 因为:绘制的信息适量,没有特别的性能要求。 2.逐条加载的图表(动态曲线图),我们尽量使用surfaceview来绘制 因为:如果在view里使用线程sleep控制逐条加载,会导致主线程阻塞 (也就是页面看着卡顿半天,等阻塞恢复之后,再忽然绘制出来的效果)。 如果想不卡顿,只能在view中使用线程或timer来处理逐条效果,然后再与主线程进行通信。 与其这么麻烦,我们不如使用surfaceview,直接能在子线程中刷新view不是更好吗。
看完上面的介绍,相信大家对view与surfaceview的区别和用法,也应该了解一些了。
那么,咱们开始下一步吧。
设计
这一个功能实现相对复杂一些,我们最好对demo进行一个简单的分层或模块设计。
分析我们的demo应有的结构,主要包含
- 两种自定义图表控件(view和surfaceview)、
- 一些简单的业务逻辑、
- 数据的处理。
那么,咱们直接用现成的框架吧,mvc、mvp都是可以的,不过mvc、mvp用哪个好呢?
我们直接使用mvp吧,解耦比mvc更好一些。
此处就不画架构图了,直接文本表示吧:
m(数据层):
1. ichartdata.java 图表数据接口(提供了一个方法:获得图表数据) 2. chartdataimpl.java 图表数据实现类(实现了上面的接口) 3. chartdatainfo.java 图表数据实体类(封装了两个属性:日期和数值) 4. chartdateutils.java 工具类(主要是日期格式的处理)
p(presenter中间层):
1.chartpresenter.java 用于连接m和v层,负责业务逻辑的处理,此处也就是:获得了数据,交给ui
v(ui层)
1. ichartui.java ui接口,提供了显示图表的方法,供presenter使用 2. mainactivity.java ui接口的实现类,用于曲线图的展示与交互 3. surfacechartactivity.java ui接口的实现类,用于动态曲线图的展示与交互 4. chartview.java 曲线图控件(直接使用画布、画笔绘制) 5. chartsurfaceview.java 动态曲线图控件(使用timer、线程池、线程、画布、画笔绘制) 6. drawchartutils.java 绘图工具类(绘制的代码主要封装在该类里面)
功能如何实现已经设计好了,那么,开始下一步吧。
***
实现
- 数据层
数据层主要使用随机数模拟真实数据,没有难的技术点,咱们仅把代码贴出来吧
1.1 图表数据实体类
/** * 类:chartdatainfo 图表数据实体类 * 作者: qxc * 日期:2018/4/18. */ public class chartdatainfo { private string date; private int num; public chartdatainfo(string date, int num) { this.date = date; this.num = num; } public string getdate() { return date; } public void setdate(string date) { this.date = date; } public int getnum() { return num; } public void setnum(int num) { this.num = num; } }
1.2 图表数据接口
import java.util.list; /** * 类:ichartdata 图表数据接口 * 作者: qxc * 日期:2018/4/18. */ public interface ichartdata { /** * 获得图表数据 * @param size 数据条数 * @return 数据集合 */ list<chartdatainfo> getchartdata(int size); }
1.3 图表数据实现类
import java.util.arraylist; import java.util.list; import java.util.random; /** * 类:chartdataimpl 图表数据实现类 * 作者: qxc * 日期:2018/4/18. */ public class chartdataimpl implements ichartdata{ private int maxnum = 100; /** * 返回随机的图表数据 * @param size 数据条数 * @return 图表数据集合 */ @override public list<chartdatainfo> getchartdata(int size) { list<chartdatainfo> data = new arraylist<>(); random random = new random(); random.setseed(chartdateutils.getdatenow()); //返回maxnum以内的随机数 for(int i = size-1; i>=0 ; i--){ chartdatainfo datainfo = new chartdatainfo(chartdateutils.getdate(i), random.nextint(maxnum)); data.add(datainfo); } return data; } }
1.4 数据层工具类
import java.text.simpledateformat; import java.util.calendar; import java.util.date; /** * 类:dateutils 数据层工具类 * 1.日期的处理 * 2. * 作者: qxc * 日期:2018/4/18. */ public class chartdateutils { public static long getdatenow(){ date date = new date(); return date.gettime(); } public static string getdate(int day){ calendar calendar = calendar.getinstance(); simpledateformat sdf = new simpledateformat("yyyymmdd"); calendar.add(calendar.date, -day); string date = sdf.format(calendar.gettime()); return date; } }
- presenter层
这一层就是标准的presenter,持有m和v的接口,对他们的业务逻辑进行处理。
2.1 chartpresenter
import com.iwangzhe.mvpchart.model.chartdataimpl; import com.iwangzhe.mvpchart.model.chartdatainfo; import com.iwangzhe.mvpchart.model.ichartdata; import com.iwangzhe.mvpchart.view.ichartui; import java.util.list; /** * 类:chartpresenter * 作者: qxc * 日期:2018/4/18. */ public class chartpresenter { private ichartui ichartview; private ichartdata ichartdata; public chartpresenter(ichartui ichartview) { this.ichartview = ichartview; this.ichartdata = new chartdataimpl(); } //获取图表数据的业务逻辑 public void getchartdata(){ //请求的数据数量 int size = 50; //获得图表数据 list<chartdatainfo> data = ichartdata.getchartdata(size); //把数据设置给ui ichartview.showchartdata(data); } }
- ui层(view)
绘图的技术是本文的核心点,需要重点讲解
3.1 ichartui 接口
package com.iwangzhe.mvpchart.view; import com.iwangzhe.mvpchart.model.chartdatainfo; import java.util.list; /** * 类:ichartview * 作者: qxc * 日期:2018/4/18. */ public interface ichartui { /** * 显示图表 * @param data 数据 */ void showchartdata(list<chartdatainfo> data); }
3.2 mainactivity
布局
<?xml version="1.0" encoding="utf-8"?> <relativelayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000000"> <button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#343643" android:layout_marginleft="8dp" android:layout_margintop="10dp" android:text=" 刷新chartview数据 " android:textcolor="#ffffff" android:textsize="18sp"/> <button android:id="@+id/btnsurface" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#343643" android:layout_torightof="@+id/btn" android:layout_marginleft="8dp" android:layout_margintop="10dp" android:text=" 使用surfaceview展示图表 " android:textcolor="#ffffff" android:textsize="18sp"/> <com.iwangzhe.mvpchart.view.customview.chartview android:id="@+id/cv" android:layout_below="@+id/btn" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="8dp"/> </relativelayout>
代码
package com.iwangzhe.mvpchart.view; import android.app.activity; import android.content.intent; import android.os.bundle; import android.util.log; import android.view.view; import android.widget.button; import com.iwangzhe.mvpchart.r; import com.iwangzhe.mvpchart.model.chartdatainfo; import com.iwangzhe.mvpchart.presenter.chartpresenter; import com.iwangzhe.mvpchart.view.customview.chartview; import java.util.list; public class mainactivity extends activity implements ichartui { chartpresenter chartpresenter; chartview cv; button btn; button btnsurface; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_main); //初始化presenter chartpresenter = new chartpresenter(this); //初始化控件 initview(); //初始化数据 initdata(); //初始化事件 initevent(); } //初始化控件 private void initview() { cv = (chartview) findviewbyid(r.id.cv); btn = (button) findviewbyid(r.id.btn); btnsurface = (button) findviewbyid(r.id.btnsurface); } //初始化数据 private void initdata() { chartpresenter.getchartdata();//请求数据 } //初始化事件 private void initevent() { //刷新数据 btn.setonclicklistener(new view.onclicklistener() { @override public void onclick(view view) { chartpresenter.getchartdata();//重新请求数据(刷新数据) } }); //跳转到动态曲线页面 btnsurface.setonclicklistener(new view.onclicklistener(){ @override public void onclick(view view) { intent intent = new intent(mainactivity.this, surfacechartactivity.class); startactivity(intent); } }); } //p层的数据回调 @override public void showchartdata(list<chartdatainfo> data) { //图表控件设置数据源 cv.setdataset(data); } }
3.3 surfacechartactivity
布局
<?xml version="1.0" encoding="utf-8"?> <relativelayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#000000"> <button android:id="@+id/btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#343643" android:layout_marginleft="8dp" android:layout_margintop="10dp" android:text=" 刷新surfaceview数据 " android:textcolor="#ffffff" android:textsize="18sp"/> <com.iwangzhe.mvpchart.view.customview.chartsurfaceview android:id="@+id/cv" android:layout_below="@+id/btn" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="8dp"/> </relativelayout>
代码
package com.iwangzhe.mvpchart.view; import android.app.activity; import android.os.bundle; import android.util.log; import android.view.view; import android.widget.button; import com.iwangzhe.mvpchart.r; import com.iwangzhe.mvpchart.model.chartdatainfo; import com.iwangzhe.mvpchart.presenter.chartpresenter; import com.iwangzhe.mvpchart.view.customview.chartsurfaceview; import java.util.list; /** * 类:surfacechartactivity * 作者: qxc * 日期:2018/4/19. */ public class surfacechartactivity extends activity implements ichartui{ chartpresenter chartpresenter; chartsurfaceview cv; button btn; @override protected void oncreate(bundle savedinstancestate) { super.oncreate(savedinstancestate); setcontentview(r.layout.activity_surface_chart); //初始化presenter chartpresenter = new chartpresenter(this); //初始化控件 initview(); //初始化数据 initdata(); //初始化事件 initevent(); } //初始化控件 private void initview() { cv = (chartsurfaceview) findviewbyid(r.id.cv); btn = (button) findviewbyid(r.id.btn); } //初始化数据 private void initdata() { chartpresenter.getchartdata();//请求数据 } //初始化事件 private void initevent() { //刷新数据 btn.setonclicklistener(new view.onclicklistener() { @override public void onclick(view view) { chartpresenter.getchartdata();//重新请求数据(刷新数据) } }); } @override public void showchartdata(list<chartdatainfo> data) { //图表控件设置数据源 cv.setdatasource(data); } }
3.4 chartview
package com.iwangzhe.mvpchart.view.customview; import android.content.context; import android.graphics.canvas; import android.graphics.paint; import android.util.attributeset; import android.view.view; import com.iwangzhe.mvpchart.model.chartdatainfo; import java.util.list; /** * 类:chartview * 作者: qxc * 日期:2018/4/18. */ public class chartview extends view{ int canvaswidth;//画布宽度 int canvasheight;//画布高度 int padding = 100;//边界间隔 paint paint;//画笔 list<chartdatainfo> data;//数据 public chartview(context context, attributeset attrs) { super(context, attrs); //初始化画笔属性 initpaint(); } //设置图表数据 public void setdataset(list<chartdatainfo> data){ this.data = data; //强制重绘 invalidate(); } //初始化画笔属性 private void initpaint(){ //设置防锯齿 paint = new paint(paint.anti_alias_flag); //绘制图形样式 //paint.style.stroke描边 //paint.style.fill内容 //paint.style.fill_and_stroke内容+描边 paint.setstyle(paint.style.stroke); //设置画笔宽度 paint.setstrokewidth(1); } //每一次外观变化,都会调用该方法 @override protected void onsizechanged(int w, int h, int oldw, int oldh) { super.onsizechanged(w, h, oldw, oldh); //获得画布宽度 this.canvaswidth = getwidth() - padding * 2; //获得画布高度 this.canvasheight = getheight() - padding * 2; } @override protected void ondraw(canvas canvas) { //每次重绘,绘制图表信息 drawchartutils.getinstance().drawchart(canvas, paint, canvaswidth,canvasheight,padding,data); } }
该类中, 1.在onsizechanged中获得了画布的宽度和高度,作为背景边线和曲线数据的绘制区域 2.画布的宽度和高度减去了padding信息(两边都需要有padding,所以乘以了2) 3.该view创建时,初始化了一支画笔,设置了画笔的一些属性 4.在onsizechanged方法执行后,都会执行ondraw方法进行绘制,该方法中可以获得画布 5.每次刷新数据,调用setdataset方法后,也会强制执行ondraw方法进行绘制,因为invalidate方法会强制重绘 6.我们统一在ondraw方法中绘制图表信息,而图表信息的绘制封装在drawchartutils类中
3.5 chartsurfaceview
package com.iwangzhe.mvpchart.view.customview; import android.content.context; import android.graphics.canvas; import android.graphics.paint; import android.util.attributeset; import android.view.surfaceholder; import android.view.surfaceview; import com.iwangzhe.mvpchart.model.chartdatainfo; import java.util.arraylist; import java.util.list; import java.util.timer; import java.util.timertask; import java.util.concurrent.executorservice; import java.util.concurrent.executors; /** * 类:chartsurfaceview * 作者: qxc * 日期:2018/4/19. */ public class chartsurfaceview extends surfaceview implements surfaceholder.callback{ surfaceholder holder; timer timer; list<chartdatainfo> data;//总数据 list<chartdatainfo> showdata;//当前绘制的数据 executorservice threadpool;//线程池 canvas canvas;//画布 paint paint;//画笔 int canvaswidth;//画布宽度 int canvasheight;//画布高度 int padding = 100;//边界间隔 public chartsurfaceview(context context, attributeset attrs) { super(context, attrs); initview(); initpaint(); } private void initview(){ holder = getholder(); holder.addcallback(this); holder.setkeepscreenon(true); threadpool = executors.newcachedthreadpool();//缓存线程池 } //初始化画笔属性 private void initpaint(){ //设置防锯齿 paint = new paint(paint.anti_alias_flag); //绘制图形样式 //paint.style.stroke描边 //paint.style.fill内容 //paint.style.fill_and_stroke内容+描边 paint.setstyle(paint.style.stroke); //设置画笔宽度 paint.setstrokewidth(1); } //设置图表数据源 public void setdatasource(list<chartdatainfo> data){ this.data = data; this.showdata = new arraylist<>(); if(timer!=null){ timer.cancel(); } if(canvaswidth > 0){ starttimer(); } } @override public void surfacecreated(surfaceholder surfaceholder) { canvaswidth = getwidth() - padding * 2; canvasheight = getheight() - padding * 2; starttimer(); } @override public void surfacechanged(surfaceholder surfaceholder, int i, int i1, int i2) { } @override public void surfacedestroyed(surfaceholder surfaceholder) { } int index; private void starttimer(){ index = 0; timer = new timer(); timertask task=new timertask() { @override public void run() { index += 1; showdata.clear(); showdata.addall(data.sublist(0,index)); //开启子线程 绘制页面,并使用线程池管理 threadpool.execute(new chartrunnable()); if(index>=data.size()){ timer.cancel(); } } }; timer.schedule(task, 0 , 20); } //子线程 class chartrunnable implements runnable{ @override public void run() { //获得画布 canvas = holder.lockcanvas(); //绘制曲线图形 drawchartutils.getinstance().drawchart (canvas,paint,canvaswidth,canvasheight,padding,showdata); //提交画布 holder.unlockcanvasandpost(canvas); } } }
该类主要与chartview 的差异就是,图形绘制是在子线程中进行的 相同的东西,此处不再赘述,主要讲一下差异性的内容: 1.需要实现surfaceholder.callback,重写3个方法 surfacecreated 当view创建成功会触发,指示可以做绘图工作了 surfacechanged 当view发生变化会触发,一般可以在里面数据参数的重新赋值处理; surfacedestroyed 当view销毁时会触发,一般做一些销毁前的处理工作,如线程等 2.此处的逐条加载是通过timer实现的,每一个timer周期,集合中多增加了一条数据, 同时创建一个线程绘制一次,当所有的数据绘制完毕,取消timer; 3.使用timer,每个周期都创建了一个线程,那么我们需要提高效率,应使用缓存线程池管控线程; 4.surfaceview中的画布获取方式与view中不一样 view是在ondraw方法中直接获取 surfaceview是通过holder.lockcanvas()获得,绘制完毕,必须执行提交: holder.unlockcanvasandpost(canvas); 否则,页面卡顿不动。
3.6 drawchartutils
package com.iwangzhe.mvpchart.view.customview; import android.graphics.canvas; import android.graphics.color; import android.graphics.paint; import android.graphics.path; import com.iwangzhe.mvpchart.model.chartdatainfo; import java.util.list; /** * 类:chartutils * 作者: qxc * 日期:2018/4/19. */ public class drawchartutils { private canvas canvas;//画布 private paint paint;//画笔 private int canvaswidth;//画布宽度 private int canvasheight;//画布高度 private int padding;//view边界间隔 private final string color_bg = "#343643";//背景色 private final string color_bg_line = "#999dd2";//背景色 private final string color_line = "#7176ff";//线颜色 private final string color_text = "#ffffff";//文本颜色 list<chartdatainfo> showdata;//图表数据 private static drawchartutils chartutils; public static drawchartutils getinstance(){ if(chartutils == null){ synchronized (drawchartutils.class){ if(chartutils == null){ chartutils = new drawchartutils(); } } } return chartutils; } //绘制图表 public void drawchart(canvas canvas, paint paint, int canvaswidth, int canvasheight, int padding, list<chartdatainfo> showdata) { //初始化画布、画笔等数据 this.canvas = canvas; this.paint = paint; this.canvaswidth = canvaswidth; this.canvasheight = canvasheight; this.padding = padding; this.showdata = showdata; if(canvas == null || paint==null || canvaswidth<=0 ||canvasheight<=0||showdata==null || showdata.size() ==0){ return; } //绘制图表背景 drawbg(); //绘制图表线 drawline(); } //绘制图表背景 private void drawbg(){ //绘制背景色 canvas.drawcolor(color.parsecolor(color_bg)); //绘制背景坐标轴线 drawbgaxisline(); } //绘制图表背景坐标轴线 private void drawbgaxisline(){ //5条线:表示横纵各画5条线 int linenum = 5; path path = new path(); //x、y轴间隔 int x_space = canvaswidth / linenum; int y_space = canvasheight / linenum; //画横线 for(int i=0; i<=linenum; i++){ path.moveto(0 + padding, i * y_space+ padding); path.lineto(canvaswidth+ padding, i * y_space+ padding); } //画纵线 for(int i=0; i<=linenum; i++){ path.moveto(i * x_space+ padding, 0 + padding); path.lineto(i * x_space+ padding, canvasheight+ padding); } //设置画笔宽度、样式、颜色 paint.setstrokewidth(2); paint.setstyle(paint.style.stroke); paint.setcolor(color.parsecolor(color_bg_line)); //画路径 canvas.drawpath(path, paint); } //绘制图表线(数据曲线) private void drawline(){ if(showdata == null){ return; } int size = showdata.size(); //画布自适应显示数据(即:画布的宽度应显示全量的图表数据) //x轴间隔 float x_space = canvaswidth / size; //y轴最大最小值区间对应画布高度(即画布的高度应显示全量的图表数据) float max = getmaxdata(); float min = getmindata(); float pre_x = 0; float pre_y = 0; path path = new path(); //从左向右画图 //将数值转化成对应的坐标值 for(int i=0; i<size; i++){ float num = showdata.get(i).getnum(); float x = (i*x_space) + (x_space/2)+ padding; float y = (num-min)/(max - min)*canvasheight+ padding; if(i == 0){ path.moveto(x,y); }else { path.quadto(pre_x, pre_y, x, y); } pre_x = x; pre_y = y; drawtext(string.valueof(showdata.get(i).getnum()),x,y); } //设置画笔宽度、样式、颜色 paint.setstrokewidth(5); paint.setstyle(paint.style.stroke); paint.setcolor(color.parsecolor(color_line)); //画路径 canvas.drawpath(path, paint); drawaxisxtext(); } //画坐标轴文本 private void drawaxisxtext(){ string start = showdata.get(0).getdate(); string end = showdata.get(showdata.size()-1).getdate(); //设置画笔宽度、样式、文本大小、颜色 paint.setstrokewidth(2); paint.setstyle(paint.style.fill); paint.settextsize(40); paint.setcolor(color.parsecolor(color_text)); float width_text = paint.measuretext(end); //开始文本位置 float x_start = padding; float y_start = canvasheight + padding - paint.descent() - paint.ascent() +10; //绘制开始文本 canvas.drawtext(start, x_start, y_start, paint); //结束文本位置 float x_end = canvaswidth + padding - width_text; float y_end = canvasheight + padding-paint.descent()-paint.ascent() +10; canvas.drawtext(end, x_end, y_end, paint); } //画线条文本 private void drawtext(string text, float x, float y){ //设置画笔宽度、样式、文本大小、颜色 paint.setstrokewidth(2); paint.setstyle(paint.style.fill); paint.settextsize(30); paint.setcolor(color.parsecolor(color_text)); canvas.drawtext(text, x, y, paint); } //获得最大值:用于计算、适配y轴区间 private int getmaxdata(){ int max = showdata.get(0).getnum(); for(chartdatainfo info : showdata){ max = info.getnum()>max?info.getnum():max; } return max; } //获得最小值:用于计算、适配y轴区间 private int getmindata(){ int min = showdata.get(0).getnum(); for(chartdatainfo info : showdata){ min = info.getnum()<min?info.getnum():min; } return min; } }
此类是个绘图工具类,只是包括绘制的方法,而画布、画笔等参数需要外界传入 1.getinstance方法,获得该类的单例(线程安全的单例) 2.drawchart方法,是对外提供的绘图入口方法 接收外界传参并判断合法性 调用绘制图表背景的方法 调用绘制图表线的方法 3.drawbg,绘制背景方法,包含两部分:背景色、背景边框 背景色是直接填充的方式,不用画笔 4.drawbgaxisline,绘制背景边框线 横线纵线各画5+1条,每一条线,我们可认为是画笔走过的路径, 那么,我们可以把每一条路径封装起来,放入集合中。 我们不需要自己定义这种集合,直接使用系统提供的path就可以了 path有几个常用的方法: moveto(float dx, float dy) 直接移动至某个点,中间不会产生连线; lineto(float dx, float dy) 使用直线连接至某个点; quadto(float dx1, float dy1, float dx2, float dy2) 使用曲线连接至某个点(贝塞尔曲线); cubicto(float x1,float y1,float x2,float y2,float x3,float y3) 使用曲线连接至某个点,参数更多而已; 5.画笔的设置,方法比较多,此处只列咱们用到的 paint = new paint(paint.anti_alias_flag);抗锯齿,如不设置,界面粗糙有锯齿效果; paint.setstrokewidth(2);设置描边的宽度 paint.setstyle(stroke); 设置样式,主要包括实心、描边、实心和描边3种类型,画线一般设置成描边即可; paint.setcolor(color.parsecolor(color_bg_line));//设置颜色 6.drawline画曲线,主要将数据(集合index和数值大小)分别对应到坐标系的坐标 x轴按照集合的下标平分x轴长度; y轴根据最大最小值定位数值的位置; 画线仍然使用path,要比每根曲线单独画要更合适一些; 7.绘制文本 paint.setstyle(paint.style.fill); 画笔可调整成实心,绘制文本更美观,当然也可其他类型,请根据喜好自行调整; float width_text = paint.measuretext(end); 通过设置画笔参数和文本内容,使用画笔的measuretext方法可以精确计算出文本的实际宽度; 文本的坐标与其他图形有差异,绘制位置是基于文本的baseline, 此处曲线文本的绘制时,文本位置未做精确处理; 而日期的绘制时,文本位置是做了精确处理的; float y_start = canvasheight + padding - paint.descent() - paint.ascent() +10; 如果想对文本位置控制的更精确,请参考文章:https://www.jianshu.com/p/3e48dd0547a0
总结
本次分享涉及的技术点较多,再给大家简单梳理一下:
-- mvp框架的应用;
-- 自定义view实现图表;
-- 自定义surfaceview实现图表;
-- view和surfaceview的主要差异和使用场景差异;
-- 画布、画笔、path等画图类的使用;
-- timer、runnable、线程池的应用;
其他种类的图形,思路基本上是一样的。
如果还想做图表控件的交互,如数据拖动、触摸、缩放、滑动定位等特效,需要大家再去多学学事件传递交互机制、gesturedetector、scalegesturedetector等技术。
以后要是有时间,也可再详细给大家介绍一下。
本次demo的下载地址:https://pan.baidu.com/s/1jm8lyryeyovos_iylz4dra
因为时间关系,demo没有做特别详细的测试,如果有问题请大家自行调整。