经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Android » 查看文章
Android自定义控件:图形报表的实现(折线图、曲线图、动态曲线图)(View与SurfaceView分别实现图表控件)
来源:cnblogs  作者:齐行超  时间:2019/10/12 9:04:48  对本文有异议

图形报表很常用,因为展示数据比较直观,常见的形式有很多,如:折线图、柱形图、饼图、雷达图、股票图、还有一些3D效果的图表等。
Android中也有不少第三方图表库,但是很难兼容各种各样的需求。
如果第三方库不能满足我们的需要,那么就需要自己去写这么一个控件。

往往在APP需求给定后,很多开发者却无从下手,不知道该如何写。
今天刚好抽出点时间,做了个小Demo,给大家讲解一下。
本节,主要分享自定义图表的基本过程,不会涉及过于复杂的知识点。
咱们还是按照:需求、分析、设计、实现、总结这种方式给大家讲解吧!!!
这样大家也更容易看得懂。
***

需求

先上效果图:
页面1:曲线图.gif

页面2:动态曲线图.gif

需求内容:
1.数据:
-- 模拟50天的雾霾数值吧,每天的数值是一个100以内的随机数;
-- 以当前日期为最后一天,向前取50天的数据,也就是50条;
2.业务逻辑
-- 页面加载时,请求数据,展示在图表上;
-- 点击【刷新】数据,重新请求数据,展示在图表上;
3.View
-- 图表背景色为暗灰色:#343643;
-- 图表背景边框线颜色为浅蓝色:#999dd2;
-- 曲线颜色为蓝色:#7176ff;
-- 文字颜色为白色;
-- 图表可设置Padding值;
-- 图表全量显示数据,即适配显示;
-- 曲线上的数值文本显示在对应的位置;
-- X坐标轴左右分别显示 开始和结束的日期,并与左右边框线对齐;
-- 图表应支持两种查看方式:整体加载(全量加载) 和 逐条加载(动态加载)


分析

1.数据比较简单,做个随机数即可,略;
2.业务逻辑,较简单,略;
3.View,本节的重点,需要详细分析一下:
3.1 这种图表控件如何实现?

  1. 一般做法:使用画布、画笔进行绘制。
  2. 如何绘制:使用画笔在画布上绘制图形
  3. (画布类提供了很多画图的方法,画笔可以设置各种笔触效果)。
  4. 建议:大家最好提前了解一下画布和画笔的用法。

3.2 背景色如何绘制?

  1. canvas.drawColor(参数:颜色)即可,很简单,即:画布直接填充背景颜色,不用画笔。

3.3 背景边框线如何实现?

  1. 方案1:先定义路径Path,记录每一个跟边框线的信息,再使用canvas.drawPath进行绘制;
  2. 方案2:使用canvas.drawLine分别绘制每一条横线和纵线;
  3. 建议:多线条时,canvas.drawPath管理更简单,绘制会更方便一些。

3.4 曲线如何绘制?

  1. 我们可以看作二维坐标系,包含X轴和Y轴;
  1. 那么,曲线的数据如何才能在坐标系中合适的显示呢?
  2. 其实不难,我们可以根据画布大小(或控件大小(如果画布尺寸等于控件尺寸)),
  3. 计算出曲线的每个数据在X轴和Y轴的位置信息,然后将这些位置点连成线就可以了;
  1. X轴应显示数据的位置:
  2. 以图表能适配全量数据为参考(也就是能显示全部的数据,本Demo中就是50条雾霾数据的点):
  3. X轴的长度应与数据总条数对应,那么每一条数据在X轴的位置,应是:
  4. 每条数据在X轴的间隔 = X轴长度 / 数据条数;
  5. 每条数据在X轴的位置 = N条数据 * 间隔;
  1. Y轴应显示数据的位置:
  2. 以图表能适配全量数据为参考,
  3. Y轴的区域应能包含所有数据大小,那么,我们需要先获得数据的最大最小值与之对应,
  4. 每一条数据numY轴的位置,应是:
  5. 每条数据的Y轴比率 = (num - min ) / (max - min);
  6. 每条数据在Y轴的位置 = 比率 * Y轴长度;
  1. 获得了数据在XY轴的位置,我们就可以绘制曲线了,
  2. 此处仍然使用Path收集每一个数据点的位置,同时使用曲线进行连接,
  3. path.quadTox1, y1,x2,y2)(该方法后面有介绍);
  4. 然后再画布上绘制曲线路径:canvas.drawPathpath,paint);

3.5 如何绘制文本?

  1. 使用canvas.drawText(text, x, y, paint);
  2. 不过x,y的位置的计算,稍微麻烦一些,大家可以看一下这篇文章的相关介绍:
  3. https://www.jianshu.com/p/3e48dd0547a0
  4. 文章 -- 绘图基础 -- 绘制文本

