在图表里面,常用的图标一般为折线图、柱形图和饼图,上周,博主已经将柱形图分享。在博主的项目里面其实还用到了饼图,但没用到折线图。其实学会了其中一个,再去写其他的,应该都是知道该怎么写的,原理都是自己绘制图形,然后获取触摸位置判定点击事件。好了,废话不多说,直接上今天的饼图的效果图
这次也是博主从项目里面抽离出来的,这次的代码注释会比上次的柱形图更加的详细,更加便于有兴趣的朋友一起学习。图中的那个圆形指向箭头不属于饼图的部分,是在布局文件中为了美化另外添加进去的,有兴趣的朋友可以下载完整的项目下来研究学习。
下载地址: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(类似线性布局,相对布局那种,可以往里面添加控件的)。希望能够帮助到看到此篇文章的人。