当前位置: 代码迷 >> Android >> Android开发之自定义View课题(二):自定义饼图
  详细解决方案

Android开发之自定义View课题(二):自定义饼图

热度:37   发布时间:2016-04-28 03:03:25.0
Android开发之自定义View专题(二):自定义饼图

在图表里面,常用的图标一般为折线图、柱形图和饼图,上周,博主已经将柱形图分享。在博主的项目里面其实还用到了饼图,但没用到折线图。其实学会了其中一个,再去写其他的,应该都是知道该怎么写的,原理都是自己绘制图形,然后获取触摸位置判定点击事件。好了,废话不多说,直接上今天的饼图的效果图


这次也是博主从项目里面抽离出来的,这次的代码注释会比上次的柱形图更加的详细,更加便于有兴趣的朋友一起学习。图中的那个圆形指向箭头不属于饼图的部分,是在布局文件中为了美化另外添加进去的,有兴趣的朋友可以下载完整的项目下来研究学习。

下载地址:http://download.csdn.net/detail/victorfreedom/8322639

本来想上传到github的,但是网络不给力,过几天再上传吧。


代码部分就直接贴出自定义饼图部分,支持xml文件写入构造,也支持new方法构造。

package com.freedom.piegraph;import android.annotation.SuppressLint;import android.content.Context;import android.content.res.TypedArray;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.RectF;import android.os.Handler;import android.util.AttributeSet;import android.view.MotionEvent;import android.view.View;/** * @ClassName: PiegraphView * @author victor_freedom ([email protected]) * @createddate 2015年1月3日 下午4:30:10 * @Description: 自定义饼状图 */@SuppressLint({ "DrawAllocation" })public class PiegraphView extends View implements Runnable {	// 动画速度	private float moveSpeed = 3.0F;	// 总数值	private double total;	// 各饼块对应的数值	private Double[] itemValuesTemp;	// 各饼块对应的数值	private Double[] itemsValues;	// 各饼块对应的颜色	private String[] itemColors;	// 各饼块的角度	private float[] itemsAngle;	// 各饼块的起始角度	private float[] itemsStartAngle;	// 各饼块的占比	private float[] itemsPercent;	// 旋转起始角度	private float rotateStartAng = 0.0F;	// 旋转结束角度	private float rotateEndAng = 0.0F;	// 正转还是反转	private boolean isClockWise;	// 正在旋转	private boolean isRotating;	// 是否开启动画	private boolean isAnimEnabled = true;	// 边缘圆环的颜色	private String loopStrokeColor;	// 边缘圆环的宽度	private float strokeWidth = 0.0F;	// 饼图半径,不包括圆环	private float radius;	// 当前item的位置	private int itemPostion = -1;	// 停靠位置	private int stopPosition = 0;	// 停靠位置	public static final int TO_RIGHT = 0;	public static final int TO_BOTTOM = 1;	public static final int TO_LEFT = 2;	public static final int TO_TOP = 3;	// 颜色值	private final String[] DEFAULT_ITEMS_COLORS = { "#FF0000", "#FFFF01",			"#FF9933", "#9967CC", "#00CCCC", "#00CC33", "#0066CC", "#FF6799",			"#99FF01", "#FF67FF", "#4876FF", "#FF00FF", "#FF83FA", "#0000FF",			"#363636", "#FFDAB9", "#90EE90", "#8B008B", "#00BFFF", "#FFFF00",			"#00FF00", "#006400", "#00FFFF", "#00FFFF", "#668B8B", "#000080",			"#008B8B" };	// 消息接收器	private Handler piegraphHandler = new Handler();	// 监听器集合	private OnPiegraphItemSelectedListener itemSelectedListener;	public PiegraphView(Context context, String[] itemColors,			Double[] itemSizes, float total, int radius, int strokeWidth,			String strokeColor, int stopPosition, int separateDistence) {		super(context);		this.stopPosition = stopPosition;		if ((itemSizes != null) && (itemSizes.length > 0)) {			itemValuesTemp = itemSizes;			this.total = total;			// 重设总值			reSetTotal();			// 重设各个模块的值			refreshItemsAngs();		}		if (radius < 0)			// 默认半径设置为100			this.radius = 100.0F;		else {			this.radius = radius;		}		// 默认圆环宽度设置为2		if (strokeWidth < 0)			strokeWidth = 2;		else {			this.strokeWidth = strokeWidth;		}		loopStrokeColor = strokeColor;		if (itemColors == null) {			// 如果没有设定颜色,则使用默认颜色值			setDefaultColor();		} else if (itemColors.length < itemSizes.length) {			this.itemColors = itemColors;			// 如果设置的颜色值和设定的集合大小不一样,那么需要充默认颜色值集合里面补充颜色,一般是不会出现这种情况。			setDifferentColor();		} else {			this.itemColors = itemColors;		}		invalidate();	}	public PiegraphView(Context context, AttributeSet attrs) {		super(context, attrs);		loopStrokeColor = "#000000";		// 把我们自定义的属性,放在attrs的属性集合里面		TypedArray a = context.obtainStyledAttributes(attrs,				R.styleable.PiegraphView);		radius = ScreenUtil.dip2px(getContext(),				a.getFloat(R.styleable.PiegraphView_radius, 100));		strokeWidth = ScreenUtil.dip2px(getContext(),				a.getFloat(R.styleable.PiegraphView_strokeWidth, 2));		moveSpeed = a.getFloat(R.styleable.PiegraphView_moveSpeed, 5);		if (moveSpeed < 1F) {			moveSpeed = 1F;		}		if (moveSpeed > 5.0F) {			moveSpeed = 5.0F;		}		invalidate();		a.recycle();	}	/**	 * @Title: setRaduis	 * @Description: 设置半径	 * @param radius	 * @throws	 */	public void setRaduis(float radius) {		if (radius < 0)			this.radius = 100.0F;		else {			this.radius = radius;		}		invalidate();	}	public float getRaduis() {		return radius;	}	/**	 * @Title: setStrokeWidth	 * @Description: 设置圆环宽度	 * @param strokeWidth	 * @throws	 */	public void setStrokeWidth(int strokeWidth) {		if (strokeWidth < 0)			strokeWidth = 2;		else {			this.strokeWidth = strokeWidth;		}		invalidate();	}	public float getStrokeWidth() {		return strokeWidth;	}	/**	 * @Title: setStrokeColor	 * @Description: 设置圆环颜色	 * @param strokeColor	 * @throws	 */	public void setStrokeColor(String strokeColor) {		loopStrokeColor = strokeColor;		invalidate();	}	public String getStrokeColor() {		return loopStrokeColor;	}	/**	 * @Title: setitemColors	 * @Description: 设置个饼块的颜色	 * @param colors	 * @throws	 */	public void setitemColors(String[] colors) {		if ((itemsValues != null) && (itemsValues.length > 0)) {			// 如果传入值未null,则使用默认的颜色			if (colors == null) {				setDefaultColor();			} else if (colors.length < itemsValues.length) {				// 如果传入颜色不够,则从默认颜色中填补				itemColors = colors;				setDifferentColor();			} else {				itemColors = colors;			}		}		invalidate();	}	public String[] getitemColors() {		return itemColors;	}	/**	 * @Title: setitemsValues	 * @Description: 设置各饼块数据	 * @param items	 * @throws	 */	public void setitemsValues(Double[] items) {		if ((items != null) && (items.length > 0)) {			itemValuesTemp = items;			// 重设总值,默认为所有值的和			reSetTotal();			refreshItemsAngs();			setitemColors(itemColors);		}		invalidate();	}	public Double[] getitemsValues() {		return itemValuesTemp;	}	public void setTotal(int total) {		this.total = total;		reSetTotal();		invalidate();	}	public double getTotal() {		return total;	}	/**	 * @Title: setAnimEnabled	 * @Description: 设置是否开启旋转动画	 * @param isAnimEnabled	 * @throws	 */	public void setAnimEnabled(boolean isAnimEnabled) {		this.isAnimEnabled = isAnimEnabled;		invalidate();	}	public boolean isAnimEnabled() {		return isAnimEnabled;	}	public void setmoveSpeed(float moveSpeed) {		if (moveSpeed < 1F) {			moveSpeed = 1F;		}		if (moveSpeed > 5.0F) {			moveSpeed = 5.0F;		}		this.moveSpeed = moveSpeed;	}	public float getmoveSpeed() {		if (isAnimEnabled()) {			return moveSpeed;		}		return 0.0F;	}	/**	 * @Title: setShowItem	 * @Description: 旋转到指定位置的item	 * @param position	 *            位置	 * @param anim	 *            是否动画	 * @param listen	 *            是否设置监听器	 * @throws	 */	public void setShowItem(int position, boolean anim) {		if ((itemsValues != null) && (position < itemsValues.length)				&& (position >= 0)) {			// 拿到需要旋转的角度			rotateEndAng = getLastrotateStartAngle(position);			itemPostion = position;			if (anim) {				rotateStartAng = 0.0F;				if (rotateEndAng > 0.0F) {					// 如果旋转角度大于零,则顺时针旋转					isClockWise = true;				} else {					// 如果小于零则逆时针旋转					isClockWise = false;				}				// 开始旋转				isRotating = true;			} else {				rotateStartAng = rotateEndAng;			}			// 如果有监听器			if (null != itemSelectedListener) {				itemSelectedListener.onPieChartItemSelected(position,						itemColors[position], itemsValues[position],						itemsPercent[position],						getAnimTime(Math.abs(rotateEndAng - rotateStartAng)));			}			// 开始旋转			piegraphHandler.postDelayed(this, 1L);		}	}	private float getLastrotateStartAngle(int position) {		float result = 0.0F;		// 拿到旋转角度,根据停靠位置进行修正		result = itemsStartAngle[position] + itemsAngle[position] / 2.0F				+ getstopPositionAngle();		if (result >= 360.0F) {			result -= 360.0F;		}		if (result <= 180.0F)			result = -result;		else {			result = 360.0F - result;		}		return result;	}	/**	 * @Title: getstopPositionAngle	 * @Description: 根据停靠位置修正旋转角度	 * @return	 * @throws	 */	private float getstopPositionAngle() {		float resultAngle = 0.0F;		switch (stopPosition) {		case TO_RIGHT:			resultAngle = 0.0F;			break;		case TO_LEFT:			resultAngle = 180.0F;			break;		case TO_TOP:			resultAngle = 90.0F;			break;		case TO_BOTTOM:			resultAngle = 270.0F;			break;		}		return resultAngle;	}	public int getShowItem() {		return itemPostion;	}	public void setstopPosition(int stopPosition) {		this.stopPosition = stopPosition;	}	public int getstopPosition() {		return stopPosition;	}	/**	 * @Title: refreshItemsAngs	 * @Description: 初始化各个角度	 * @throws	 */	private void refreshItemsAngs() {		if ((itemValuesTemp != null) && (itemValuesTemp.length > 0)) {			// 如果出现总值比设定的集合的总值还大,那么我们自动的增加一个模块出来(几乎不会出现这种情况)			if (getTotal() > getAllSizes()) {				itemsValues = new Double[itemValuesTemp.length + 1];				for (int i = 0; i < itemValuesTemp.length; i++) {					itemsValues[i] = itemValuesTemp[i];				}				itemsValues[(itemsValues.length - 1)] = (getTotal() - getAllSizes());			} else {				itemsValues = new Double[itemValuesTemp.length];				itemsValues = itemValuesTemp;			}			// 开始给各模块赋值			itemsPercent = new float[itemsValues.length];			itemsStartAngle = new float[itemsValues.length];			itemsAngle = new float[itemsValues.length];			float startAngle = 0.0F;			for (int i = 0; i < itemsValues.length; i++) {				itemsPercent[i] = ((float) (itemsValues[i] * 1.0D / getTotal() * 1.0D));			}			for (int i = 0; i < itemsPercent.length; i++) {				itemsAngle[i] = (360.0F * itemsPercent[i]);				if (i != 0) {					itemsStartAngle[i] = startAngle + itemsAngle[i - 1];					startAngle = 360.0F * itemsPercent[(i - 1)] + startAngle;				} else {					// Android默认起始位置设定是右侧水平,初始化默认停靠位置也在右边。有兴趣的同学可以根据自己的喜好修改					itemsStartAngle[i] = -itemsAngle[i] / 2;					startAngle = itemsStartAngle[i];				}			}		}	}	/**	 * 绘图	 */	protected void onDraw(Canvas canvas) {		super.onDraw(canvas);		// 饼图半径加圆环半径		float realRadius = radius + strokeWidth;		Paint paint = new Paint();		paint.setAntiAlias(true);		float lineLength = 2.0F * radius + strokeWidth;		if (strokeWidth != 0.0F) {			// 空心的画笔,先画外层圆环			paint.setStyle(Paint.Style.STROKE);			paint.setColor(Color.parseColor(loopStrokeColor));			paint.setStrokeWidth(strokeWidth);			canvas.drawCircle(realRadius, realRadius, realRadius - 5, paint);		}		if ((itemsAngle != null) && (itemsStartAngle != null)) {			// 旋转角度			canvas.rotate(rotateStartAng, realRadius, realRadius);			// 设定饼图矩形			RectF oval = new RectF(strokeWidth, strokeWidth, lineLength,					lineLength);			// 开始画各个扇形			for (int i = 0; i < itemsAngle.length; i++) {				oval = new RectF(strokeWidth, strokeWidth, lineLength,						lineLength);				// 先画实体				paint.setStyle(Paint.Style.FILL);				paint.setColor(Color.parseColor(itemColors[i]));				canvas.drawArc(oval, itemsStartAngle[i], itemsAngle[i], true,						paint);				// 再画空心体描边				paint.setStyle(Paint.Style.STROKE);				paint.setStrokeWidth(strokeWidth / 2);				paint.setColor(Color.WHITE);				canvas.drawArc(oval, itemsStartAngle[i], itemsAngle[i], true,						paint);			}		}		// 画中心的小圆		paint.setStyle(Paint.Style.FILL);		paint.setColor(Color.LTGRAY);		canvas.drawCircle(realRadius, realRadius,				ScreenUtil.dip2px(getContext(), 40), paint);		// 描边		paint.setStyle(Paint.Style.STROKE);		paint.setColor(Color.WHITE);		paint.setStrokeWidth(strokeWidth);		canvas.drawCircle(realRadius, realRadius,				ScreenUtil.dip2px(getContext(), 40), paint);	}	/**	 * 触摸事件	 */	public boolean onTouchEvent(MotionEvent event) {		if ((!isRotating) && (itemsValues != null) && (itemsValues.length > 0)) {			float x1 = 0.0F;			float y1 = 0.0F;			switch (event.getAction()) {			// 按下			case MotionEvent.ACTION_DOWN:				x1 = event.getX();				y1 = event.getY();				float r = radius + strokeWidth;				if ((x1 - r) * (x1 - r) + (y1 - r) * (y1 - r) - r * r <= 0.0F) {					// 拿到位置					int position = getShowItem(getTouchedPointAngle(r, r, x1,							y1));					// 旋转到指定位置					setShowItem(position, isAnimEnabled());				}				break;			}		}		return super.onTouchEvent(event);	}	/**	 * @Title: getTouchedPointAngle	 * @Description: 计算触摸角度	 * @param radiusX	 *            圆心	 * @param radiusY	 *            圆心	 * @param x1	 *            触摸点	 * @param y1	 *            触摸点	 * @return	 * @throws	 */	private float getTouchedPointAngle(float radiusX, float radiusY, float x1,			float y1) {		float differentX = x1 - radiusX;		float differentY = y1 - radiusY;		double a = 0.0D;		double t = differentY				/ Math.sqrt(differentX * differentX + differentY * differentY);		if (differentX > 0.0F) {			// 0~90			if (differentY > 0.0F)				a = 6.283185307179586D - Math.asin(t);			else				// 270~360				a = -Math.asin(t);		} else if (differentY > 0.0F)			// 90~180			a = 3.141592653589793D + Math.asin(t);		else {			// 180~270			a = 3.141592653589793D + Math.asin(t);		}		return (float) (360.0D - a * 180.0D / 3.141592653589793D % 360.0D);	}	/**	 * @Title: getShowItem	 * @Description: 拿到触摸位置	 * @param touchAngle	 *            触摸位置角度	 * @return	 * @throws	 */	private int getShowItem(float touchAngle) {		int position = 0;		for (int i = 0; i < itemsStartAngle.length; i++) {			if (i != itemsStartAngle.length - 1) {				if ((touchAngle >= itemsStartAngle[i])						&& (touchAngle < itemsStartAngle[(i + 1)])) {					position = i;					break;				}			} else if ((touchAngle > itemsStartAngle[(itemsStartAngle.length - 1)])					&& (touchAngle < itemsStartAngle[0])) {				position = itemsValues.length - 1;			} else {				// 如果触摸位置不对,则旋转到最大值得位置				position = getPointItem(itemsStartAngle);			}		}		return position;	}	private int getPointItem(float[] startAngle) {		int item = 0;		float temp = startAngle[0];		for (int i = 0; i < startAngle.length - 1; i++) {			if (startAngle[(i + 1)] - temp > 0.0F)				temp = startAngle[i];			else {				return i;			}		}		return item;	}	protected void onDetachedFromWindow() {		super.onDetachedFromWindow();		piegraphHandler.removeCallbacks(this);	}	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {		super.onMeasure(widthMeasureSpec, heightMeasureSpec);		float widthHeight = 2.0F * (radius + strokeWidth + 1.0F);		// 重设view的宽高		setMeasuredDimension((int) widthHeight, (int) widthHeight);	}	/**	 * 旋转动作	 */	public void run() {		if (isClockWise) {			// 顺时针旋转			rotateStartAng += moveSpeed;			invalidate();			piegraphHandler.postDelayed(this, 10L);			if (rotateStartAng - rotateEndAng >= 0.0F) {				rotateStartAng = 0.0F;				// 如果已经转到指定位置,则停止动画				piegraphHandler.removeCallbacks(this);				// 重设各模块起始角度值				resetStartAngle(rotateEndAng);				isRotating = false;			}		} else {			// 逆时针旋转			rotateStartAng -= moveSpeed;			invalidate();			piegraphHandler.postDelayed(this, 10L);			if (rotateStartAng - rotateEndAng <= 0.0F) {				rotateStartAng = 0.0F;				piegraphHandler.removeCallbacks(this);				resetStartAngle(rotateEndAng);				isRotating = false;			}		}	}	private float getAnimTime(float ang) {		return (int) Math.floor(ang / getmoveSpeed() * 10.0F);	}	/**	 * @Title: resetStartAngle	 * @Description: 重设个模块角度	 * @param angle	 * @throws	 */	private void resetStartAngle(float angle) {		for (int i = 0; i < itemsStartAngle.length; i++) {			float newStartAngle = itemsStartAngle[i] + angle;			if (newStartAngle < 0.0F)				itemsStartAngle[i] = (newStartAngle + 360.0F);			else if (newStartAngle > 360.0F)				itemsStartAngle[i] = (newStartAngle - 360.0F);			else				itemsStartAngle[i] = newStartAngle;		}	}	/**	 * @Title: setDefaultColor	 * @Description: 设置默认颜色	 * @throws	 */	private void setDefaultColor() {		if ((itemsValues != null) && (itemsValues.length > 0)				&& (itemColors == null)) {			itemColors = new String[itemsValues.length];			if (itemColors.length <= DEFAULT_ITEMS_COLORS.length) {				System.arraycopy(DEFAULT_ITEMS_COLORS, 0, itemColors, 0,						itemColors.length);			} else {				int multiple = itemColors.length / DEFAULT_ITEMS_COLORS.length;				int difference = itemColors.length						% DEFAULT_ITEMS_COLORS.length;				for (int a = 0; a < multiple; a++) {					System.arraycopy(DEFAULT_ITEMS_COLORS, 0, itemColors, a							* DEFAULT_ITEMS_COLORS.length,							DEFAULT_ITEMS_COLORS.length);				}				if (difference > 0)					System.arraycopy(DEFAULT_ITEMS_COLORS, 0, itemColors,							multiple * DEFAULT_ITEMS_COLORS.length, difference);			}		}	}	/**	 * @Title: setDifferentColor	 * @Description: 补差颜色	 * @throws	 */	private void setDifferentColor() {		if ((itemsValues != null) && (itemsValues.length > itemColors.length)) {			String[] preitemColors = new String[itemColors.length];			preitemColors = itemColors;			int leftall = itemsValues.length - itemColors.length;			itemColors = new String[itemsValues.length];			System.arraycopy(preitemColors, 0, itemColors, 0,					preitemColors.length);			if (leftall <= DEFAULT_ITEMS_COLORS.length) {				System.arraycopy(DEFAULT_ITEMS_COLORS, 0, itemColors,						preitemColors.length, leftall);			} else {				int multiple = leftall / DEFAULT_ITEMS_COLORS.length;				int left = leftall % DEFAULT_ITEMS_COLORS.length;				for (int a = 0; a < multiple; a++) {					System.arraycopy(DEFAULT_ITEMS_COLORS, 0, itemColors, a							* DEFAULT_ITEMS_COLORS.length,							DEFAULT_ITEMS_COLORS.length);				}				if (left > 0) {					System.arraycopy(DEFAULT_ITEMS_COLORS, 0, itemColors,							multiple * DEFAULT_ITEMS_COLORS.length, left);				}			}			preitemColors = null;		}	}	/**	 * @Title: reSetTotal	 * @Description: 重设总值	 * @throws	 */	private void reSetTotal() {		double totalSizes = getAllSizes();		if (getTotal() < totalSizes)			total = totalSizes;	}	private double getAllSizes() {		float tempAll = 0.0F;		if ((itemValuesTemp != null) && (itemValuesTemp.length > 0)) {			for (double itemsize : itemValuesTemp) {				tempAll += itemsize;			}		}		return tempAll;	}	public void setItemSelectedListener(			OnPiegraphItemSelectedListener itemSelectedListener) {		this.itemSelectedListener = itemSelectedListener;	}}

自定义View专题报表类的view到此就讲完了。博主没有写过自定义的折线图。但是学会了这两个图形的话再去自己写折线图我想也是不难的。

后续还有2期的自定义view的专题。一期是关于自定义gridView的(可以拖动gridView,但是不是和网上其他的那种拖动item,而是将item里面的内容拖动切换位置),一期是关于自定义viewGroup(类似线性布局,相对布局那种,可以往里面添加控件的)。希望能够帮助到看到此篇文章的人。









  相关解决方案