文本绘制原理
文本绘制差异:

  1. 文本绘制时并非从文本的左上角开始绘制,而是基于Baseline开始绘制。
  2. 举例:
  3. 如果我们想在自定义控件左上角位置绘制文本,
  4. 可能会这么写canvas.drawText("MfgiA", 0, 0, paint);
  5. 但是这么写,等运行出来,我们发现该控件左上角只会显示Baseline下面的内容,
  6. 也就只能看到字母g的下半部分,
  7. 而其他部分,因为超出了自定义控件上边界,所以没有被绘制出来。

如果不明白也不要紧,我们先学习主要的知识。
如果想把文本位置控制的特别精确,请务必参考该文章。

3.6 动态图表如何绘制?
图表的动态效果其实就是每隔一定时间重绘一次,也就是动态了(视频效果也是这么个原理);
之所以做成两种效果(非动态/动态),主要是让大家了解一下View和SurfaceView的用法差异。
主要差异如下:

  1. View
  2. -- 仅能在主线程中刷新。
  3. 缺点:如果绘制内容过多或频率过高,会影响主线程FPS,造成页面卡顿
  4. -- 使用了单缓冲;
  5. 缓冲可以理解成对处理的包装,举个简单易懂点的例子:
  6. 工人搬砖
  7. 工人有10000块砖要从A区搬到B区,他每次搬一块,要搬10000次,
  8. 为了不想来回跑这么多次,工人想了个办法,找了个筐来背砖,每筐可以背100块,
  9. 这样他就来回跑100次就行了,提高了搬砖效率。那么,这个筐呢就是一个缓冲处理。
  10. View的绘制上也很容易理解,例如:我们使用画笔按序(中间可有停顿)绘制多个图形,
  11. 但是View并没有一个个的去绘制,而是在一次draw方法中,全部绘制了出来。
  12. 因为,View也使用了缓冲处理。
  13. SurfaceView
  14. -- 可在子线程中刷新;
  15. 如果绘制的内容少,不建议使用,因为创建线程和缓冲区,也增加了内存。
  16. 反之,推荐使用,但是要注意线程的管控。
  17. -- 使用了双缓冲;
  18. 继续以工人搬砖的例子讲解。
  19. 工人转身忽然看到了一辆卡车(一车能装>1万块),心想这不更省事了么,
  20. 于是他先把一框框砖搬到了车上,再把车开到B区,卸砖。
  21. 这辆车也就相当于第二次缓冲了。
  22. 在控件绘制时实现双缓冲一般可以这么做:
  23. 1.新建一个临时图片,并创建其临时画布(画布相当于那辆卡车);
  24. 2.将我们想绘制的内容,先绘制到临时图片的画布上(即图片上)
  25. 3.在控件需要绘制时,再把图片绘制到控件的真正画布上;
  1. 经过上面的对比分析,我们可以得出结论:
  2. 1.全量加载的图表(曲线图),使用ViewSurfaceView来绘制都是可以的
  3. 因为:绘制的信息适量,没有特别的性能要求。
  4. 2.逐条加载的图表(动态曲线图),我们尽量使用SurfaceView来绘制
  5. 因为:如果在View里使用线程sleep控制逐条加载,会导致主线程阻塞
  6. (也就是页面看着卡顿半天,等阻塞恢复之后,再忽然绘制出来的效果)。
  7. 如果想不卡顿,只能在View中使用线程或Timer来处理逐条效果,然后再与主线程进行通信。
  8. 与其这么麻烦,我们不如使用SurfaceView,直接能在子线程中刷新View不是更好吗。

看完上面的介绍,相信大家对View与SurfaceView的区别和用法,也应该了解一些了。
那么,咱们开始下一步吧。


设计

这一个功能实现相对复杂一些,我们最好对Demo进行一个简单的分层或模块设计。
分析我们的Demo应有的结构,主要包含

  1. 两种自定义图表控件(View和SurfaceView)、
  2. 一些简单的业务逻辑、
  3. 数据的处理。

那么,咱们直接用现成的框架吧,MVC、MVP都是可以的,不过MVC、MVP用哪个好呢?
我们直接使用MVP吧,解耦比MVC更好一些。
此处就不画架构图了,直接文本表示吧:

M(数据层):

  1. 1. IChartData.java 图表数据接口(提供了一个方法:获得图表数据)
  2. 2. ChartDataImpl.java 图表数据实现类(实现了上面的接口)
  3. 3. ChartDataInfo.java 图表数据实体类(封装了两个属性:日期和数值)
  4. 4. ChartDateUtils.java 工具类(主要是日期格式的处理)

P(Presenter中间层):

  1. 1.ChartPresenter.java 用于连接MV层,负责业务逻辑的处理,此处也就是:获得了数据,交给UI

