Android自定义绘制图表

最近项目中需要数据统计进行图表展示,无奈设计效果太奇特无法使用一些图表控件来达到效果,只能自己动手绘制,咬牙坚持三天终于有点

效果了,记录分享以备后用。本着学习的态度,还是从基础开始吧

1.Android画图最基本的三个对象(Color,Paint,Canvas)

1)Canvas画布

    Canvas主要用于2D绘图,那么它也提供了很多相应的drawXxx()方法,方便我们在Canvas对象上画画,drawXxx()具有多种类型,可以画出:

    点、线、矩形、圆形、椭圆、文字、位图等的图形

    Canvas():创建一个空的画布,可以使用setBitmap()方法来设置绘制的具体画布

    Canvas(Bitmapbitmap):以bitmap对象创建一个画布,则将内容都绘制在bitmap上,bitmap不得为null

    getHeight():得到Canvas的高度

    getWidth():得到Canvas的宽度

    drawBitmap(Bitmap bitmap,float left,float top,Paint paint):在指定坐标绘制位图

    drawLine(float startX,float startY,float stopX,float stopY,Paint paint):根据给定的起始点和结束点之间绘制连线

    drawPath(Path path,Paint paint):根据给定的path,绘制连线

    drawPoint(float x,float y,Paint paint):根据给定的坐标,绘制点

    drawText(String text,int start,int end,Paint paint):根据给定的坐标,绘制文字

    drawRect(RectF,Paint)方法用于画矩形,第一个参数为图形显示区域,第二个参数为画笔,设置好图形显示区域Rect和画笔Paint后,即可画图

    drawRoundRect(RectF, float, float, Paint):方法用于画圆角矩形,第一个参数为图形显示区域,第二个参数和第三个参数分别是水平圆角半径和垂直圆角半径

    void drawLine(startX, startY, stopX, stopY,paint):前四个参数的类型均为float,最后一个参数类型为Paint。表示用画笔paint从点(startX,startY)到点(stopX,stopY)画一条直线;

    void drawArc(oval,startAngle, sweepAngle, useCenter,paint):第一个参数oval为RectF类型,即圆弧显示区域,startAngle和sweepAngle均为float类型,分别表示圆弧起始角度和圆弧度数,3点钟方向为0度,useCenter设置是否显示圆心,boolean类型,paint为画笔;

    void drawCircle(float,float, float,Paint)方法用于画圆,前两个参数代表圆心坐标,第三个参数为圆半径,第四个参数是画笔;

2)Paint画笔 

    画笔主要用于设置绘图风格,包括画笔颜色、画笔粗细、填充风格

    setARGB(int a,int r,int g,int b):设置ARGB颜色

    setColor(int color):设置颜色

    setAlpha(int a):设置透明度

    setPathEffect(PathEffect effect):设置绘制路径时的路径效果

    setShader(Shader shader):设置Paint的填充效果

    setAntiAlias(boolean aa):设置是否抗锯齿

    setStrokeWidth(float width):设置Paint的笔触宽度

    setStyle(Paint.Style style):设置Paint的填充风格

    setTextSize(float textSize):设置绘制文本时的文字大小

3)Color颜色

    Color.颜色名,来获取颜色,应为是静态的,返回一个整数值,如果是调至出来的颜色,可以通过Color.parseColor("#808080")转换

    注:Moveto是设置起点坐标,Lineto是设置终点坐标,Lineto还有自动把上一点当作起点,所以设置起点只需设置一次,

    以后只需用Lineto便可以把各类不间断的曲线画出来

2.位置坐标

    画布原点为左上角,向右是X轴正方向,向下是Y轴正方向,下图320X480分辨率屏看着更直观,Rect(150,75, 260, 120)矩形

