您的位置:首页 > 移动开发 > Android开发

Android PieChart 饼图控件

2016-07-01 10:54 351 查看
今天写一个饼图自定义View的文章。由于公司的项目需要用到饼图,UI给的设计图和自己找的一个饼图框架的标题位置不符,所以就自己画了一个。

请尊重作者劳动成果,转载请标明原文地址:
http://blog.csdn.net/u010053224/article/details/51799523

1,使用预览



PieChart mChart mChart = (PieChart) findViewById(R.id.pieChar);
mChart = (PieChart) findViewById(R.id.pieChar);
String[] titles = new String[] {"钱包余额","金钱袋资产","金宝箱资产"};
mChart.setTitles(titles);
int[] colors = new int[]{0xfff5a002,0xfffb5a2f,0xff36bc99};
mChart.setColors(colors);
mChart.setValues(new double[]{999,999,999});
mChart.postInvalidate();
需要传入标题和值,每个饼的颜色也可以重新设置,不传颜色值就用默认的。画饼图的难点就在数学计算和逻辑思维能力。

2代码分析

/**
* 获得每个值所占的角度
* @return
*/
private float[] getAngles(){
if(mValues == null || mValues.length == 0) return null;
double sum = 0;
int len = mTitles.length;
float[] angles = new float[len];
int gapCount = 0;//饼图间隙条数
for(int i=0;i<len;i++){
sum += mValues[i];
if(mValues[i]>0)
gapCount++;
}
float angle = 0;
pieAngle = 360 - gapCount*ANGLE_DIS;
for(int i=0;i<len-1;i++){
angles[i] = (float)(pieAngle*mValues[i]/sum);
angle += angles[i];
}
if(mValues[len-1]>0)
angles[len - 1] = pieAngle - angle;

return angles;
}
根据传过来的值计算每个值所占的角度,饼图之间有1°的间隙,
if(mValues[i]>0) gapCount++;只有值大于0时才画饼,才增加一条间隙。
pieAngle = 360 - gapCount*ANGLE_DIS;这里计算所有饼所占整个圆的角度。
if(mValues[len-1]>0) angles[len - 1] = pieAngle - angle;最后一个饼的角度为饼的总角度减去前面所有饼的角度之和,这样做是为了让所有饼的角度加起来等于pieAngle这个角度,减少除法运算的小数误差效果。

/**
* 在饼图的每个饼上写上百分比,必须放在计算了每个值所占的角度angles之后
* @param canvas
*/
private void setPieContentText(Canvas canvas){
float pre = 0;
float[] centerAngle = new float[mAngles.length];
for(int i=0;i<mAngles.length;i++){
if(mAngles[i] == 0) continue;
centerAngle[i] = ANGLE_DIS+mAngles[i]/2 + pre;
pre += mAngles[i];
}
float cenR = pieR*1.0f/2*3/5;
float lastPir = 1.0f;//为了使所有的%比加起来等于1
mTextPaint.setColor(0xFFFFFFFF);
float cenX = 0;
float cenY = 0;
for(int i=0;i<centerAngle.length;i++){
if(centerAngle[i]==0 ) continue;
float xa = (float) (cenR * Math.cos(centerAngle[i] * (Math.PI / 180)));
float ya = (float) (cenR * Math.sin(centerAngle[i] * (Math.PI / 180)));
cenX = getWidth()*1.0f/2+xa;
cenY = TOP_PADDING + pieR*1.0f/2 + ya;
mTextPaint.setTextSize(mPieTextSize);
double curPer = numDecimals(mAngles[i]/pieAngle);
String perMsg = i==centerAngle.length-1?percentFormat(lastPir):percentFormat(curPer);
canvas.drawText(perMsg, cenX-getTextWidth(perMsg, mPieTextSize)*1.0f/2, cenY+getTextHeight(perMsg, mPieTextSize)*1.0f/2/2, mTextPaint);
lastPir -= curPer;
}
}


这段代码是计算百分比文字在圆内的角度,centerAngle[i] = ANGLE_DIS+mAngles[i]/2 + pre;百分比文字在每个饼的中心。
float cenR = pieR*1.0f/2*3/5;计算百分比文字中心到圆心的距离,可根据需要做调整,pieR为饼图直径。