V(UI层)

  1. 1. IChartUI.java UI接口,提供了显示图表的方法,供Presenter使用
  2. 2. MainActivity.java UI接口的实现类,用于曲线图的展示与交互
  3. 3. SurfaceChartActivity.java UI接口的实现类,用于动态曲线图的展示与交互
  4. 4. ChartView.java 曲线图控件(直接使用画布、画笔绘制)
  5. 5. ChartSurfaceView.java 动态曲线图控件(使用Timer、线程池、线程、画布、画笔绘制)
  6. 6. DrawChartUtils.java 绘图工具类(绘制的代码主要封装在该类里面)

代码结构图

功能如何实现已经设计好了,那么,开始下一步吧。
***

实现

  1. 数据层
    数据层主要使用随机数模拟真实数据,没有难的技术点,咱们仅把代码贴出来吧
    1.1 图表数据实体类
  1. /**
  2. * 类:ChartDataInfo 图表数据实体类
  3. * 作者: qxc
  4. * 日期:2018/4/18.
  5. */
  6. public class ChartDataInfo {
  7. private String date;
  8. private int num;
  9. public ChartDataInfo(String date, int num) {
  10. this.date = date;
  11. this.num = num;
  12. }
  13. public String getDate() {
  14. return date;
  15. }
  16. public void setDate(String date) {
  17. this.date = date;
  18. }
  19. public int getNum() {
  20. return num;
  21. }
  22. public void setNum(int num) {
  23. this.num = num;
  24. }
  25. }

1.2 图表数据接口

  1. import java.util.List;
  2. /**
  3. * 类:IChartData 图表数据接口
  4. * 作者: qxc
  5. * 日期:2018/4/18.
  6. */
  7. public interface IChartData {
  8. /**
  9. * 获得图表数据
  10. * @param size 数据条数
  11. * @return 数据集合
  12. */
  13. List<ChartDataInfo> getChartData(int size);
  14. }

1.3 图表数据实现类

  1. import java.util.ArrayList;
  2. import java.util.List;
  3. import java.util.Random;
  4. /**
  5. * 类:ChartDataImpl 图表数据实现类
  6. * 作者: qxc
  7. * 日期:2018/4/18.
  8. */
  9. public class ChartDataImpl implements IChartData{
  10. private int maxNum = 100;
  11. /**
  12. * 返回随机的图表数据
  13. * @param size 数据条数
  14. * @return 图表数据集合
  15. */
  16. @Override
  17. public List<ChartDataInfo> getChartData(int size) {
  18. List<ChartDataInfo> data = new ArrayList<>();
  19. Random random = new Random();
  20. random.setSeed(ChartDateUtils.getDateNow());
  21. //返回maxNum以内的随机数
  22. for(int i = size-1; i>=0 ; i--){
  23. ChartDataInfo dataInfo = new ChartDataInfo(ChartDateUtils.getDate(i), random.nextInt(maxNum));
  24. data.add(dataInfo);
  25. }
  26. return data;
  27. }
  28. }

1.4 数据层工具类

  1. import java.text.SimpleDateFormat;
  2. import java.util.Calendar;
  3. import java.util.Date;
  4. /**
  5. * 类:DateUtils 数据层工具类
  6. * 1.日期的处理
  7. * 2.
  8. * 作者: qxc
  9. * 日期:2018/4/18.
  10. */
  11. public class ChartDateUtils {
  12. public static long getDateNow(){
  13. Date date = new Date();
  14. return date.getTime();
  15. }
  16. public static String getDate(int day){
  17. Calendar calendar = Calendar.getInstance();
  18. SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
  19. calendar.add(Calendar.DATE, -day);
  20. String date = sdf.format(calendar.getTime());
  21. return date;
  22. }
  23. }
  1. Presenter层
    这一层就是标准的Presenter,持有M和V的接口,对他们的业务逻辑进行处理。
    2.1 ChartPresenter
  1. import com.iwangzhe.mvpchart.model.ChartDataImpl;
  2. import com.iwangzhe.mvpchart.model.ChartDataInfo;
  3. import com.iwangzhe.mvpchart.model.IChartData;
  4. import com.iwangzhe.mvpchart.view.IChartUI;
  5. import java.util.List;
  6. /**
  7. * 类:ChartPresenter
  8. * 作者: qxc
  9. * 日期:2018/4/18.
  10. */
  11. public class ChartPresenter {
  12. private IChartUI iChartView;
  13. private IChartData iChartData;
  14. public ChartPresenter(IChartUI iChartView) {
  15. this.iChartView = iChartView;
  16. this.iChartData = new ChartDataImpl();
  17. }
  18. //获取图表数据的业务逻辑
  19. public void getChartData(){
  20. //请求的数据数量
  21. int size = 50;
  22. //获得图表数据
  23. List<ChartDataInfo> data = iChartData.getChartData(size);
  24. //把数据设置给UI
  25. iChartView.showChartData(data);
  26. }
  27. }
  1. UI层(View)
    绘图的技术是本文的核心点,需要重点讲解
    3.1 IChartUI 接口
  1. package com.iwangzhe.mvpchart.view;
  2. import com.iwangzhe.mvpchart.model.ChartDataInfo;
  3. import java.util.List;
  4. /**
  5. * 类:IChartView
  6. * 作者: qxc
  7. * 日期:2018/4/18.
  8. */
  9. public interface IChartUI {
  10. /**
  11. * 显示图表
  12. * @param data 数据
  13. */
  14. void showChartData(List<ChartDataInfo> data);
  15. }

