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

来源:互联网 发布:大型数据机房制冷系统 编辑:程序博客网 时间:2024/06/06 12:58

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


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

下载地址: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)// 默认半径设置为100this.radius = 100.0F;else {this.radius = radius;}// 默认圆环宽度设置为2if (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~90if (differentY > 0.0F)a = 6.283185307179586D - Math.asin(t);else// 270~360a = -Math.asin(t);} else if (differentY > 0.0F)// 90~180a = 3.141592653589793D + Math.asin(t);else {// 180~270a = 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);elseitemsStartAngle[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(类似线性布局,相对布局那种,可以往里面添加控件的)。希望能够帮助到看到此篇文章的人。









0 0
原创粉丝点击