如上图所示,float xa = (float) (cenR * Math.cos(centerAngle[i] * (Math.PI / 180)));float ya = (float) (cenR * Math.sin(centerAngle[i] * (Math.PI / 180)));中xa和ya为文字到圆心的水平垂直距离。cenX = getWidth()*1.0f/2+xa; cenY = TOP_PADDING + pieR*1.0f/2 + ya;cenX 和cenY为文字在view上面的坐标点。



3,自定义饼图源码

import java.text.DecimalFormat;
import java.util.List;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import cn.golditfin.component.amount.AmountUtils;

/**
* 圆形饼图控件,title为多少条就显示多少条,无视value
* @author HuangYuGuang
* Create on 2015年12月18日
* File Name PieChart.java
*/
public class PieChart extends View{
private final float density = getResources().getDisplayMetrics().density;
/**饼图之间间隔的角度*/
private final float ANGLE_DIS = 1;
/**左右两边空间距离*/
private final float LR_PADDING = 25 * density;
/**顶部空间距离*/
private final float TOP_PADDING = 25 * density;
/**饼图和下边文字的距离*/
private final float PIE_TEXT_DIS = 22 * density;
/**上下行文字的距离*/
private final float TEXT_TEXT_DIS = 15 * density;
/**底部和文字的距离,实际为BOTTOM_DIS+TEXT_TEXT_DIS*/
private final float BOTTOM_DIS = 10 * density;
/**标题和值的最小距离*/
private final float TITLE_VALUE_DIS = 18 * density;

/**每部分的颜色值,默认有10个颜色 */
private int[] mColors = new int[]{0xfff5a002,0xfffb5a2f,0xff36bc99,0xff43F90C,0xff181A18,0xffF802F6
,0xff022DF8,0xffECF802,0xff02F8E8,0xffEA0F8E};
/**值*/
private double[] mValues;
/**值转换成角度*/
private float[] mAngles;
/**饼图直径*/
private float pieR;
/**饼图所占总的角度*/
private float pieAngle;

private String[] mTitles ; //每部分的内容
private String mEmptyMsg = "暂无数据"; //无数据提示的内容

private float mTitleSize;
private float mValueSize;
/**饼图里面文字的大小*/
private float mPieTextSize;

private int mTitleColor = 0xFF595959;
private int mValueColor = 0xFF595959;
private int mDefaultPointColor = 0xfff5a002; //无数据时提示文字的颜色

private Rect mTextBound;
private Paint mTextPaint;
private Paint mPiePaint;

public PieChart(Context context) {
this(context,null);
}

public PieChart(Context context, AttributeSet attrs) {
this(context, attrs,0);
}

public PieChart(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mTitleSize = sp2px(14);
mPieTextSize = sp2px(12);
mValueSize = sp2px(16);

mPiePaint = new Paint();
mTextPaint = new Paint();
mTextBound = new Rect();
mTextPaint.setColor(0xff595959);
mTextPaint.setTextSize(mTitleSize);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPiePaint.setAntiAlias(true);
mTextPaint.setAntiAlias(true);
/**------------------------------无数据--------------------------------*/
if(mTitles == null || mTitles.length == 0 || isValueEmpty()){
mPiePaint.setColor(mDefaultPointColor);
mTextPaint.setColor(0xffffffff);
float cr = (getWidth()<getHeight()?getWidth()/2:getHeight()/2) - LR_PADDING;
canvas.drawCircle(getWidth()/2, getHeight()/2, cr, mPiePaint);
mTextPaint.getTextBounds(mEmptyMsg, 0, mEmptyMsg.length(), mTextBound);
canvas.drawText(mEmptyMsg, (getWidth()-mTextBound.width())/2, (getHeight()+mTextBound.height())/2, mTextPaint);
return;
}
/**------------------------------无数据--------------------------------*/

/**------------------------------画饼图--------------------------------*/
int textHeight = getTextHeight("00", Math.max(mTitleSize, mValueSize));
float r1 = getWidth() - LR_PADDING*2;
float r2 = getHeight() - TOP_PADDING - PIE_TEXT_DIS - (TEXT_TEXT_DIS+textHeight)*(mTitles.length) - BOTTOM_DIS;
pieR = Math.min(r1, r2);//为了防止饼图越界,饼图直径选取最小值
RectF oval = new RectF((getWidth()-pieR)/2, TOP_PADDING, (getWidth()+pieR)/2, TOP_PADDING+pieR);
mPiePaint.setStyle(Paint.Style.FILL);
mAngles = getAngles();

float startAngle = 0;
for(int i=0;i<mAngles.length;i++){
mPiePaint.setColor(mColors[i]);
if(mAngles[i] == 0) continue;
canvas.drawArc(oval, startAngle, mAngles[i], true, mPiePaint);
startAngle += (mAngles[i]+ANGLE_DIS);
}
/**------------------------------画饼图--------------------------------*/

/**------------------------------最下面的文字,title和value--------------------------------*/
float cr = 5 * density; //圆点半径
float ctd = 8 * density; //圆点和右边文字距离
float titleLen = getMaxTextWidth(mTitles, mTitleSize) + TITLE_VALUE_DIS;
float len = 2*cr + ctd + titleLen + getMaxTextWidth(mValues, mValueSize);
float topDis = TOP_PADDING + PIE_TEXT_DIS +pieR + textHeight; //第一行文字底部和控件顶部距离
float cX = (getWidth()-len)/2+cr;
float titleX = cX+cr+ctd;
float valueX = titleX + titleLen;
int valueLen = mValues.length-1;
for(int i=0;i<mTitles.length;i++){
mPiePaint.setColor(mColors[i]);
float yDis = topDis+(textHeight+TEXT_TEXT_DIS)*i;
canvas.drawCircle(cX, yDis-textHeight/2, cr, mPiePaint);
mTextPaint.setTextSize(mTitleSize);
mTextPaint.setColor(mTitleColor);
canvas.drawText(mTitles[i], titleX, yDis, mTextPaint);
mTextPaint.setColor(mValueColor);
mTextPaint.setTextSize(mValueSize);
canvas.drawText(AmountUtils.moneyFormat(i>valueLen?0:mValues[i]), valueX, yDis, mTextPaint);
}
/**------------------------------最下面的文字,title和value--------------------------------*/

//饼图上的文字
setPieContentText(canvas);
}

/**
* 设置每个饼图代表的名字
* @param titles
*/
public void setTitles(List<String> titles){
mTitles = (String[])titles.toArray();
}

/**
* 设置每个饼图代表的名字
* @author HuangYuGuang
* Create on 2015年12月30日
* @param titles
*/
public void setTitles(String[] titles){
mTitles = titles;
}

/**
* 设置值
* @param values
*/
public void setValues(List<Double> values){
mValues = new double[values.size()];
for(int i=0;i<values.size();i++){
mValues[i] = values.get(i);
}
}

/**
* 设置值
* @param values
*/
public void setValues(double[] values){
mValues = values;
}

/**
* 设置每块饼图的颜色
* @param colors
*/
public void setColors(List<Integer> colors){
mColors = new int[colors.size()];
for(int i=0;i<colors.size();i++){
mColors[i] = colors.get(i);
}
}

/**
* 设置每块饼图的颜色
* @param colors
*/
public void setColors(int[] colors){
mColors = colors;
}

/**
* 设置名字的字体大小
* @param size
*/
public void setTitleSize(float size){
mTitleSize = sp2px(size);
}

/**
* 设置数值的大小
* @param size
*/
public void setValueSize(float size){
mValueSize = sp2px(size);
}

/**
* 设置饼图上文字的大小
* @param size
*/
public void setPieTextSize(float size){
mPieTextSize = sp2px(size);
}

/**
* 名字的颜色
* @param titleColor
*/
public void setTitleColor(int titleColor){
mTitleColor = titleColor;
}

/**
* 数值的颜色
* @param valueColor
*/
public void setValueColor(int valueColor){
mValueColor = valueColor;
}

/**
* 设置无数据时提示文字的颜色
* Create on 2015年12月31日
* @param color
*/
public void setDefaultPointColor(int color){
mDefaultPointColor = color;
}
/**
* 无数据的提示内容
* @param msg
*/
public void setEmptyMsg(String msg){
mEmptyMsg = msg;
}

private boolean isValueEmpty(){
if(mValues == null || mValues.length == 0)
return true;
for(double va:mValues){
if(va > 0)
return false;
}
mEmptyMsg = "暂无数据";
return true;
}

/** * 获得每个值所占的角度 * @return */ private float[] getAngles(){ if(mValues == null || mValues.length == 0) return null; double sum = 0; int len = mTitles.length; float[] angles = new float[len]; int gapCount = 0;//饼图间隙条数 for(int i=0;i<len;i++){ sum += mValues[i]; if(mValues[i]>0) gapCount++; } float angle = 0; pieAngle = 360 - gapCount*ANGLE_DIS; for(int i=0;i<len-1;i++){ angles[i] = (float)(pieAngle*mValues[i]/sum); angle += angles[i]; } if(mValues[len-1]>0) angles[len - 1] = pieAngle - angle; return angles; }

/**
* 在饼图的每个饼上写上百分比,必须放在计算了每个值所占的角度angles之后
* @param canvas
*/
private void setPieContentText(Canvas canvas){
float pre = 0;
float[] centerAngle = new float[mAngles.length];
for(int i=0;i<mAngles.length;i++){
if(mAngles[i] == 0) continue;
centerAngle[i] = ANGLE_DIS+mAngles[i]/2 + pre;
pre += mAngles[i];
}
float cenR = pieR*1.0f/2*3/5;
float lastPir = 1.0f;//为了使所有的%比加起来等于1
mTextPaint.setColor(0xFFFFFFFF);
float cenX = 0;
float cenY = 0;
for(int i=0;i<centerAngle.length;i++){
if(centerAngle[i]==0 ) continue;
float xa = (float) (cenR * Math.cos(centerAngle[i] * (Math.PI / 180)));
float ya = (float) (cenR * Math.sin(centerAngle[i] * (Math.PI / 180)));
cenX = getWidth()*1.0f/2+xa;
cenY = TOP_PADDING + pieR*1.0f/2 + ya;
mTextPaint.setTextSize(mPieTextSize);
double curPer = numDecimals(mAngles[i]/pieAngle);
String perMsg = i==centerAngle.length-1?percentFormat(lastPir):percentFormat(curPer);
canvas.drawText(perMsg, cenX-getTextWidth(perMsg, mPieTextSize)*1.0f/2, cenY+getTextHeight(perMsg, mPieTextSize)*1.0f/2/2, mTextPaint);
lastPir -= curPer;
}
}

private int getMaxTextWidth(double[] ds,float size){
String[] strs = new String[ds.length];
for(int i=0;i<ds.length;i++){
strs[i] = AmountUtils.moneyFormat(ds[i]);
}
return getMaxTextWidth(strs, size);
}

private int getMaxTextWidth(String[] strs,float size){
Rect textBound = new Rect();
Paint paint = new Paint();
paint.setTextSize(size);
int len = 0;
for(int i=0;i<strs.length;i++){
paint.getTextBounds(strs[i], 0, strs[i].length(), textBound);
len = Math.max(len, textBound.width());
}
return len;
}

private int getTextWidth(String str,float size){
return getTextRect(str, size).width();
}

private int getTextHeight(String str,float size){
return getTextRect(str, size).height();
}

private Rect getTextRect(String str,float size){
Rect textBound = new Rect();
Paint paint = new Paint();
paint.setTextSize(size);
paint.getTextBounds(str, 0, str.length(), textBound);
return textBound;
}

private float sp2px(float sp){
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics());
}

/**
* 格式化为百分比格式
* @param num
* @return
*/
private String percentFormat(double num){
DecimalFormat df = new DecimalFormat("#0.00%");
return df.format(num);
}

/**
* 保留四位小数
* @param num
* @return
*/
public static double numDecimals(double num){
return ((int)(num*10000))*1.0d/10000;
}
}


饼图控件下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息