3.2 MainActivity
布局

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:id="@+id/activity_main"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. android:background="#000000">
  7. <Button
  8. android:id="@+id/btn"
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:background="#343643"
  12. android:layout_marginLeft="8dp"
  13. android:layout_marginTop="10dp"
  14. android:text=" 刷新ChartView数据 "
  15. android:textColor="#ffffff"
  16. android:textSize="18sp"/>
  17. <Button
  18. android:id="@+id/btnSurface"
  19. android:layout_width="wrap_content"
  20. android:layout_height="wrap_content"
  21. android:background="#343643"
  22. android:layout_toRightOf="@+id/btn"
  23. android:layout_marginLeft="8dp"
  24. android:layout_marginTop="10dp"
  25. android:text=" 使用SurfaceView展示图表 "
  26. android:textColor="#ffffff"
  27. android:textSize="18sp"/>
  28. <com.iwangzhe.mvpchart.view.customView.ChartView
  29. android:id="@+id/cv"
  30. android:layout_below="@+id/btn"
  31. android:layout_width="match_parent"
  32. android:layout_height="match_parent"
  33. android:layout_margin="8dp"/>
  34. </RelativeLayout>

代码

  1. package com.iwangzhe.mvpchart.view;
  2. import android.app.Activity;
  3. import android.content.Intent;
  4. import android.os.Bundle;
  5. import android.util.Log;
  6. import android.view.View;
  7. import android.widget.Button;
  8. import com.iwangzhe.mvpchart.R;
  9. import com.iwangzhe.mvpchart.model.ChartDataInfo;
  10. import com.iwangzhe.mvpchart.presenter.ChartPresenter;
  11. import com.iwangzhe.mvpchart.view.customView.ChartView;
  12. import java.util.List;
  13. public class MainActivity extends Activity implements IChartUI {
  14. ChartPresenter chartPresenter;
  15. ChartView cv;
  16. Button btn;
  17. Button btnSurface;
  18. @Override
  19. protected void onCreate(Bundle savedInstanceState) {
  20. super.onCreate(savedInstanceState);
  21. setContentView(R.layout.activity_main);
  22. //初始化presenter
  23. chartPresenter = new ChartPresenter(this);
  24. //初始化控件
  25. initView();
  26. //初始化数据
  27. initData();
  28. //初始化事件
  29. initEvent();
  30. }
  31. //初始化控件
  32. private void initView() {
  33. cv = (ChartView) findViewById(R.id.cv);
  34. btn = (Button) findViewById(R.id.btn);
  35. btnSurface = (Button) findViewById(R.id.btnSurface);
  36. }
  37. //初始化数据
  38. private void initData() {
  39. chartPresenter.getChartData();//请求数据
  40. }
  41. //初始化事件
  42. private void initEvent() {
  43. //刷新数据
  44. btn.setOnClickListener(new View.OnClickListener() {
  45. @Override
  46. public void onClick(View view) {
  47. chartPresenter.getChartData();//重新请求数据(刷新数据)
  48. }
  49. });
  50. //跳转到动态曲线页面
  51. btnSurface.setOnClickListener(new View.OnClickListener(){
  52. @Override
  53. public void onClick(View view) {
  54. Intent intent = new Intent(MainActivity.this, SurfaceChartActivity.class);
  55. startActivity(intent);
  56. }
  57. });
  58. }
  59. //P层的数据回调
  60. @Override
  61. public void showChartData(List<ChartDataInfo> data) {
  62. //图表控件设置数据源
  63. cv.setDataSet(data);
  64. }
  65. }

3.3 SurfaceChartActivity
布局

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:id="@+id/activity_main"
  4. android:layout_width="match_parent"
  5. android:layout_height="match_parent"
  6. android:background="#000000">
  7. <Button
  8. android:id="@+id/btn"
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:background="#343643"
  12. android:layout_marginLeft="8dp"
  13. android:layout_marginTop="10dp"
  14. android:text=" 刷新SurfaceView数据 "
  15. android:textColor="#ffffff"
  16. android:textSize="18sp"/>
  17. <com.iwangzhe.mvpchart.view.customView.ChartSurfaceView
  18. android:id="@+id/cv"
  19. android:layout_below="@+id/btn"
  20. android:layout_width="match_parent"
  21. android:layout_height="match_parent"
  22. android:layout_margin="8dp"/>
  23. </RelativeLayout>

