您的位置:首页 > 移动开发 > Android开发

Android中自定义折线图

2016-04-30 15:31 453 查看
有时候,我们在做开发的时候,需要让用户更直观的看到数据变化,而又不应该给其提供一堆表格显示,有时候就需要用到,类似Excel中的图表,可是Google官方并没有提供自带的图表控件,这时候就需要我们自定义一个图表控件用于显示,我们想要的效果。(直接上图)如下:



先分析一下,效果图(忽略标题栏)。最上面是运动天数共31天,间隔一定距离向右偏移。向下是Y轴虚线图,数量点图标及折线趋势,最后做了一下区域渲染。

此自定义控件需要继承View,一般情况下(不需要特殊要求的时候)我们自定义控件是都是继承自View/ViewGroup。当需要在某个控件基础上扩展的时候,才继承View/ViewGroup的子类。继承后需要从写onDraw()方法,在此方法中绘制图像,当然不可避免的需要用到几个东西:CanvasPaintPath

Paint:英文意思为油漆.绘画.描绘。在计算机中称为画笔。作用就类似于绘制图像要用的一样笔,当然可以为paint设置不同的颜色,实心或空心,透明度,粗细,抗锯齿等等,这些都可以通过set系列方法设置。比如,P.setAntiAlias(true)设置抗锯齿;P.setStrokeWidth(float)设置画笔粗细;P.setTextSize(float)设置画笔写的文字大小;P.setColor(Color.BLUE)设置颜色;P.setAlpha(int)设置透明度;P.setShader(shader)设置渲染器。

Canvas:英文意思是帆布,画布。简单理解就是,一个工具类,用于画我们想要的效果,比如直线,曲线,圆,矩形……调用draw系列方法可以实现。在从写ondraw(Canvas c)方法时系统会给我们一个Canvas类的对象,这样就可以通过调用一系列方法绘制我们想要的图形了。本片文章需要用到:

根据路径画线c.drawPath(path,paint);第一个参数为绘制的路径path对象,第二个参数为画笔对象paint。

根据起始点和终点画线c.drawLine(startX, startY, stopX, stopY, paint);前两个参数为起始点坐标(x,y)。后两个参数为终点坐标(x,y)。注意,我们在手机上的坐标系与生活中的坐标系0点位置不一样,在Android中屏幕坐标系,x轴为横轴,y轴为纵轴。但是起点即零点为屏幕左上角顶点,X轴向右逐渐增大,Y轴向下逐渐增大。通俗讲,X轴向右为正方向,Y轴向下为正方向。

画图c.drawBitmap(bitmap, left, top, paint);第一个参数bitmap对象,第二个与第三个参数为起始点(图的左上角点),第四个参数为paint画笔对象。注意,在Android中画任何图的时候,都应该有一个左上角点,不管是圆形,椭圆,不规则的图,都可以想象成一个矩形框包裹住,这样矩形的左上角顶点,就是绘制图形开始的起点,即向右边画。这样在控制图像位置时候,就会很精确。

画点c.drawPoint(x, y, paint);前两个参数为,点的坐标。最后为画笔工具。

画矩形c.drawRect(left, top, right, bottom, paint);前两个参数为矩形的左上角顶点坐标,然后两个参数为矩形的右下角顶点坐标,最后参数为paint画笔工具。用一张图更形象的理解。四根红色线代表前4个参数的距离。



画文本(写字)c.drawText(text, x, y, paint);第一个参数为要写的文本String,后面两个参数为画文本开始的左上角点位置(想象成在一个矩形框内写字,开始点为矩形的左上角点),一直在强调左上角。

Path:英文意思是路径,道路。Android中也是绘图路径的意思,一般和canvas一起用。介绍几个常用的方法看意思也知道意思的方法,p.lineTo(x, y)就是画线至(x,y)坐标位置。p.moveTo(x, y);将路径移动到(x,y)位置。p.close();关闭路径,即把终点和起点连起来。

说了一大堆废话,现在开始了,首先我们需要定义一些变量,用于绘制图形

