首页 > 代码库 > Android开发之自定义View专题(二):自定义饼图

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 (x_freedom_reddevil@126.com)
 * @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(类似线性布局,相对布局那种,可以往里面添加控件的)。希望能够帮助到看到此篇文章的人。









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