代码

  1. package com.iwangzhe.mvpchart.view;
  2. import android.app.Activity;
  3. import android.os.Bundle;
  4. import android.util.Log;
  5. import android.view.View;
  6. import android.widget.Button;
  7. import com.iwangzhe.mvpchart.R;
  8. import com.iwangzhe.mvpchart.model.ChartDataInfo;
  9. import com.iwangzhe.mvpchart.presenter.ChartPresenter;
  10. import com.iwangzhe.mvpchart.view.customView.ChartSurfaceView;
  11. import java.util.List;
  12. /**
  13. * 类:SurfaceChartActivity
  14. * 作者: qxc
  15. * 日期:2018/4/19.
  16. */
  17. public class SurfaceChartActivity extends Activity implements IChartUI{
  18. ChartPresenter chartPresenter;
  19. ChartSurfaceView cv;
  20. Button btn;
  21. @Override
  22. protected void onCreate(Bundle savedInstanceState) {
  23. super.onCreate(savedInstanceState);
  24. setContentView(R.layout.activity_surface_chart);
  25. //初始化presenter
  26. chartPresenter = new ChartPresenter(this);
  27. //初始化控件
  28. initView();
  29. //初始化数据
  30. initData();
  31. //初始化事件
  32. initEvent();
  33. }
  34. //初始化控件
  35. private void initView() {
  36. cv = (ChartSurfaceView) findViewById(R.id.cv);
  37. btn = (Button) findViewById(R.id.btn);
  38. }
  39. //初始化数据
  40. private void initData() {
  41. chartPresenter.getChartData();//请求数据
  42. }
  43. //初始化事件
  44. private void initEvent() {
  45. //刷新数据
  46. btn.setOnClickListener(new View.OnClickListener() {
  47. @Override
  48. public void onClick(View view) {
  49. chartPresenter.getChartData();//重新请求数据(刷新数据)
  50. }
  51. });
  52. }
  53. @Override
  54. public void showChartData(List<ChartDataInfo> data) {
  55. //图表控件设置数据源
  56. cv.setDataSource(data);
  57. }
  58. }