3.触摸事件

    首先要实现OnTouchListener接口,然后重写方法:

    public boolean onTouch(View v, MotionEvent event); 

    从这个方法中我们就可以获取实现两指缩放功能的全部信息。View v是触发事件的源,MotionEvent event即一个触摸事件。对屏幕的

    几乎所有操作都会触发事件,如点击、放开、滑动等。不同的事件在MotionEvent中有不同的id,我们可以根据event.getAction() &

    MotionEvent.ACTION_MASK的结果来判断是何种事件。有如下事件使我们要用到的:

    •MotionEvent.ACTION_DOWN:在第一个点被按下时触发

    •MotionEvent.ACTION_UP:当屏幕上唯一的点被放开时触发

    •MotionEvent.ACTION_POINTER_DOWN:当屏幕上已经有一个点被按住,此时再按下其他点时触发。

    •MotionEvent.ACTION_POINTER_UP:当屏幕上有多个点被按住,松开其中一个点时触发(即非最后一个点被放开时)。

    •MotionEvent.ACTION_MOVE:当有点在屏幕上移动时触发。值得注意的是,由于它的灵敏度很高,而我们的手指又不可能完全静止

    (即使我们感觉不到移动,但其实我们的手指也在不停地抖动),所以实际的情况是,基本上只要有点在屏幕上,此事件就会一直不停地被触发。

    举例子来说:当我们放一个食指到屏幕上时,触发ACTION_DOWN事件;再放一个中指到屏幕上,触发ACTION_POINTER_DOWN事件;此时

    再把食指或中指放开,都会触发ACTION_POINTER_UP事件;再放开最后一个手指,触发ACTION_UP事件;而同时在整个过程中,

    ACTION_MOVE事件会一直不停地被触发。

4.回调事件

    有些绘图中关联事件会有需要回调事件来获取当前位置的数据或者其他内容,这个时候可以自定义回调事件

5.自定义折线图案例

    效果如图

   

    具体实现代码如下:

/**
 * 单线折线图控件
 * 
 * @author 14042054
 * 
 */
public class SingleLineChart extends View {

	private Context mContext;
	/* 画笔 */
	private Paint mPaint = null;
	/* 数据集合 */
	private List<MySalesByDay> salesPVHolders = null;
	/* 数据坐标点集合 */
	private List<ChartPoint> salesPVPoints = null;
	private double highest = 0; // 数据最大值
	private double lowerest = 0; // 数据最小值
	/* 是否手动设值 */
	private boolean isSetting = true;
	/* 滑动线监听 */
	private LineMoveListener moveListener;
	/* 触控点 */
	private PointF mTouchPoint = new PointF();
	/* 画布宽高 */
	private int w = 0;
	private int h = 0;
	private int paddingLeftRight = 34; // 坐标间视图的水平间距,可以由此调节坐标轴的宽度
	private int paddingTop = 100; // 视图上边距,View顶部区域,可以由此调节坐标轴的
	private int padingBottom = 54; // 视图下边距,View底部区域,可以由此调节坐标轴的
	/* 文字大小 */
	private int label_text_size = 22;
	/* 圆点大小 */
	private int radius = 8;
	/* 水平日期 天数 */
	private int date = 10;
	/* 线高 */
	private int paint_height_line = 2;
	/* 选中位置 */
	private int selectIndex = -1;
	/* 画布颜色 */
	private int color_canvas = Color.parseColor("#212933");
	/* 上下分割线颜色 */
	private int color_cut_line = Color.parseColor("#808080");
	/* 水平线颜色 */
	private int color_horizontal_line = Color.parseColor("#323e4d");
	/* 垂直线颜色 */
	private int color_vertical_line = Color.parseColor("#50b255");
	/* 数据线颜色 */
	private int color_data_line = Color.parseColor("#ffaa00");

	private Bitmap handleBitmap; // 滑线手柄

	public SingleLineChart(Context context, AttributeSet attrs) {
		super(context, attrs);
		this.mContext = context;
		this.mPaint = new Paint();
		this.mPaint.setAntiAlias(true);
		this.handleBitmap = getHandleDrawable();
		this.salesPVHolders = new ArrayList<MySalesByDay>();
		this.salesPVPoints = new ArrayList<ChartPoint>();
	}