//折线图点集合
private List<Integer> milliliter;
//基准宽度
private float tb;
// 左右间距
private float interval_left_right;
// 左间距
private float interval_left;
// 画笔
private Paint paint_date, paint_brokenLine, paint_dottedline,
paint_brokenline_big, framPanint;
//文本画笔
private Paint textPaint;
// 点图
private Bitmap bitmap_point;
//路径
private Path path;
//折线点上数字
private float dotted_text;
private int fineLineColor = 0x5faaaaaa; // 灰色,背景柱状图
private int blueLineColor = 0xff00ffff; // 蓝色,线色


然后初始化要用到的变量参数。

if (null == milliliter || milliliter.size() == 0)
return;
this.milliliter = delZero(milliliter);
Resources res = getResources();
// 定义基准宽度
tb = res.getDimension(R.dimen.historyscore_tb);
interval_left_right = tb * 2.0f;
interval_left = tb * 0.5f;

paint_date = new Paint();
paint_date.setStrokeWidth(tb * 0.1f);
paint_date.setTextSize(tb * 0.7f);
paint_date.setColor(fineLineColor);

paint_brokenLine = new Paint();
paint_brokenLine.setStrokeWidth(tb * 0.1f);
paint_brokenLine.setColor(blueLineColor);
paint_brokenLine.setAntiAlias(true);

paint_dottedline = new Paint();
//设置画笔风格
paint_dottedline.setStyle(Paint.Style.STROKE);
paint_dottedline.setColor(fineLineColor);

paint_brokenline_big = new Paint();
paint_brokenline_big.setStrokeWidth(tb * 0.2f);
paint_brokenline_big.setColor(fineLineColor);
paint_brokenline_big.setAntiAlias(true);

framPanint = new Paint();
framPanint.setAntiAlias(true);
//设置空心宽度
framPanint.setStrokeWidth(2f);

textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setStrokeWidth(1f);
textPaint.setTextSize(tb * 0.4f);
textPaint.setColor(Color.BLUE);
Paint P = new Paint();

path = new Path();
bitmap_point = BitmapFactory.decodeResource(getResources(),
R.drawable.icon_point_blue);
// 设置宽高尺寸
setLayoutParams(new LayoutParams((int) (31 * interval_left_right),
LayoutParams.MATCH_PARENT));


数据初始化完成后,我们就可以开始,进行绘制操作了。这里要注意一下上面再定义基准宽度的时候用了一个方法res.getDimension(R.dimen.historyscore_tb);这个是 从xml资源文件中的到尺寸大小的。好处就在于屏幕适配,和当需要更改的时候,只需要在dimens.xml文件中找到对应的dimen标签,更改数据就可以了,当然,在不同的手机分辨率不一样,屏幕大小不一样,这个时候通过xml文件适配,就很合适了。关于屏幕适配,在接下来的blog中有介绍。数据源,这个,我没有在自定义控件中写死。主要是为了方便扩展,用了set方法。

public void setMilliliter(List<Integer> milliliter)
{
this.milliliter = milliliter;
}


当然,也可以在自定义控件的构造方法内赋值。

public ChatView(Context context, List<Integer> milliliter)
{
super(context);
//给集合赋值(提供数据源)
init(milliliter);
}


接下来,我们要做的就是,把图表的背景及Y轴绘制出啦。 这里有个虚线的绘制,是通过DashPathEffect类,DashPathEffect是PathEffect类的一个子类,可以使paint画出类似虚线的形式,并且可以任意指定虚实的排列方式(如间距,样式)通过构造函数传入两个参数,传入float[]为虚线的ON和OFF数组(实线,空白),该数组的length必须大于等于2,第二个参数为绘制时的偏移量。使用paint的setPathEffect方法设置画笔参数。再drawPath出想要的虚线。

