最终实现效果如图所示,可以支持多个折线的对比效果,当手指滑动到相应区域的时候会显示相应点的折线的数据.
下面开始项目的构建,我们可以将整个效果分为几个部分,第一个部分是x轴和x轴上的文字,第二个部分是y轴和y轴上的文字,第三部分是数据上的线和圆的绘制,第四部分是响应我们的手指触摸事件.
其中X轴和Y轴中的部分可以直接绘制,但是对于关于数据上的每个点,我们可以知道每个点有相同的属性和方法,所以我们应该创建一个类LineViewPointBean,这个类里面应该有这个点在坐标系中的坐标Point类,一个x轴的数值和一个y轴的数值,还有一个这个点属于哪组数据的标识符sort.具体代码如下:
<span style="font-size:18px;">/**
* 封装了LineView中数据点的类,每个点有4个属性,分别是他的x和y的值,以及在lineView中的position,还有他属于第几组数据
*
* @author CK
*
*/
class LineViewPointBean {
String xData, yData;// x,y的数据
PointF position;// 每个点在当前屏幕中的位置
int sort;// 每个点属于哪类数据
public String getxData() {
return xData;
}
public void setxData(String xData) {
this.xData = xData;
}
public String getyData() {
return yData;
}
public void setyData(String yData) {
this.yData = yData;
}
public PointF getPosition() {
return position;
}
public void setPosition(PointF position) {
this.position = position;
}
public int getSort() {
return sort;
}
public void setSort(int sort) {
this.sort = sort;
}
/**
* 每个点绘制自己的方法
*
* @param canvas
* @param pointPaint
*/
public void drawPoint(Canvas canvas, Paint pointPaint) {
canvas.drawCircle(position.x, position.y, 10, pointPaint);
}
@Override
public String toString() {
return "LineViewPointBean [xData=" + xData + ", yData=" + yData
+ ", position=" + position + ", sort=" + sort + "]";
}
}</span>
然后我们看一下这个view应该接收的数据,对于x轴和y轴的数据可以直接设置为一个数组,然后是折线上的数据,因为有实际和目标两组数据,单个list无法满足要求,所以我们使用List<List<String>>来表示折线上的数据,其中外层list的长度代表有多少组数据,内层的list代表每层数据有多少个点.所以我们该view的构造方法应该传入这三个参数及Context(因为继承自View,在View的构造方法中需要用到Context,所以我们也要传入Context并使用super(context)).构造方法如下所示:
<span style="font-size:18px;">/**
* @param dataSource
* 数据源-最外层list的长度代表每种数据有多少种data,内层list长度代表每种data的长度
* @param yData
* y轴坐标值,分别传入0和最大值
* @param xData
* x轴的坐标值
*/
public CKLineView(List<List<String>> dataSource, String[] yData,
String[] xData, Context context) {
super(context);
this.dataSource = dataSource;
this.yData = yData;
this.xData = xData;
// 初始化颜色数组
LINE_COLOR_DATA = new String[dataSource.size()];
TEXT_COLOR_DATA = new String[dataSource.size()];
// 初始化画笔
paintLineData = new Paint[dataSource.size()];
paintTextData = new Paint[dataSource.size()];
}</span>
对于这个折线图,我们对每个部分的颜色是可以设置的,所以我们需要设置不同的画笔和颜色,然后x轴及y轴的尺寸,文字的大小
<span style="font-size:18px;">// size
int MARGIN_SIZE_HORIZONTAL = 60, MARGIN_SIZE_VERTICAL = 60;//折线图的margin
float TEXT_SIZE_X = 0, TEXT_SIZE_Y = 0, TEXT_SIZE_DATA = 0;//x轴文字,y轴文字,数据文字的大小
int LENGTH_X, LENGTN_Y;//x,y轴的长度
int SCALE_X, SCALE_Y;//x,y轴每个数据的长度
int width, height;//view的宽高
// color
String TEXT_COLOR_X, TEXT_COLOR_Y;
String LINE_COLOR_X, LINE_COLOR_Y;
String TEXT_COLOR_DATA[], LINE_COLOR_DATA[];
// position
int xStart = 0, yStart = 0;//x轴和y轴的起始点
// paint
Paint paintLineX, paintLineY, paintTextX, paintTextY, paintLineData[],
paintTextData[];</span>
接下来我们将传入的List<List<String>>的数据,转化为我们的List<List<LineViewBean>>,handleDataSource
<span style="font-size:18px;"> void handleDataSource() {
data = new ArrayList<List<LineViewPointBean>>();
for (int i = 0; i < dataSource.size(); i++) {
// 有多少条线
List<String> tempDataSource = dataSource.get(i);
List<LineViewPointBean> tempData = new ArrayList<LineViewPointBean>();
for (int j = 0; j < tempDataSource.size(); j++) {
// 每条线有多少点
LineViewPointBean tempLineViewPointBean = new LineViewPointBean();
tempLineViewPointBean.setxData(j + "");
tempLineViewPointBean.setyData(tempDataSource.get(j));
tempLineViewPointBean.setSort(i);
PointF temPointF = new PointF();
// 通过数据值,计算每个点
Float x = 0f, y = 0f;
x = (float) (xStart + j * SCALE_X);
if (Float.parseFloat(yData[1]) == 0) {
// 如果最大值和最小值都为0,那么应该做特殊处理.将0的位置设为y轴中间,同时所有的数据都为0
y = (float) (yStart - 2 * SCALE_Y);
tempLineViewPointBean.setyData("0");
} else {
y = yStart
- SCALE_Y
- (Float.parseFloat(tempDataSource.get(j)) / Float
.parseFloat(yData[1])) * 2 * SCALE_Y;
}
temPointF.set(x, y);
tempLineViewPointBean.setPosition(temPointF);
tempData.add(tempLineViewPointBean);
}
data.add(tempData);
}
}</span>
最后我们看一下如何处理触摸事件,当我们手指滑动到某个点时,在ontouchEvent中判断触摸的点在哪个数据的区域内,然后我们就把touchRegion赋值,在draw方法中绘制.
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < data.get(0).size(); i++) {
RectF tempRectF = new RectF((float) (xStart + (i - 0.5)
* SCALE_X), 0f, (float) (xStart + (i + 0.5) * SCALE_X),
(float) yStart);
if (tempRectF.contains(event.getX(), event.getY())) {
touchRegion = i;
postInvalidate();
return true;
}
}
break;
case MotionEvent.ACTION_DOWN:
for (int i = 0; i < data.get(0).size(); i++) {
RectF tempRectF = new RectF((float) (xStart + (i - 0.5)
* SCALE_X), 0f, (float) (xStart + (i + 0.5) * SCALE_X),
(float) yStart);
if (tempRectF.contains(event.getX(), event.getY())) {
touchRegion = i;
postInvalidate();
return true;
}
}
break;
}
return true;
}
最后我们看下这个view的全部代码:
/**折线图
* @author CK
*
*/
public class CKLineView extends View {
// data
List<List<String>> dataSource;
String[] yData;
String[] xData;
List<List<LineViewPointBean>> data;
// size
int MARGIN_SIZE_HORIZONTAL = 60, MARGIN_SIZE_VERTICAL = 60;//折线图的margin
float TEXT_SIZE_X = 0, TEXT_SIZE_Y = 0, TEXT_SIZE_DATA = 0;//x轴文字,y轴文字,数据文字的大小
int LENGTH_X, LENGTN_Y;//x,y轴的长度
int SCALE_X, SCALE_Y;//x,y轴每个数据的长度
int width, height;//view的宽高
// color
String TEXT_COLOR_X, TEXT_COLOR_Y;
String LINE_COLOR_X, LINE_COLOR_Y;
String TEXT_COLOR_DATA[], LINE_COLOR_DATA[];
// position
int xStart = 0, yStart = 0;//x轴和y轴的起始点
// paint
Paint paintLineX, paintLineY, paintTextX, paintTextY, paintLineData[],
paintTextData[];
// touchRegion手指滑动区域
int touchRegion = -1;
/**
* @param dataSource
* 数据源-最外层list的长度代表每种数据有多少种data,内层list长度代表每种data的长度
* @param yData
* y轴坐标值,分别传入0和最大值
* @param xData
* x轴的坐标值
*/
public CKLineView(List<List<String>> dataSource, String[] yData,
String[] xData, Context context) {
super(context);
this.dataSource = dataSource;
this.yData = yData;
this.xData = xData;
// 初始化颜色数组
LINE_COLOR_DATA = new String[dataSource.size()];
TEXT_COLOR_DATA = new String[dataSource.size()];
// 初始化画笔
paintLineData = new Paint[dataSource.size()];
paintTextData = new Paint[dataSource.size()];
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 初始化
init();
// 开始画线
// 画x轴
canvas.drawLine(xStart, yStart, xStart
+ (width - 2 * MARGIN_SIZE_HORIZONTAL), yStart, paintLineX);
// 写x轴文字
for (int i = 0; i < xData.length; i++) {
if (i % 2 == 0) {
float tempWidth = paintTextX.measureText(xData[i]);
canvas.drawText(xData[i], xStart + i * SCALE_X - tempWidth / 2,
yStart + TEXT_SIZE_X, paintTextX);
}
}
// 画y轴
for (int i = 0; i < xData.length; i++) {
canvas.drawLine(xStart + i * SCALE_X, yStart, xStart + i * SCALE_X,
yStart - (height - 2 * MARGIN_SIZE_VERTICAL), paintLineY);
}
// 写y轴文字
// for (int i = 0; i < yData.length; i++) {
// }
if (Float.parseFloat(yData[1]) == 0) {// 如果最大值和最小值都为0,那么应该做特殊处理.将0的位置设为y轴中间
float tempWidth = paintTextX.measureText(yData[1]);
canvas.drawText(yData[1], xStart - tempWidth - 15, yStart - 2
* SCALE_Y + TEXT_SIZE_Y / 2, paintTextY);
} else {
float tempWidth = paintTextX.measureText(yData[0]);
canvas.drawText(yData[0], xStart - tempWidth - 15, yStart - SCALE_Y
+ TEXT_SIZE_Y / 2, paintTextY);
float tempWidth1 = paintTextX.measureText(yData[1]);
canvas.drawText(yData[1], xStart - tempWidth1 - 15, yStart - 3
* SCALE_Y + TEXT_SIZE_Y / 2, paintTextY);
}
// 画数据
for (int i = 0; i < data.size(); i++) {
List<LineViewPointBean> temp = data.get(i);
for (int j = 0; j < temp.size(); j++) {
LineViewPointBean tempBean = temp.get(j);
tempBean.drawPoint(canvas, paintLineData[i]);
if (j > 0) {
canvas.drawLine(temp.get(j - 1).getPosition().x,
temp.get(j - 1).getPosition().y, temp.get(j)
.getPosition().x,
temp.get(j).getPosition().y, paintLineData[i]);
}
}
}
// 画选中的点和文字
if (touchRegion >= 0) {// 初始化touchRegion为-1,如果有点击到相应区域会对此值进行更改为>=0的数
for (int i = 0; i < data.size(); i++) {
List<LineViewPointBean> temp = data.get(i);
LineViewPointBean tempBean = temp.get(touchRegion);
canvas.drawCircle(tempBean.getPosition().x,
tempBean.getPosition().y, 13, paintLineData[i]);
switch (tempBean.getSort()) {
case 0:
canvas.drawText(tempBean.getyData(),
tempBean.getPosition().x + 5,
tempBean.getPosition().y + 1.5f * TEXT_SIZE_DATA,
paintTextData[i]);
break;
case 1:
canvas.drawText(tempBean.getyData(),
tempBean.getPosition().x + 5,
tempBean.getPosition().y - TEXT_SIZE_DATA,
paintTextData[i]);
break;
default:
break;
}
}
}
}
void init() {
// 获取控件宽高
width = getMeasuredWidth();
height = getMeasuredHeight();
// 计算xStart,yStart
xStart = MARGIN_SIZE_HORIZONTAL;
yStart = MARGIN_SIZE_VERTICAL + (height - 2 * MARGIN_SIZE_VERTICAL);
// 计算SCALE_X SCALE_Y
SCALE_X = (width - 2 * MARGIN_SIZE_HORIZONTAL) / (xData.length-1);
SCALE_Y = (height - 2 * MARGIN_SIZE_VERTICAL) / (yData.length + 2);
// 初始化颜色
LINE_COLOR_X = "#99666666";
LINE_COLOR_Y = "#99666666";
TEXT_COLOR_X = "#99666666";
TEXT_COLOR_Y = "#99666666";
for (int i = 0; i < TEXT_COLOR_DATA.length; i++) {
TEXT_COLOR_DATA[i] = "#99666666";
LINE_COLOR_DATA[i] = "#99666666";
}
// 初始化文字大小
TEXT_SIZE_X = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12,
getResources().getDisplayMetrics());
TEXT_SIZE_Y = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12,
getResources().getDisplayMetrics());
TEXT_SIZE_DATA = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
12, getResources().getDisplayMetrics());
// 初始化画笔
paintLineX = paintSetColor(LINE_COLOR_X);
paintLineY = paintSetColor(LINE_COLOR_Y);
paintTextX = paintSetColor(TEXT_COLOR_X);
paintTextX.setTextSize(TEXT_SIZE_X);
paintTextY = paintSetColor(TEXT_COLOR_Y);
paintTextY.setTextSize(TEXT_SIZE_Y);
for (int i = 0; i < TEXT_COLOR_DATA.length; i++) {
paintLineData[i] = paintSetColor(LINE_COLOR_DATA[i]);
paintTextData[i] = paintSetColor(TEXT_COLOR_DATA[i]);
paintTextData[i].setTextSize(TEXT_SIZE_DATA);
}
// 处理dataSource
handleDataSource();
}
/**
* 初始化画笔的辅助方法.
*
* @param paint
* @param color
*/
Paint paintSetColor(String color) {
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setAntiAlias(true);// 去锯齿
paint.setColor(Color.parseColor(color));
return paint;
}
void handleDataSource() {
data = new ArrayList<List<LineViewPointBean>>();
for (int i = 0; i < dataSource.size(); i++) {
// 有多少条线
List<String> tempDataSource = dataSource.get(i);
List<LineViewPointBean> tempData = new ArrayList<LineViewPointBean>();
for (int j = 0; j < tempDataSource.size(); j++) {
// 每条线有多少点
LineViewPointBean tempLineViewPointBean = new LineViewPointBean();
tempLineViewPointBean.setxData(j + "");
tempLineViewPointBean.setyData(tempDataSource.get(j));
tempLineViewPointBean.setSort(i);
PointF temPointF = new PointF();
// 通过数据值,计算每个点
Float x = 0f, y = 0f;
x = (float) (xStart + j * SCALE_X);
if (Float.parseFloat(yData[1]) == 0) {
// 如果最大值和最小值都为0,那么应该做特殊处理.将0的位置设为y轴中间,同时所有的数据都为0
y = (float) (yStart - 2 * SCALE_Y);
tempLineViewPointBean.setyData("0");
} else {
y = yStart
- SCALE_Y
- (Float.parseFloat(tempDataSource.get(j)) / Float
.parseFloat(yData[1])) * 2 * SCALE_Y;
}
temPointF.set(x, y);
tempLineViewPointBean.setPosition(temPointF);
tempData.add(tempLineViewPointBean);
}
data.add(tempData);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < data.get(0).size(); i++) {
RectF tempRectF = new RectF((float) (xStart + (i - 0.5)
* SCALE_X), 0f, (float) (xStart + (i + 0.5) * SCALE_X),
(float) yStart);
if (tempRectF.contains(event.getX(), event.getY())) {
touchRegion = i;
postInvalidate();
return true;
}
}
break;
case MotionEvent.ACTION_DOWN:
for (int i = 0; i < data.get(0).size(); i++) {
RectF tempRectF = new RectF((float) (xStart + (i - 0.5)
* SCALE_X), 0f, (float) (xStart + (i + 0.5) * SCALE_X),
(float) yStart);
if (tempRectF.contains(event.getX(), event.getY())) {
touchRegion = i;
postInvalidate();
return true;
}
}
break;
}
return true;
}
/**
* 封装了LineView中数据点的类,每个点有4个属性,分别是他的x和y的值,以及在lineView中的position,还有他属于第几组数据
*
* @author CK
*
*/
class LineViewPointBean {
String xData, yData;// x,y的数据
PointF position;// 每个点在当前屏幕中的位置
int sort;// 每个点属于哪类数据
public String getxData() {
return xData;
}
public void setxData(String xData) {
this.xData = xData;
}
public String getyData() {
return yData;
}
public void setyData(String yData) {
this.yData = yData;
}
public PointF getPosition() {
return position;
}
public void setPosition(PointF position) {
this.position = position;
}
public int getSort() {
return sort;
}
public void setSort(int sort) {
this.sort = sort;
}
/**
* 每个点绘制自己的方法
*
* @param canvas
* @param pointPaint
*/
public void drawPoint(Canvas canvas, Paint pointPaint) {
canvas.drawCircle(position.x, position.y, 10, pointPaint);
}
@Override
public String toString() {
return "LineViewPointBean [xData=" + xData + ", yData=" + yData
+ ", position=" + position + ", sort=" + sort + "]";
}
}
}