	/**
	 * 设置图表数据
	 * 
	 * @param holders
	 */
	public void setInitData(List<MySalesByDay> holders) {
		if (null != holders && !holders.isEmpty()) {
			// 手动设置
			this.isSetting = true;
			// 图表数据
			this.salesPVHolders = holders;
			// 图表天数
			date = this.salesPVHolders.size();
			// 获取列表中最大最小值
			double[] exvalue = findHightestAndLowerst(salesPVHolders);
			this.highest = (exvalue[0]);
			this.lowerest = (exvalue[1]);
			// 清除旧得数据
			this.salesPVPoints.clear();
			// 默认当前选择最后一天位置
			this.selectIndex = holders.size() - 1;
		} else {
			return;
		}
		invalidate();
	}

	/**
	 * 绘制趋势图
	 */
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		w = getWidth();
		h = getHeight();
		// 设置画布颜色
		canvas.drawColor(color_canvas);
		// 绘制数据加载提示
		if (null == this.salesPVPoints) {
			drawLoading(canvas);
		}
		// 绘制背景和数据
		else {
			drawChart(canvas);
		}
	}
	
	/**
	 * 绘制图表加载提示
	 * 
	 * @param canvas
	 */
	private void drawLoading(Canvas canvas){
		mPaint.setStrokeWidth(paint_height_line);
		mPaint.setTextSize(label_text_size);
		mPaint.setStyle(Style.FILL);
		mPaint.setColor(Color.BLACK);
		String notify = "loading...";
		canvas.drawText(notify, w / 2 - mPaint.measureText(notify) / 2, h / 2, mPaint);
	}

	/**
	 * 绘制图表
	 * 
	 * @param canvas
	 */
	public void drawChart(Canvas canvas) {
		// 计算图表单位宽度
		int cell_width = (w - paddingLeftRight * 2) / (date - 1);
		// 计算图表单位高度
		int cell_height = h / 7;
		// 计算坐标单位高度
		double pix_value_unit = 0;
		if (this.highest == this.lowerest) {
			pix_value_unit = (h - paddingTop - padingBottom) / (this.highest - this.lowerest + 1);
		} else {
			pix_value_unit = (h - paddingTop - padingBottom) / (this.highest - this.lowerest);
		}
		// 清空图表坐标数据集合
		this.salesPVPoints.clear();
		Path path = new Path();
		path.reset();
		// 计算图表比拟坐标
		for (int i = 0, isize = date; i < isize; i++) {
			int startX = paddingLeftRight + i * cell_width + 6;
			if (i < salesPVHolders.size()) {
				MySalesByDay holder = salesPVHolders.get(i);
				int x = startX;
				double y = ((h - pix_value_unit * (holder.getIncome() - this.lowerest)) - cell_height - 2);
				ChartPoint point = new ChartPoint(x, (int) y);
				// 保存图表比拟坐标
				this.salesPVPoints.add(point);
				// 生成坐标连线路径
				if (0 == i) {
					path.moveTo(point.x, point.y);
				} else {
					path.lineTo(point.x, point.y);
				}
			}
		}

		// 画水平网格
		mPaint.setColor(color_horizontal_line);
		mPaint.setStrokeWidth(paint_height_line / 4);
		for (int i = 1, isize = 7; i < isize; i++) { // 隐藏最上最下两条水平线
			canvas.drawLine(0, cell_height * i, w, cell_height * i, mPaint);
		}
		// 画垂直网格
		for (int i = 0, isize = 10; i <= isize; i++) {
			canvas.drawLine(w / isize * i, cell_height, w / isize * i, h - cell_height - 5, mPaint);
		}
		// 画数据折线
		mPaint.setColor(color_data_line);
		mPaint.setStyle(Style.STROKE);
		mPaint.setStrokeWidth(paint_height_line * 2);
		canvas.drawPath(path, mPaint);
		// 底部分割线
		mPaint.setColor(color_cut_line);
		mPaint.setStrokeWidth(paint_height_line);
		canvas.drawLine(0, h - cell_height - 2, w, h - cell_height - 2, mPaint);
		// 左右日期文字
		mPaint.setColor(Color.WHITE);
		mPaint.setStyle(Style.FILL);
		mPaint.setStrokeWidth(paint_height_line / 2);
		mPaint.setTextSize(label_text_size);
		mPaint.setFakeBoldText(false);
		String startDate = DateUtil.getMonthAndDayFromString(salesPVHolders.get(0).getDate());// 日期格式06/12
		canvas.drawText(startDate, 10, h - cell_height + 25, mPaint);
		String endDate = DateUtil.getMonthAndDayFromString(salesPVHolders.get(salesPVHolders.size() - 1).getDate());
		canvas.drawText(endDate, w - 70, h - cell_height + 25, mPaint);

		// 选中
		ChartPoint selectedPoint = null;
		if (-1 != selectIndex && this.isSetting) {
			selectedPoint = salesPVPoints.get(selectIndex); 
		} else {
			// 找到离触摸点最近的点
			Object rets[] = findNearestPoint(salesPVPoints); 
			selectedPoint = (ChartPoint) rets[0];
			selectIndex = (Integer) rets[1];
		}
		if (selectedPoint == null) { 
			return;
		}
		if (selectIndex >= 0) {
			String date = DateUtil.getMonthAndDayFromString(salesPVHolders.get(selectIndex).getDate());
			// 移动竖线
			drawMoveLine(canvas, selectedPoint, cell_width, date);
			// 选中区间
			drawSelectedInterval(canvas, selectIndex);
		}
		
		// 添加交线小圆点
		mPaint.setColor(color_data_line);
		mPaint.setStyle(Style.FILL);
		canvas.drawCircle(selectedPoint.x, selectedPoint.y, radius, mPaint);
		// 复原画笔颜色
		mPaint.setColor(Color.WHITE);
		// 监听回调
		if (null != moveListener) {
			moveListener.onMove(selectIndex);
		}
	}

	/**
	 * 画半透明区域
	 * 
	 * @param canvas
	 * @param startPoint
	 * @param endPoint
	 */
	private void drawRectransparent(Canvas canvas, Path p, ChartPoint startPoint, ChartPoint endPoint, int currentIndex) {
		int cell_height = h / 7;
		Rect rtect = new Rect(startPoint.x, cell_height, endPoint.x, h - cell_height - 2);
		mPaint.setColor(0x6634DE71);
		mPaint.setStyle(Style.FILL);
		mPaint.setAlpha(38);
		canvas.drawRect(rtect, mPaint);
	}
	
	/**
	 *  画半透明区域
	 * 
	 * @param canvas
	 * @param currentIndex
	 */
	private void drawSelectedInterval(Canvas canvas, int currentIndex){
		int cell_height = h / 7;
		Path path = new Path();
		path.reset();
		int size = salesPVPoints.size();
		for (int i = currentIndex; i < size; i++) {
			if (currentIndex == i) {
				if(salesPVHolders.get(i).getIncome() > 0){
					path.moveTo(salesPVPoints.get(i).x, h -cell_height);
					path.lineTo(salesPVPoints.get(i).x, salesPVPoints.get(i).y);
				} else {
					path.moveTo(salesPVPoints.get(i).x, salesPVPoints.get(i).y);
				}
			} else {
				if(i == size - 1 && salesPVHolders.get(size - 1).getIncome() > 0){
					ChartPoint endPoint = salesPVPoints.get(size - 1);
					path.lineTo(salesPVPoints.get(i).x, salesPVPoints.get(i).y);
					path.lineTo(endPoint.x, h - cell_height);
				} else {
					path.lineTo(salesPVPoints.get(i).x, salesPVPoints.get(i).y);
				}
			}
		}
		path.close();
		mPaint.setColor(0x6634DE71);
		mPaint.setStyle(Style.FILL);
		mPaint.setAlpha(38);
		canvas.drawPath(path, mPaint);
	}

	/**
	 * 画移动竖线
	 * 
	 * @param selectedPoint
	 * @param cell_width
	 * @param canvas
	 */
	public void drawMoveLine(Canvas canvas, ChartPoint selectedPoint, int cell_width, String date) {
		int cell_height = h / 7;
		// 移动竖线
		mPaint.setColor(color_vertical_line);
		mPaint.setStrokeWidth(paint_height_line);
		canvas.drawLine(selectedPoint.x, cell_height - 20, selectedPoint.x, h - cell_width, mPaint);// 移动竖线

		// 手柄图片
		mPaint.setDither(true);// 防抖动
		mPaint.setFilterBitmap(true);// 用来对Bitmap进行滤波处理,这样,当你选择Drawable时,会有抗锯齿的效果
		Rect src = new Rect(0, 0, handleBitmap.getWidth(), handleBitmap.getHeight());
		Rect dst = new Rect(selectedPoint.x - handleBitmap.getWidth() / 2, h - cell_height, selectedPoint.x + handleBitmap.getWidth() / 2, h - cell_height + handleBitmap.getHeight());
		canvas.drawBitmap(handleBitmap, src, dst, mPaint);

		// 日期文字
		mPaint.setColor(Color.WHITE);
		mPaint.setTextSize(label_text_size);
		mPaint.setStyle(Style.FILL);
		mPaint.setFakeBoldText(true);
		canvas.drawText(date, selectedPoint.x - 28, h, mPaint);
	}

	/**
	 * 获取滑线手柄图片
	 * 
	 * @return
	 */
	private Bitmap getHandleDrawable() {
		Drawable icon = mContext.getResources().getDrawable(R.drawable.home_sjtj_fwl_slider_nor);
		if (icon instanceof BitmapDrawable) {
			BitmapDrawable bd = (BitmapDrawable) icon;
			return bd.getBitmap();
		} else {
			return null;
		}
	}

	/**
	 * 找出列表中的最大值和最小值
	 * 
	 * @param holders
	 * @return
	 */
	private double[] findHightestAndLowerst(List<MySalesByDay> holders) {
		double hightest = Integer.MIN_VALUE, lowerst = Integer.MAX_VALUE;
		for (int i = 0, isize = holders.size(); i < isize; i++) {
			MySalesByDay holder = holders.get(i);
			double value = holder.getIncome();
			if (value > hightest) {
				hightest = value;
			}
			if (value < lowerst) {
				lowerst = value;
			}
		}
		double[] heightAndLow = { hightest, lowerst };
		return heightAndLow;
	}

	/**
	 * 找到最近的点
	 * 
	 * @param pointlist
	 * @return
	 */
	private Object[] findNearestPoint(List<ChartPoint> pointlist) {
		Point p = null;
		Integer index = 0;
		float deltax = Integer.MAX_VALUE;
		for (int i = 0, isize = pointlist.size(); i < isize; i++) {
			Point tempPoint = pointlist.get(i);
			float distance = Math.abs(mTouchPoint.x - tempPoint.x);
			if (distance < deltax) {
				deltax = distance;
				p = tempPoint;
				index = i;
			}
		}
		Object[] data = { p, index };
		return data;
	}

	/**
	 * 接触事件
	 */
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		this.isSetting = false;
		mTouchPoint.x = event.getX();
		mTouchPoint.y = event.getY();
		invalidate();
		return true;
	}

	public LineMoveListener getMoveListener() {
		return moveListener;
	}

	public void setMoveListener(LineMoveListener moveListener) {
		this.moveListener = moveListener;
	}

	public int getSelectIndex() {
		return selectIndex;
	}

	public void setSelectIndex(int selectIndex) {
		this.isSetting = true;
		this.selectIndex = selectIndex;
		invalidate();
	}

	/**
	 * 监听接口
	 * 
	 * @author 14042054
	 *
	 */
	public static interface LineMoveListener {
		public void onMove(int index);
	}

	public static interface DrawCallBack {
		public void onDrawFinished();
	}

}

比拟坐标对象

public class ChartPoint extends Point {
	
	public int x;
	public int y;

	public ChartPoint(int x, int y) {
		set(x, y);
		this.x = x;
		this.y = y;
	}
}

界面使用

// 填充折线图和定位初始位置(7天)
salesLineChartView.setInitData(mySalesByDays);
salesLineChartView.setSelectIndex(mySalesByDays.size() - DEFAULT_DAYS);


 

附参考:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1212/703.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值