public void drawStraightLine(Canvas c)
{
paint_dottedline.setColor(fineLineColor);
Path path = new Path();
for (int i = 0; i < 32; i++)
{
path.moveTo(interval_left_right * i + tb, tb * 1.5f);
//getHeight()用于得到控件的高度
path.lineTo(interval_left_right * i + tb, getHeight());
// 虚线
PathEffect effects = new DashPathEffect(new float[] { tb * 0.3f,
tb * 0.3f, tb * 0.3f, tb * 0.3f }, tb * 0.1f);
paint_dottedline.setPathEffect(effects);
c.drawPath(path, paint_dottedline);
}
//底部粗实线(可有可无)
c.drawLine(0, getHeight() - tb * 0.2f, getWidth(), getHeight() - tb
* 0.2f, paint_brokenline_big);
}


绘制顶部时间/天数。用for循环增加X轴的距离,起到间隔作用。

public void drawDate(Canvas c)
{
for (int i = 0; i < 31; i++)
{
paint_date.setStrokeWidth(tb * 0.4f);
c.drawText(i + 1 + "天", interval_left_right * i + tb / 2,
tb * 1.0f, paint_date);
}

}


接下来。就开始,我们的折线图绘制了,有点绕,请看注释。

public void drawBrokenLine(Canvas c)
{
//getHeight()用于得到控件的高度
float base = (getHeight() - tb * 3.0f) / Collections.max(milliliter)
* 0.7f;
// 渲染,给折线区域渲染颜色的,
Shader mShader = new LinearGradient(0, 0, 0, getHeight(), new int[] {
Color.argb(150, 0, 200, 250), Color.argb(70, 0, 200, 250),
Color.argb(22, 0, 200, 250) }, null, Shader.TileMode.CLAMP);
framPanint.setShader(mShader);
//画点,及数值
for (int i = 0; i < milliliter.size() - 1; i++)
{
float x1 = interval_left_right * i + tb;
float y1 = getHeight() - tb * 1.5f - (base * milliliter.get(i));
//下一点的坐标
float x2 = interval_left_right * (i + 1) + tb;
float y2 = getHeight() - tb * 1.5f - (base * milliliter.get(i + 1));
if (i == 0)
{
path.moveTo(tb,
getHeight() - tb * 1.5f - (base * milliliter.get(0)));
}
c.drawLine(x1, y1, x2, y2, paint_brokenLine);
path.lineTo(x1, y1);
// 打点,最后参数paint可不用
c.drawBitmap(bitmap_point, x1 - bitmap_point.getWidth() / 2, y1
- bitmap_point.getHeight() / 2, null);
Log.w("drawBitmap", "" + milliliter.size());
// 画数值(文本)
c.drawText("" + milliliter.get(i),
x1 - bitmap_point.getWidth() / 2,
y1 - bitmap_point.getHeight() / 2, textPaint);
//此处注意,-2.因为当在最后一个点的时候,是没有X2,Y2的。所以,有个判断
if (i == milliliter.size() - 2)
{
path.lineTo(x2, y2);
path.lineTo(x2, getHeight());
path.lineTo(tb, getHeight());
path.close();//一定要闭合
//设置渲染效果
c.drawPath(path, framPanint);
//画最后一个点
c.drawBitmap(bitmap_point, x2 - bitmap_point.getWidth() / 2, y2
- bitmap_point.getHeight() / 2, null);
//画最后点的数值
c.drawText("" + milliliter.get(i + 1),
x2 - bitmap_point.getWidth() / 2,
y2 - bitmap_point.getHeight() / 2, textPaint);
}
}
}


上面代码中用到了LinearGradient类叫做线性渲染。作用是实现某一区域(闭合)内颜色的线性渐变效果,构造方法为LinearGradient (float x0, float y0, float x1, float y1, int[] colors, float[] positions, Shader.TileMode tile);

其中,参数x0表示渐变的起始点x坐标;参数y0表示渐变的起始点y坐标;参数x1表示渐变的终点x坐标;参数y1表示渐变的终点y坐标;参数colors表示渐变的颜色数组;参数positions用来指定颜色数组的相对位置;参数tile表示平铺方式。通常,参数positions设为null,表示颜色数组以斜坡线的形式均匀分布。

