android 强制执行ondraw,Android自定义控件:图形报表的实现(折线图、曲线图、动态曲线图)(View与SurfaceView分别实现图表控件)...

图形报表很常用,因为展示数据比较直观,常见的形式有很多,如:折线图、柱形图、饼图、雷达图、股票图、还有一些3D效果的图表等。

Android中也有不少第三方图表库,但是很难兼容各种各样的需求。

如果第三方库不能满足我们的需要,那么就需要自己去写这么一个控件。

往往在APP需求给定后,很多开发者却无从下手,不知道该如何写。

今天刚好抽出点时间,做了个小Demo,给大家讲解一下。

本节,主要分享自定义图表的基本过程,不会涉及过于复杂的知识点。

咱们还是按照:需求、分析、设计、实现、总结这种方式给大家讲解吧!!!

这样大家也更容易看得懂。

需求

先上效果图:

31fb4206ecc7be6b5a11c74efaaafd09.png

4f142e63ad414c20cffb4efc5a8c5556.png

需求内容:

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

文章 -- 绘图基础 -- 绘制文本

ce5b67321fc8cbb185230cd8ad912062.png

文本绘制差异:文本绘制时并非从文本的左上角开始绘制,而是基于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 绘图工具类(绘制的代码主要封装在该类里面)

e4bf78638717dba0706b453dd9909752.png

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

实现数据层

数据层主要使用随机数模拟真实数据,没有难的技术点,咱们仅把代码贴出来吧

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 数据集合

*/

ListgetChartData(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 ListgetChartData(int size) {

Listdata = 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 ChartPresenterimport 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;

//获得图表数据

Listdata = 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(Listdata);

}

3.2 MainActivity

布局

代码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(Listdata) {

//图表控件设置数据源

cv.setDataSet(data);

}

}

3.3 SurfaceChartActivity

布局

代码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(Listdata) {

//图表控件设置数据源

cv.setDataSource(data);

}

}

3.4 ChartViewpackage 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;//画笔

Listdata;//数据

public ChartView(Context context, AttributeSet attrs) {

super(context, attrs);

//初始化画笔属性

initPaint();

}

//设置图表数据

public void setDataSet(Listdata){

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 ChartSurfaceViewpackage 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;

Listdata;//总数据

ListshowData;//当前绘制的数据

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(Listdata){

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 DrawChartUtilspackage 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";//文本颜色

ListshowData;//图表数据

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, ListshowData) {

//初始化画布、画笔等数据

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; imax?info.getNum():max;

}

return max;

}

//获得最小值:用于计算、适配Y轴区间

private int getMinData(){

int min = showData.get(0).getNum();

for(ChartDataInfo info : showData){

min = info.getNum()

}

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没有做特别详细的测试,如果有问题请大家自行调整。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值