3.4 ChartView

  1. package com.iwangzhe.mvpchart.view.customView;
  2. import android.content.Context;
  3. import android.graphics.Canvas;
  4. import android.graphics.Paint;
  5. import android.util.AttributeSet;
  6. import android.view.View;
  7. import com.iwangzhe.mvpchart.model.ChartDataInfo;
  8. import java.util.List;
  9. /**
  10. * 类:ChartView
  11. * 作者: qxc
  12. * 日期:2018/4/18.
  13. */
  14. public class ChartView extends View{
  15. int canvasWidth;//画布宽度
  16. int canvasHeight;//画布高度
  17. int padding = 100;//边界间隔
  18. Paint paint;//画笔
  19. List<ChartDataInfo> data;//数据
  20. public ChartView(Context context, AttributeSet attrs) {
  21. super(context, attrs);
  22. //初始化画笔属性
  23. initPaint();
  24. }
  25. //设置图表数据
  26. public void setDataSet(List<ChartDataInfo> data){
  27. this.data = data;
  28. //强制重绘
  29. invalidate();
  30. }
  31. //初始化画笔属性
  32. private void initPaint(){
  33. //设置防锯齿
  34. paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  35. //绘制图形样式
  36. //Paint.Style.STROKE描边
  37. //Paint.Style.FILL内容
  38. //Paint.Style.FILL_AND_STROKE内容+描边
  39. paint.setStyle(Paint.Style.STROKE);
  40. //设置画笔宽度
  41. paint.setStrokeWidth(1);
  42. }
  43. //每一次外观变化,都会调用该方法
  44. @Override
  45. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  46. super.onSizeChanged(w, h, oldw, oldh);
  47. //获得画布宽度
  48. this.canvasWidth = getWidth() - padding * 2;
  49. //获得画布高度
  50. this.canvasHeight = getHeight() - padding * 2;
  51. }
  52. @Override
  53. protected void onDraw(Canvas canvas) {
  54. //每次重绘,绘制图表信息
  55. DrawChartUtils.getInstance().drawChart(canvas, paint, canvasWidth,canvasHeight,padding,data);
  56. }
  57. }
  1. 该类中,
  2. 1.onSizeChanged中获得了画布的宽度和高度,作为背景边线和曲线数据的绘制区域
  3. 2.画布的宽度和高度减去了padding信息(两边都需要有padding,所以乘以了2
  4. 3.View创建时,初始化了一支画笔,设置了画笔的一些属性
  5. 4.onSizeChanged方法执行后,都会执行onDraw方法进行绘制,该方法中可以获得画布
  6. 5.每次刷新数据,调用setDataSet方法后,也会强制执行onDraw方法进行绘制,因为invalidate方法会强制重绘
  7. 6.我们统一在onDraw方法中绘制图表信息,而图表信息的绘制封装在DrawChartUtils类中

3.5 ChartSurfaceView

  1. package com.iwangzhe.mvpchart.view.customView;
  2. import android.content.Context;
  3. import android.graphics.Canvas;
  4. import android.graphics.Paint;
  5. import android.util.AttributeSet;
  6. import android.view.SurfaceHolder;
  7. import android.view.SurfaceView;
  8. import com.iwangzhe.mvpchart.model.ChartDataInfo;
  9. import java.util.ArrayList;
  10. import java.util.List;
  11. import java.util.Timer;
  12. import java.util.TimerTask;
  13. import java.util.concurrent.ExecutorService;
  14. import java.util.concurrent.Executors;
  15. /**
  16. * 类:ChartSurfaceView
  17. * 作者: qxc
  18. * 日期:2018/4/19.
  19. */
  20. public class ChartSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
  21. SurfaceHolder holder;
  22. Timer timer;
  23. List<ChartDataInfo> data;//总数据
  24. List<ChartDataInfo> showData;//当前绘制的数据
  25. ExecutorService threadPool;//线程池
  26. Canvas canvas;//画布
  27. Paint paint;//画笔
  28. int canvasWidth;//画布宽度
  29. int canvasHeight;//画布高度
  30. int padding = 100;//边界间隔
  31. public ChartSurfaceView(Context context, AttributeSet attrs) {
  32. super(context, attrs);
  33. initView();
  34. initPaint();
  35. }
  36. private void initView(){
  37. holder = getHolder();
  38. holder.addCallback(this);
  39. holder.setKeepScreenOn(true);
  40. threadPool = Executors.newCachedThreadPool();//缓存线程池
  41. }
  42. //初始化画笔属性
  43. private void initPaint(){
  44. //设置防锯齿
  45. paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  46. //绘制图形样式
  47. //Paint.Style.STROKE描边
  48. //Paint.Style.FILL内容
  49. //Paint.Style.FILL_AND_STROKE内容+描边
  50. paint.setStyle(Paint.Style.STROKE);
  51. //设置画笔宽度
  52. paint.setStrokeWidth(1);
  53. }
  54. //设置图表数据源
  55. public void setDataSource(List<ChartDataInfo> data){
  56. this.data = data;
  57. this.showData = new ArrayList<>();
  58. if(timer!=null){
  59. timer.cancel();
  60. }
  61. if(canvasWidth > 0){
  62. startTimer();
  63. }
  64. }
  65. @Override
  66. public void surfaceCreated(SurfaceHolder surfaceHolder) {
  67. canvasWidth = getWidth() - padding * 2;
  68. canvasHeight = getHeight() - padding * 2;
  69. startTimer();
  70. }
  71. @Override
  72. public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
  73. }
  74. @Override
  75. public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
  76. }
  77. int index;
  78. private void startTimer(){
  79. index = 0;
  80. timer = new Timer();
  81. TimerTask task=new TimerTask() {
  82. @Override
  83. public void run() {
  84. index += 1;
  85. showData.clear();
  86. showData.addAll(data.subList(0,index));
  87. //开启子线程 绘制页面,并使用线程池管理
  88. threadPool.execute(new ChartRunnable());
  89. if(index>=data.size()){
  90. timer.cancel();
  91. }
  92. }
  93. };
  94. timer.schedule(task, 0 , 20);
  95. }
  96. //子线程
  97. class ChartRunnable implements Runnable{
  98. @Override
  99. public void run() {
  100. //获得画布
  101. canvas = holder.lockCanvas();
  102. //绘制曲线图形
  103. DrawChartUtils.getInstance().drawChart
  104. (canvas,paint,canvasWidth,canvasHeight,padding,showData);
  105. //提交画布
  106. holder.unlockCanvasAndPost(canvas);
  107. }
  108. }
  109. }
  1. 该类主要与ChartView 的差异就是,图形绘制是在子线程中进行的
  2. 相同的东西,此处不再赘述,主要讲一下差异性的内容:
  3. 1.需要实现SurfaceHolder.Callback,重写3个方法
  4. surfaceCreated View创建成功会触发,指示可以做绘图工作了
  5. surfaceChanged View发生变化会触发,一般可以在里面数据参数的重新赋值处理;
  6. surfaceDestroyed View销毁时会触发,一般做一些销毁前的处理工作,如线程等
  7. 2.此处的逐条加载是通过Timer实现的,每一个Timer周期,集合中多增加了一条数据,
  8. 同时创建一个线程绘制一次,当所有的数据绘制完毕,取消timer;
  9. 3.使用timer,每个周期都创建了一个线程,那么我们需要提高效率,应使用缓存线程池管控线程;
  10. 4.SurfaceView中的画布获取方式与View中不一样
  11. View是在onDraw方法中直接获取
  12. SurfaceView是通过holder.lockCanvas()获得,绘制完毕,必须执行提交:
  13. holder.unlockCanvasAndPost(canvas);
  14. 否则,页面卡顿不动。

