Android自定义折线图

最终实现效果如图所示,可以支持多个折线的对比效果,当手指滑动到相应区域的时候会显示相应点的折线的数据.

下面开始项目的构建,我们可以将整个效果分为几个部分,第一个部分是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 + "]";
		}

	}
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值