美文网首页android收集精选案例Android开发经验谈
Android自定义控件:图形报表的实现(折线图、曲线图、动态曲

Android自定义控件:图形报表的实现(折线图、曲线图、动态曲

作者: Android技术分享 | 来源:发表于2018-04-19 17:44 被阅读617次

    图形报表很常用,因为展示数据比较直观,常见的形式有很多,如:折线图、柱形图、饼图、雷达图、股票图、还有一些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 这种图表控件如何实现?

       -- 一般需要使用画布、画笔进行绘制。
       -- 大家最好提前了解一下画布和画笔的用法。
       -- 绘制过程:使用画笔在画布上绘制图形,画布类提供了很多画图的方法,画笔可以设置各种笔触效果。
    

    3.2 背景色如何绘制?

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

    3.3 背景边框线如何实现?

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

    3.4 曲线如何绘制?

    我们可以看作二维坐标系,包含X轴和Y轴;
    
    因为需求中,图表应适配显示全量的曲线数据,也就是有多少数据,图表都能正常显示;
    那么,曲线的数据如何才能在坐标系中合适的显示呢?
    其实,也就是计算出曲线的每个数据在X轴和Y轴的位置信息,然后将这些位置点连成线就可以了;
    
    X轴应显示数据的位置:
    以图表能适配全量数据为参考,
    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应有的结构,主要包含

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

    那么,咱们直接用现成的框架吧,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.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;
        }
    }
    
    1. 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);
        }
    }
    
    1. 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没有做特别详细的测试,如果有问题请大家自行调整。

    相关文章

      网友评论

      本文标题:Android自定义控件:图形报表的实现(折线图、曲线图、动态曲

      本文链接:https://www.haomeiwen.com/subject/cahzfftx.html