Y轴 单元base值的计算,是通过控件的总高getHeight()得到减去上面文字的高度和间隔。除以数据源集合中的最大值用Collections.max()方法得到,再乘以0.7的系数,是希望不要让最大值在顶上,影响美观。还需要注意的是,path路径是跟随折线图一起运动,当到达最后一个点时候,让path路径直接path.lineTo(x2, getHeight());到达最下边底部。再path.lineTo(tb, getHeight());到达第一根竖虚线底部,调用close()方法闭合路径。这样就可以使用渲染效果了。

最后,在onDraw()方法中将上面三个方法。添加到里面。这样就完成了,onDraw()的重写,当然里面有个判断数据源是否为空,避免报空指针异常。

@Override
protected void onDraw(Canvas c)
{
//判断
if (null == milliliter || milliliter.size() == 0)
{
Log.w("onDraw()", "为空");
return;
}
drawStraightLine(c);
drawBrokenLine(c);
drawDate(c);

}


是不是很简单啊。哈哈!第一篇博客。。有不足之处请多指教!看着窗外的风景,听听音乐,敲敲代码其实也挺好的。是吧!

下面添加自定义类java文件,里面有个方法用于去除集合中首尾为0的点,可以看情况自己使用。谢谢!

public class ChatView extends View

{

private List<Integer> milliliter;
private float tb;
// 左右间距
private float interval_left_right;
// 左间距
private float interval_left;
// 画笔
private Paint paint_date, paint_brokenLine, paint_dottedline,
paint_brokenline_big, frampaint;

private Paint textPaint;
private int time_index;
// 点图
private Bitmap bitmap_point;
private Path path;
private float dotted_text;

public float getDotted_text()
{
return dotted_text;
}

public void setDotted_text(float dotted_text)
{
this.dotted_text = dotted_text;
}

public void setMilliliter(List<Integer> milliliter) { this.milliliter = milliliter; }

private int fineLineColor = 0x5faaaaaa; // 灰色,背景柱状图
private int blueLineColor = 0xff00ffff; // 蓝色,线色
private int orangeLineColor = 0xffd56f2b; // 橙色,基准线色

public ChatView(Context context, List<Integer> milliliter)
{
super(context);
init(milliliter);
}

public void init(List<Integer> milliliter)
{
if (null == milliliter || milliliter.size() == 0)
return;
this.milliliter = delZero(milliliter);
Resources res = getResources();
// 定义基准宽度
tb = res.getDimension(R.dimen.historyscore_tb);
interval_left_right = tb * 2.0f;
interval_left = tb * 0.5f;

paint_date = new Paint();
paint_date.setStrokeWidth(tb * 0.1f);
paint_date.setTextSize(tb * 0.7f);
paint_date.setColor(fineLineColor);

paint_brokenLine = new Paint();
paint_brokenLine.setStrokeWidth(tb * 0.1f);
paint_brokenLine.setColor(blueLineColor);
paint_brokenLine.setAntiAlias(true);

paint_dottedline = new Paint();
paint_dottedline.setStyle(Paint.Style.STROKE);
paint_dottedline.setColor(fineLineColor);

paint_brokenline_big = new Paint();
paint_brokenline_big.setStrokeWidth(tb * 0.2f);
paint_brokenline_big.setColor(fineLineColor);
paint_brokenline_big.setAntiAlias(true);

frampaint = new Paint();
frampaint.setAntiAlias(true);
frampaint.setStrokeWidth(2f);

textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setStrokeWidth(1f);
textPaint.setTextSize(tb * 0.4f);
textPaint.setColor(Color.BLUE);
Paint P = new Paint();

path = new Path();
bitmap_point = BitmapFactory.decodeResource(getResources(),
R.drawable.icon_point_blue);
// 设置宽高尺寸
setLayoutParams(new LayoutParams((int) (31 * interval_left_right),
LayoutParams.MATCH_PARENT));
}

/**
* 移除左右为零的数据
*
* @return
*/
public List<Integer> delZero(List<Integer> milliliter)
{
List<Integer> list = new ArrayList<Integer>();
int sta = 0;
int end = 0;
for (int i = 0; i < milliliter.size(); i++)
{
if (milliliter.get(i) != 0)
{
sta = i;
break;
}
}
for (int i = milliliter.size() - 1; i >= 0; i--)
{
if (milliliter.get(i) != 0)
{
end = i;
break;
}
}
for (int i = 0; i < milliliter.size(); i++)
{
if (i >= sta && i <= end)
{
list.add(milliliter.get(i));
}
}
time_index = sta;
dotted_text = ((Collections.max(milliliter) - Collections
.min(milliliter)) / 12.0f * 5.0f);
return list;
}

@Override
protected void onDraw(Canvas c)
{
if (null == milliliter || milliliter.size() == 0)
{
Log.w("onDraw()", "为空");
return;
}
drawStraightLine(c);
drawBrokenLine(c);
drawDate(c);

}

/**
* 绘制竖线 绘制背景Y轴
*
* @param c
*/
public void drawStraightLine(Canvas c)
{
paint_dottedline.setColor(fineLineColor);
Path path = new Path();
for (int i = 0; i < 32; i++)
{
path.moveTo(interval_left_right * i + tb, tb * 1.5f);
path.lineTo(interval_left_right * i + tb, getHeight());
// 虚线
PathEffect effects = new DashPathEffect(new float[] { tb * 0.3f,
tb * 0.3f, tb * 0.3f, tb * 0.3f }, tb * 0.1f);
paint_dottedline.setPathEffect(effects);
c.drawPath(path, paint_dottedline);
}
c.drawLine(0, getHeight() - tb * 0.2f, getWidth(), getHeight() - tb
* 0.2f, paint_brokenline_big);
}

/**
* 绘制折线
*
* @param c
*/
public void drawBrokenLine(Canvas c)
{
float base = (getHeight() - tb * 3.0f) / Collections.max(milliliter)
* 0.7f;
// 渲染
Shader mShader = new LinearGradient(0, 0, 0, getHeight(), new int[] {
Color.argb(150, 0, 200, 250), Color.argb(70, 0, 200, 250),
Color.argb(22, 0, 200, 250) }, null, Shader.TileMode.CLAMP);
frampaint.setShader(mShader);

for (int i = 0; i < milliliter.size() - 1; i++)
{
float x1 = interval_left_right * i + tb;
float y1 = getHeight() - tb * 1.5f - (base * milliliter.get(i));
float x2 = interval_left_right * (i + 1) + tb;
float y2 = getHeight() - tb * 1.5f - (base * milliliter.get(i + 1));
if (i == 0)
{
path.moveTo(tb,
getHeight() - tb * 1.5f - (base * milliliter.get(0)));
}
c.drawLine(x1, y1, x2, y2, paint_brokenLine);
path.lineTo(x1, y1);
// 打点
c.drawBitmap(bitmap_point, x1 - bitmap_point.getWidth() / 2, y1
- bitmap_point.getHeight() / 2, null);
Log.w("drawBitmap", "" + milliliter.size());
// 画步数值
c.drawText("" + milliliter.get(i),
x1 - bitmap_point.getWidth() / 2,
y1 - bitmap_point.getHeight() / 2, textPaint);
if (i == milliliter.size() - 2)
{
path.lineTo(x2, y2);
path.lineTo(x2, getHeight());
path.lineTo(tb, getHeight());
path.close();
c.drawPath(path, frampaint);
c.drawBitmap(bitmap_point, x2 - bitmap_point.getWidth() / 2, y2
- bitmap_point.getHeight() / 2, null);
c.drawText("" + milliliter.get(i + 1),
x2 - bitmap_point.getWidth() / 2,
y2 - bitmap_point.getHeight() / 2, textPaint);
}
}
}

/**
* 绘制时间
*
* @param c
*/
public void drawDate(Canvas c) { for (int i = 0; i < 31; i++) { paint_date.setStrokeWidth(tb * 0.4f); c.drawText(i + 1 + "天", interval_left_right * i + tb / 2, tb * 1.0f, paint_date); } }


}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息