这样的图用来做统计最方便了,今天,我们又要摆脱第三方的约束,自己来实现了,是不是很开心,现在就来动手吧。
本文内容需要读者具备一定的自定义view基础,否则看起来可能比较费力,不过懂的看门道,不懂的可以凑个热闹,能看懂可以自己去改不好的地方,不懂的也可以直接拿来用。
先给个效果图:
看起来还可以吧,废话不多说,开始我们的绘制。
首先要确定我们要达成的效果,下面列出来
1:数据的获取
2:折线的绘制
3:X坐标文字的绘制
4:数字的绘制
5:数字的位置判断,防止与折线重合
6:随手指滑动的效果
这样思路清晰了,下面逐个击破
1 数据的获取
这恐怕是最简单的一步了,我在里面定义了一个方法,包含三个参数,使用的时候直接传进去,再刷新试图即可,给出方法
//设置整个坐标上的一些文字和数字
public void setData(String[] coordinate, String[] number, float extremum) {
this.coordinate = coordinate;
this.number = number;
this.extremum = extremum;
}
可以看见传入了三个参数,一个是坐标数组,一个是数字数组,这里用字符数组代替的,再一个就是Y轴上最大值
2 折线的绘制
绘制折线肯定是要使用到path,那么就要把path拿来,然后添上路径即可,那么主要就是路径坐标的问题了,X坐标我用的文字宽度加上文字和文字之间距离形成最后的坐标,然后遍历数组就能获得一个个X坐标,Y坐标我添加了一个功能,那就是能设置绘制图的上边距和下边距,其实X坐标我也设置了左边距,具体的使用大家可以去里面更改数值感受一下,如果上边距加上下编剧正好等于控件的高度的话,那就整个图就是一个直线,你懂的。
给出绘制路径代码:
float deviationY = (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM; //这里式子没有简化,是为了方便你们清楚这是怎么一个过程
float deviationX = movedistance + OFFSETX + txtrect.width() * i + txtpading * i; //这里式子没有简化,是为了方便你们清楚这是怎么一个过程
//画出折线图的路径
path.lineTo(deviationX, deviationY);
canvas.drawPath(path, linepaint); //绘制折线
解释一下,这里的movedistance就是随手指滑动会产生的位移,OFFSETX则是X轴左边距,OFFSETYTOP是Y轴上边距,OFFSETYBOTTOM是Y轴下边距。
3 X坐标文字的绘制
文字的绘制比较简单了,主要就是坐标的把握,Y轴没难度,都用问题的高度即可,X轴呢,其实就是刚才绘制折线的X坐标再减去文字宽度的一半即可,给出代码参考:
canvas.drawText(coordinate[i] + "", deviationX - txtrect.width() / 2, height - txtrect.height() / 2, extremumpaint); //绘制X轴上的坐标
4 数字的绘制
数字的绘制相对于文字的绘制就是多了一个Y轴上的变换,当然了,X轴这里要减去的是数字的宽度一半,而不是文字了,高度我们其实用之前折线的高度减去上我们数字的高度的一半即可,给出代码:
canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height() / 2, txtpaint); //绘制数字
5 数字的位置判断,防止与折线重合
这个问题呢我们可以从折线的走势判断解决,那么当前的数字所在的折线坐标会有以下情况:
1: 中间最高
2: 左边最高,右边最低
3: 左边最低,右边最高
4: 中间最低
就这四种情况,我们可以从Y轴进行判断,判断我提取了成了一个方法,给出代码:
/*这个函数的作用是判断当前数字坐标的y轴和其左右两边的数字坐标的y进行比较,从而判断数字显示的位置,避免折线和数字重复在一起的情况
*/
public int judgmentposition(int i) {
int status = 1;
if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) >
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM <
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
status = 1;
} else if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) <
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM <
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
status = 2;
} else if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) >
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM >
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
status = 3;
} else {
status = 4;
}
Log.d("statusnumbers", status + "");
return status;
}
使用时的代码:
//下面的绘制要保证文字在合适的位置,想了解的可参考我另一个博客:http://blog.csdn.net/wanxuedong/article/details/69396732
if (i > 0 && i < number.length - 1) {
switch (judgmentposition(i)) {
case 1: //中间最高
canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height(), txtpaint); //绘制数字
break;
case 2: //左边最高,右边最低
canvas.drawText(number[i] + "", deviationX, deviationY - numberrect.height() / 2, txtpaint); //绘制数字
break;
case 3: //左边最低,右边最高
canvas.drawText(number[i] + "", deviationX - numberrect.width(), deviationY - numberrect.height() / 2, txtpaint); //绘制数字
break;
case 4: //中间最低
canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY + numberrect.height() * 2, txtpaint); //绘制数字
break;
}
} else {
canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height() / 2, txtpaint); //绘制数字
}
6 随手指滑动的效果
和手势有关的就不得不使用onTouchEvent方法了,给出代码:
//这里面我们实现了让该控件可以随手指挥动而滚动,但是没写滚动条(因为觉得有滚动条也不好看)
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
positionx = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//如果是往右滑动,那么左侧距离就要判断不能过大
if (event.getRawX() - positionx > 0) {
if (!(movedistance > leftdistance)) {
movedistance += event.getRawX() - positionx;
positionx = event.getRawX();
invalidate();
}
} else { //如果是往右滑动,那么左侧距离也要判断不能过大
if (!(movedistance < -rightdistance)) {
movedistance += event.getRawX() - positionx;
positionx = event.getRawX();
invalidate();
}
}
positionx = event.getRawX();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
有两个值需要解释一下,leftdistance和rightdistance,这是我们可以再开始时设置的左右滑动的最大距离,具体可以自己进去设置。
好了,总的效果也就这样完成了,解剖后是不是看起来又有点简单了,学习就是这样,能者不惧,你就是能者。
下面给出整个代码(可以直接粘贴复制进行使用):
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* Created by wanxuedong on 2017/08/30.
* 联系QQ:2381144912
*/
public class LineChart extends View {
//这三个值是我们传进来的,绘图的数据从这里来
private String[] coordinate; //X坐标上的标记
private String[] number; //折线图上的一个个数字,传值的时候只能传数字,别瞎传哈
private float extremum; //Y坐标的最大值
// private int width; //控件的宽度
private int height; //控件的高度
private Path path; //折线的路径
private Paint linepaint; //折线的画笔
private Paint rectpaint; //方块的画笔
private Paint extremumpaint; //坐标的画笔
private Paint txtpaint; //数值文字的画笔
// private Rect rect; //方块的模型
private Rect txtrect; //利用rect获取X坐标上文字的宽度,好精确的放置文字位置
private Rect numberrect; //利用rect获取数字的宽度,好精确的放置文字位置
//下面两个属性用来操作手势有关的
private float positionx = 0; //手指移动的时候,记录X坐标的作用
private float movedistance; //手指每次移动的位移,整个图像会根据手势进行移动
/*
* 上面的数据基本上不需要动,下面的属性我们可以自行更改
* */
// private int RECTWIDTH = 5; //小方块的宽度
private int RECTHEIGHT = 6; //小方块的高度和圆的半径
private int OFFSETYTOP = 100; //Y轴方向顶端留下的长度
private int OFFSETYBOTTOM = 100; //Y轴方向底端留下的长度
private int OFFSETX = 100; //x轴方向左端留下的长度
private int XTXTSIZE = 24; //X轴文字大小
private int NUMBERSIZE = 18; //数字文字大小
private int LINEWIDTH = 3; //折线粗度
private int txtpading = 80; //X坐标轴上每个文字之间的间隔大小,这个决定了你的控件有多宽
//下面两个属性需要根据实际情况自己改动,如果有好的方案,望请告知
private int leftdistance = 50; //X轴手势往右滑动的最大距离,这个数字会和OFFSETX加起来,总的数值才是左侧看起来的距离
private int rightdistance = 500; //X轴手势往左滑动的最大距离
public LineChart(Context context) {
super(context);
path = new Path();
initpaint();
}
public LineChart(Context context, AttributeSet attrs) {
super(context, attrs);
path = new Path();
initpaint();
}
//测量获取控件宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// width = getMeasuredWidth();
height = getMeasuredHeight();
}
//这里面开始绘制文字,数字,和折线
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//定位折线的起始位置,也就是第一个数字坐标点
path.moveTo(OFFSETX + movedistance, (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[0]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM);
for (int i = 0; i < number.length; i++) {
// rect = new Rect();
txtrect = new Rect();
numberrect = new Rect();
extremumpaint.getTextBounds(coordinate[i], 0, coordinate[i].length(), txtrect);
txtpaint.getTextBounds(number[i], 0, number[i].length(), numberrect);
float deviationY = (height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM; //这里式子没有简化,是为了方便你们清楚这是怎么一个过程
float deviationX = movedistance + OFFSETX + txtrect.width() * i + txtpading * i; //这里式子没有简化,是为了方便你们清楚这是怎么一个过程
//画出折线图的路径
path.lineTo(deviationX, deviationY);
canvas.drawPath(path, linepaint); //绘制折线
//设置方块的位置和宽高属性
// rect.set(width / number.length * i - RECTWIDTH / 2 + OFFSETX, (int) (deviationY - RECTHEIGHT / 2), width / number.length * i + RECTWIDTH / 2 + OFFSETX, (int) (deviationY + RECTHEIGHT / 2));
// canvas.drawRect(rect, rectpaint); //绘制小方块
// 下面是绘制小圆球,上面是绘制小方块,有需要的可以恢复上面的小方块(不过坐标自己去设置了,懒的再去看去改了)
canvas.drawCircle(deviationX, deviationY, RECTHEIGHT, rectpaint);
//下面的绘制要保证文字在合适的位置,想了解的可参考我另一个博客:http://blog.csdn.net/wanxuedong/article/details/69396732
if (i > 0 && i < number.length - 1) {
switch (judgmentposition(i)) {
case 1: //中间最高
canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height(), txtpaint); //绘制数字
break;
case 2: //左边最高,右边最低
canvas.drawText(number[i] + "", deviationX, deviationY - numberrect.height() / 2, txtpaint); //绘制数字
break;
case 3: //左边最低,右边最高
canvas.drawText(number[i] + "", deviationX - numberrect.width(), deviationY - numberrect.height() / 2, txtpaint); //绘制数字
break;
case 4: //中间最低
canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY + numberrect.height() * 2, txtpaint); //绘制数字
break;
}
} else {
canvas.drawText(number[i] + "", deviationX - numberrect.width() / 2, deviationY - numberrect.height() / 2, txtpaint); //绘制数字
}
//上面为了数字位置进行了判断设置,但是不包括第一个数字和最后一个数字,有需要的可以自行添加
canvas.drawText(coordinate[i] + "", deviationX - txtrect.width() / 2, height - txtrect.height() / 2, extremumpaint); //绘制X轴上的坐标
}
path.reset(); //每次重绘的时候需要把path恢复成空的路径,否则,所有路径会叠加在一起,那到时就热闹了
}
//这里面我们实现了让该控件可以随手指挥动而滚动,但是没写滚动条(因为觉得有滚动条也不好看)
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
positionx = event.getRawX();
break;
case MotionEvent.ACTION_MOVE:
//如果是往右滑动,那么左侧距离就要判断不能过大
if (event.getRawX() - positionx > 0) {
if (!(movedistance > leftdistance)) {
movedistance += event.getRawX() - positionx;
positionx = event.getRawX();
invalidate();
}
} else { //如果是往右滑动,那么左侧距离也要判断不能过大
if (!(movedistance < -rightdistance)) {
movedistance += event.getRawX() - positionx;
positionx = event.getRawX();
invalidate();
}
}
positionx = event.getRawX();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
//初始化画笔,读者看的时候可略过
public void initpaint() {
linepaint = new Paint();
rectpaint = new Paint();
extremumpaint = new Paint();
txtpaint = new Paint();
//折线的一些属性
linepaint.setColor(Color.argb(255, 245, 133, 61)); //设置颜色
linepaint.setStyle(Paint.Style.STROKE); //设置空心填充
linepaint.setAntiAlias(true); //设置抗锯齿
linepaint.setStrokeWidth(LINEWIDTH); //设置粗度
//方块的一些属性
rectpaint.setColor(Color.argb(255, 245, 133, 61));
//X坐标轴上文字的一些属性
extremumpaint.setColor(Color.argb(255, 51, 51, 51));
extremumpaint.setTextSize(XTXTSIZE); //设置坐标文字大小
extremumpaint.setAntiAlias(true);
//数字的一些属性
txtpaint.setColor(Color.argb(255, 145, 145, 145));
txtpaint.setTextSize(NUMBERSIZE); //设置数字大小
txtpaint.setAntiAlias(true);
}
/*这个函数的作用是判断当前数字坐标的y轴和其左右两边的数字坐标的y进行比较,从而判断数字显示的位置,避免折线和数字重复在一起的情况
*/
public int judgmentposition(int i) {
int status = 1;
if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) >
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM <
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
status = 1;
} else if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) <
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM <
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
status = 2;
} else if (((height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i - 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) >
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM &&
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM >
(height - OFFSETYTOP) - (height - OFFSETYTOP - OFFSETYBOTTOM) * (Float.parseFloat(number[i + 1]) / extremum) + OFFSETYTOP - OFFSETYBOTTOM) {
status = 3;
} else {
status = 4;
}
Log.d("statusnumbers", status + "");
return status;
}
//设置整个坐标上的一些文字和数字
public void setData(String[] coordinate, String[] number, float extremum) {
this.coordinate = coordinate;
this.number = number;
this.extremum = extremum;
}
}
最后还是给出demo,走你:http://download.csdn.net/download/wanxuedong/9958951