3.6 DrawChartUtils

  1. package com.iwangzhe.mvpchart.view.customView;
  2. import android.graphics.Canvas;
  3. import android.graphics.Color;
  4. import android.graphics.Paint;
  5. import android.graphics.Path;
  6. import com.iwangzhe.mvpchart.model.ChartDataInfo;
  7. import java.util.List;
  8. /**
  9. * 类:ChartUtils
  10. * 作者: qxc
  11. * 日期:2018/4/19.
  12. */
  13. public class DrawChartUtils {
  14. private Canvas canvas;//画布
  15. private Paint paint;//画笔
  16. private int canvasWidth;//画布宽度
  17. private int canvasHeight;//画布高度
  18. private int padding;//View边界间隔
  19. private final String color_bg = "#343643";//背景色
  20. private final String color_bg_line = "#999dd2";//背景色
  21. private final String color_line = "#7176ff";//线颜色
  22. private final String color_text = "#ffffff";//文本颜色
  23. List<ChartDataInfo> showData;//图表数据
  24. private static DrawChartUtils chartUtils;
  25. public static DrawChartUtils getInstance(){
  26. if(chartUtils == null){
  27. synchronized (DrawChartUtils.class){
  28. if(chartUtils == null){
  29. chartUtils = new DrawChartUtils();
  30. }
  31. }
  32. }
  33. return chartUtils;
  34. }
  35. //绘制图表
  36. public void drawChart(Canvas canvas, Paint paint, int canvasWidth, int canvasHeight, int padding, List<ChartDataInfo> showData) {
  37. //初始化画布、画笔等数据
  38. this.canvas = canvas;
  39. this.paint = paint;
  40. this.canvasWidth = canvasWidth;
  41. this.canvasHeight = canvasHeight;
  42. this.padding = padding;
  43. this.showData = showData;
  44. if(canvas == null || paint==null || canvasWidth<=0 ||canvasHeight<=0||showData==null || showData.size() ==0){
  45. return;
  46. }
  47. //绘制图表背景
  48. drawBg();
  49. //绘制图表线
  50. drawLine();
  51. }
  52. //绘制图表背景
  53. private void drawBg(){
  54. //绘制背景色
  55. canvas.drawColor(Color.parseColor(color_bg));
  56. //绘制背景坐标轴线
  57. drawBgAxisLine();
  58. }
  59. //绘制图表背景坐标轴线
  60. private void drawBgAxisLine(){
  61. //5条线:表示横纵各画5条线
  62. int lineNum = 5;
  63. Path path = new Path();
  64. //x、y轴间隔
  65. int x_space = canvasWidth / lineNum;
  66. int y_space = canvasHeight / lineNum;
  67. //画横线
  68. for(int i=0; i<=lineNum; i++){
  69. path.moveTo(0 + padding, i * y_space+ padding);
  70. path.lineTo(canvasWidth+ padding, i * y_space+ padding);
  71. }
  72. //画纵线
  73. for(int i=0; i<=lineNum; i++){
  74. path.moveTo(i * x_space+ padding, 0 + padding);
  75. path.lineTo(i * x_space+ padding, canvasHeight+ padding);
  76. }
  77. //设置画笔宽度、样式、颜色
  78. paint.setStrokeWidth(2);
  79. paint.setStyle(Paint.Style.STROKE);
  80. paint.setColor(Color.parseColor(color_bg_line));
  81. //画路径
  82. canvas.drawPath(path, paint);
  83. }
  84. //绘制图表线(数据曲线)
  85. private void drawLine(){
  86. if(showData == null){
  87. return;
  88. }
  89. int size = showData.size();
  90. //画布自适应显示数据(即:画布的宽度应显示全量的图表数据)
  91. //x轴间隔
  92. float x_space = canvasWidth / size;
  93. //y轴最大最小值区间对应画布高度(即画布的高度应显示全量的图表数据)
  94. float max = getMaxData();
  95. float min = getMinData();
  96. float pre_x = 0;
  97. float pre_y = 0;
  98. Path path = new Path();
  99. //从左向右画图
  100. //将数值转化成对应的坐标值
  101. for(int i=0; i<size; i++){
  102. float num = showData.get(i).getNum();
  103. float x = (i*x_space) + (x_space/2)+ padding;
  104. float y = (num-min)/(max - min)*canvasHeight+ padding;
  105. if(i == 0){
  106. path.moveTo(x,y);
  107. }else {
  108. path.quadTo(pre_x, pre_y, x, y);
  109. }
  110. pre_x = x;
  111. pre_y = y;
  112. drawText(String.valueOf(showData.get(i).getNum()),x,y);
  113. }
  114. //设置画笔宽度、样式、颜色
  115. paint.setStrokeWidth(5);
  116. paint.setStyle(Paint.Style.STROKE);
  117. paint.setColor(Color.parseColor(color_line));
  118. //画路径
  119. canvas.drawPath(path, paint);
  120. drawAxisXText();
  121. }
  122. //画坐标轴文本
  123. private void drawAxisXText(){
  124. String start = showData.get(0).getDate();
  125. String end = showData.get(showData.size()-1).getDate();
  126. //设置画笔宽度、样式、文本大小、颜色
  127. paint.setStrokeWidth(2);
  128. paint.setStyle(Paint.Style.FILL);
  129. paint.setTextSize(40);
  130. paint.setColor(Color.parseColor(color_text));
  131. float width_text = paint.measureText(end);
  132. //开始文本位置
  133. float x_start = padding;
  134. float y_start = canvasHeight + padding - paint.descent() - paint.ascent() +10;
  135. //绘制开始文本
  136. canvas.drawText(start, x_start, y_start, paint);
  137. //结束文本位置
  138. float x_end = canvasWidth + padding - width_text;
  139. float y_end = canvasHeight + padding-paint.descent()-paint.ascent() +10;
  140. canvas.drawText(end, x_end, y_end, paint);
  141. }
  142. //画线条文本
  143. private void drawText(String text, float x, float y){
  144. //设置画笔宽度、样式、文本大小、颜色
  145. paint.setStrokeWidth(2);
  146. paint.setStyle(Paint.Style.FILL);
  147. paint.setTextSize(30);
  148. paint.setColor(Color.parseColor(color_text));
  149. canvas.drawText(text, x, y, paint);
  150. }
  151. //获得最大值:用于计算、适配Y轴区间
  152. private int getMaxData(){
  153. int max = showData.get(0).getNum();
  154. for(ChartDataInfo info : showData){
  155. max = info.getNum()>max?info.getNum():max;
  156. }
  157. return max;
  158. }
  159. //获得最小值:用于计算、适配Y轴区间
  160. private int getMinData(){
  161. int min = showData.get(0).getNum();
  162. for(ChartDataInfo info : showData){
  163. min = info.getNum()<min?info.getNum():min;
  164. }
  165. return min;
  166. }
  167. }
  1. 此类是个绘图工具类,只是包括绘制的方法,而画布、画笔等参数需要外界传入
  2. 1.getInstance方法,获得该类的单例(线程安全的单例)
  3. 2.drawChart方法,是对外提供的绘图入口方法
  4. 接收外界传参并判断合法性
  5. 调用绘制图表背景的方法
  6. 调用绘制图表线的方法
  7. 3.drawBg,绘制背景方法,包含两部分:背景色、背景边框
  8. 背景色是直接填充的方式,不用画笔
  9. 4.drawBgAxisLine,绘制背景边框线
  10. 横线纵线各画5+1条,每一条线,我们可认为是画笔走过的路径,
  11. 那么,我们可以把每一条路径封装起来,放入集合中。
  12. 我们不需要自己定义这种集合,直接使用系统提供的Path就可以了
  13. Path有几个常用的方法:
  14. MoveTo(float dx, float dy) 直接移动至某个点,中间不会产生连线;
  15. LineTo(float dx, float dy) 使用直线连接至某个点;
  16. QuadTo(float dx1, float dy1, float dx2, float dy2) 使用曲线连接至某个点(贝塞尔曲线);
  17. CubicTo(float x1,float y1,float x2,float y2,float x3,float y3)
  18. 使用曲线连接至某个点,参数更多而已;
  19. 5.画笔的设置,方法比较多,此处只列咱们用到的
  20. paint = new Paint(Paint.ANTI_ALIAS_FLAG);抗锯齿,如不设置,界面粗糙有锯齿效果;
  21. paint.setStrokeWidth(2);设置描边的宽度
  22. paint.setStyle(STROKE);
  23. 设置样式,主要包括实心、描边、实心和描边3种类型,画线一般设置成描边即可;
  24. paint.setColor(Color.parseColor(color_bg_line));//设置颜色
  25. 6.drawLine画曲线,主要将数据(集合index和数值大小)分别对应到坐标系的坐标
  26. X轴按照集合的下标平分X轴长度;
  27. Y轴根据最大最小值定位数值的位置;
  28. 画线仍然使用Path,要比每根曲线单独画要更合适一些;
  29. 7.绘制文本
  30. paint.setStyle(Paint.Style.FILL);
  31. 画笔可调整成实心,绘制文本更美观,当然也可其他类型,请根据喜好自行调整;
  32. float width_text = paint.measureText(end);
  33. 通过设置画笔参数和文本内容,使用画笔的measureText方法可以精确计算出文本的实际宽度;
  34. 文本的坐标与其他图形有差异,绘制位置是基于文本的Baseline
  35. 此处曲线文本的绘制时,文本位置未做精确处理;
  36. 而日期的绘制时,文本位置是做了精确处理的;
  37. float y_start = canvasHeight + padding - paint.descent() - paint.ascent() +10;
  38. 如果想对文本位置控制的更精确,请参考文章: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没有做特别详细的测试,如果有问题请大家自行调整。

原文链接:http://www.cnblogs.com/qixingchao/p/11652384.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号