android 自定义坐标曲线图

编程一年出头了,总是从别人的博客上学习东西,也想把自己的收获分享出去。

第一次写博客,废话不多说,android 自定义坐标曲线图

先上效果图:

第一张(每周数据):

第二张(每月数据):


两张图其实就是数据容量不一样,实现的道理是完全相同的。


先声明,曲线的实现参考了别人的方法,并非原创,此部分详细请见android画经过多点的曲线


好了,简单分析一下图的构成:

1、横坐标文字。2、纵坐标数字。3、上面大部分灰色背景和下部白色背景。4、与x轴平行的白色标度细线。5、最核心的部分,图中的蓝色曲线。


简单理一下实现步骤:

继承View类,然后:1.测控件的尺寸,分好比例。2.在计算好的位置画第一到第四部分。3.参考画曲线的方法,根据数据设置画蓝色曲线。

第一步,在重写onWindowFocusChanged方法中获取控件的宽高尺寸,根据当前的模式,将宽和高分为相应的份数。这里高分为11份,宽分为7份或12份(对应周数据模式和月数据模式)。这里注意不要在构造方法中用getWidth()和getHeight()方法获取宽高尺寸,此时控件还未完成绘制,你获得的所有尺寸都会是0。在activity的onCreate()方法中做此操作也是一样道理,获取的尺寸都是0。

第二步,在ondraw方法中在计算好的位置,在Canvas上用相应的Paint画文字、数字、背景、标度线。注意paint设置不同的颜色,粗细,填充模式。

第三步,依旧在ondraw方法中,根据数据和显示模式画曲线。


然后留出最基本的设置接口:1是设置显示模式,2是设置y轴上的数据集合,就基本完成了。

这里还有一点需要引起极大注意的是:设置数据时一定要保证控件已经获得焦点并且正确的计算出了相关尺寸,否则在没有正确基准尺寸的情况下计算显示出来的图像必定是异常的。这也是为什么代码中设置y轴数据时如果没有焦点要delay 150ms再执行相关操作的原因。如果在activity oncreate时就设置数据并且计算显示,获取的基本尺寸为0,那么肯定什么也显示不出来的。


x轴暂时只有两个尺度模式,y轴的尺度也默认是0~100,这个具体需求具体改变,比如实际项目中就有传过来1000以上的数字,且x轴要显示几月几日,这时候,数字尺度和单位都要做修改,甚至要扩展新的显示模式了,不过最基本的显示架子就是这样,具体有什么需求可以根据实际情况做改变了。


下面贴出代码

package com.epuxun.ewater.view;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import android.R.integer;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;



/**
 * 曲线坐标图
 * @author zhangyu
 * @date 2016-3-8
 */
public class CurveChartView extends View {

	private static final String TAG = "CurveChartView";
	private final int STEP = 12;
	private boolean haveFocuse = false;
	// 曲线图显示模式
	private int model;
	// 按周显示
	public static final int MODEL_WEEK = 0;
	// 控件宽高
	private int viewWidth, viewHeigth;
	// 每一份的水平、竖直尺寸
	private float averageWidth, averageHeigth;
	// 画笔
	Paint grayPaint, bluePaint, blackPaint, whitePaint;
	private String[] weekDay = new String[] { "日", "一", "二", "三", "四", "五", "六" };
	// x、y轴坐标值
	private List<Integer> pointX, pointY;
	// 曲线路径
	private Path curvePath;

	public CurveChartView(Context context) {
		super(context);
		init();
	}

	public CurveChartView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	public CurveChartView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		init();
	}

	private void init() {
		model = MODEL_WEEK;// 默认为显示周数据
		grayPaint = new Paint();
		grayPaint.setColor(Color.parseColor("#F7F7F9"));
		grayPaint.setStrokeWidth(1.5f);
		bluePaint = new Paint();
		bluePaint.setColor(Color.parseColor("#2E88CE"));
		bluePaint.setAntiAlias(true);
		bluePaint.setStyle(Paint.Style.STROKE);
		blackPaint = new Paint();
		blackPaint.setColor(Color.parseColor("#969696"));
		blackPaint.setStrokeWidth(3.0f);
		whitePaint = new Paint();
		whitePaint.setColor(Color.parseColor("#ffffff"));
		whitePaint.setStrokeWidth(2.5f);

		pointX = new ArrayList<Integer>();

		curvePath = new Path();

		for (int i = 0; i < pointX.size(); i++) {
			Log.i(TAG, "init..  pointX[" + i + "] = " + pointX.get(i) + ",pointY[" + i + "] = " + pointY.get(i));
		}
		Log.d(TAG, "init..");
	}

	/**
	 * 设置显示模式 0为显示一周每天数据 否则为显示每月数据
	 * @param model
	 */
	public void setShowModel(int model) {
		this.model = model;
		initMeasure();
		pointX.clear();
		if (model == MODEL_WEEK)
			for (int i = 0; i < 7; i++) {
				pointX.add((int) ((i + 0.5) * averageWidth));
			}
		else
			for (int i = 0; i < 12; i++)
				pointX.add((int) ((i + 0.5) * averageWidth));
		invalidate();
	}

	/**
	 * 设置y坐标上的值
	 * @param pointY
	 */
	public void setPointsY(final List<Integer> pointY1) {
		if (null != pointY1) {
			if(!haveFocuse)
				new Handler().postDelayed(new Runnable() {
					@Override
					public void run() {
						setYDatas(pointY1);
					}
				}, 180);
			else
				setYDatas(pointY1);
		}
	}

	private void setYDatas(final List<Integer> pointY1) {
		pointY = pointY1;
		for(int i = 0;i < pointY.size();i++)
			pointY.set(i, transformYCoordinate(pointY.get(i)));
		invalidate();
	}
	
	/**
	 * 将传入的y数据值转换为屏幕坐标值
	 * 
	 * @param y
	 * @return
	 */
	public int transformYCoordinate(int y) {
		Log.v(TAG, "transformYCoordinate..  viewHeigth = " + viewHeigth);
		return (int) (viewHeigth - ((y / 100f) * viewHeigth));
	}

	@Override
	protected void onDraw(Canvas canvas) {

		drawGrayBackground(canvas);
		drawYCoordinateNumber(canvas);
		drawYCoordinateLine(canvas);
		drawXCoordinateText(canvas);
		drawCurveChart(canvas);

		super.onDraw(canvas);
	}

	/**
	 * 画灰、白色背景
	 */
	private void drawGrayBackground(Canvas canvas) {
		Log.d(TAG, "drawGrayBackground..viewWidth = " + viewWidth + ",viewHeigth = " + viewHeigth);
		grayPaint.setStyle(Style.FILL);
		whitePaint.setStyle(Style.FILL);

		canvas.drawRect(0, 0, viewWidth, viewHeigth * (10f / 11), grayPaint); // 灰色背景
		canvas.drawRect(0, viewHeigth * (10f / 11), viewWidth, viewHeigth, whitePaint); // 白色背景

	}

	/**
	 * 画曲线
	 * 
	 * @param canvas
	 */
	private void drawCurveChart(Canvas canvas) {
		curvePath.reset();

		List<Cubic> calculate_x = calculate(pointX);
		List<Cubic> calculate_y = calculate(pointY);

		if (null != calculate_x && null != calculate_y && calculate_y.size() >= calculate_x.size()) {
			curvePath.moveTo(calculate_x.get(0).eval(0), calculate_y.get(0).eval(0));

			for (int i = 0; i < calculate_x.size(); i++) {
				for (int j = 1; j <= STEP; j++) {
					float u = j / (float) STEP;
					curvePath.lineTo(calculate_x.get(i).eval(u), calculate_y.get(i).eval(u));
				}
			}
		}

		bluePaint.setStrokeWidth(4);
		bluePaint.setStrokeWidth(4);
		canvas.drawPath(curvePath, bluePaint);

	}

	/**
	 * 画Y坐标上横线
	 * 
	 * @param canvas
	 */
	private void drawYCoordinateLine(Canvas canvas) {
		for (int i = 1; i < 10; i++)
			canvas.drawLine(0, averageHeigth * i, viewWidth, averageHeigth * i, whitePaint);
	}

	/**
	 * 画x坐标文字
	 * @param canvas
	 */
	private void drawXCoordinateText(Canvas canvas) {
		blackPaint.setTextSize(DisplayUtils.sp2px(getContext(), 12));
		if (model == MODEL_WEEK)
			for (int i = 0; i < 7; i++)
				canvas.drawText("星期" + weekDay[i], (0.2f + i) * averageWidth, 10.7f * averageHeigth, blackPaint);
		else {
			for (int i = 0; i < 12; i++)
				canvas.drawText(i + 1 + "月", (0.1f + i) * averageWidth, 10.7f * averageHeigth, blackPaint);
		}
	}

	/**
	 * 画y轴坐标值
	 * @param canvas
	 */
	private void drawYCoordinateNumber(Canvas canvas) {
		Log.d(TAG, "drawYCoordinateNumber..");
		blackPaint.setTextSize(DisplayUtils.sp2px(getContext(), 10));
		for (int i = 0; i < 10; i++)
			canvas.drawText("" + (i * 10), viewWidth * (13 / 140), (float) (9.9 - i) * averageHeigth, blackPaint);
	}

	/**
	 * 计算曲线.
	 * @param x
	 * @return
	 */
	private List<Cubic> calculate(List<Integer> x) {
		if (null != x && x.size() > 0) {
			int n = x.size() - 1;
			float[] gamma = new float[n + 1];
			float[] delta = new float[n + 1];
			float[] D = new float[n + 1];
			int i;
			/*
			 * We solve the equation [2 1 ] [D[0]] [3(x[1] - x[0]) ] |1 4 1 |
			 * |D[1]| |3(x[2] - x[0]) | | 1 4 1 | | . | = | . | | ..... | | . |
			 * | . | | 1 4 1| | . | |3(x[n] - x[n-2])| [ 1 2] [D[n]] [3(x[n] -
			 * x[n-1])]
			 * 
			 * by using row operations to convert the matrix to upper triangular
			 * and then back sustitution. The D[i] are the derivatives at the
			 * knots.
			 */

			gamma[0] = 1.0f / 2.0f;
			for (i = 1; i < n; i++) {
				gamma[i] = 1 / (4 - gamma[i - 1]);
			}
			gamma[n] = 1 / (2 - gamma[n - 1]);

			delta[0] = 3 * (x.get(1) - x.get(0)) * gamma[0];
			for (i = 1; i < n; i++) {
				delta[i] = (3 * (x.get(i + 1) - x.get(i - 1)) - delta[i - 1]) * gamma[i];
			}
			delta[n] = (3 * (x.get(n) - x.get(n - 1)) - delta[n - 1]) * gamma[n];

			D[n] = delta[n];
			for (i = n - 1; i >= 0; i--) {
				D[i] = delta[i] - gamma[i] * D[i + 1];
			}

			/* now compute the coefficients of the cubics */
			List<Cubic> cubics = new LinkedList<Cubic>();
			for (i = 0; i < n; i++) {
				Cubic c = new Cubic(x.get(i), D[i], 3 * (x.get(i + 1) - x.get(i)) - 2 * D[i] - D[i + 1], 2 * (x.get(i) - x.get(i + 1)) + D[i] + D[i + 1]);
				cubics.add(c);
			}
			return cubics;
		}
		return null;
	}

	public class Cubic {

		float a, b, c, d; /* a + b*u + c*u^2 +d*u^3 */

		public Cubic(float a, float b, float c, float d) {
			this.a = a;
			this.b = b;
			this.c = c;
			this.d = d;
		}

		/** evaluate cubic */
		public float eval(float u) {
			return (((d * u) + c) * u + b) * u + a;
		}
	}

	@Override
	public void onWindowFocusChanged(boolean hasWindowFocus) {
		if (hasWindowFocus) {
			haveFocuse = true;
			initMeasure();
			setShowModel(this.model);
		}
		super.onWindowFocusChanged(hasWindowFocus);
	}

	private void initMeasure() {
		viewWidth = getWidth();
		viewHeigth = getHeight();

		averageHeigth = viewHeigth / 11;
		if (model == MODEL_WEEK)
			averageWidth = viewWidth / 7;
		else
			averageWidth = viewWidth / 12;

	}
}

代码比较基础,结合注释应该很容易看明白,其中肯定还有多多少少的问题,欢迎大家指正,当然,也更希望对需要学习的同学有所帮助!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值