前言:
最近需要项目中需要使用到饼状图呈现用户的账户资金相关信息,于是顺手写了一个。
效果图:
主要思路:
环形饼状图实际上是多个扇形拼接成的,最后在中间覆盖白色圆,达到一个空心的效果。区块之间的间隔则是固定的一个角度偏移值。
涉及的主要类:
PIEChatView.class 该类继承自View,为控件的主体类
PIEChatUtils.class 自定义工具类,主要是浮点数加减乘除的封装,保留小数位数,确保计算时不出异常
PIEItem.class 数据实体类,控件使用的是ArrayList
DeviceUtil.class 自定义工具类,获取屏幕相关信息,例如屏幕宽高
StringUtil.class 自定义工具类,做数字金额的转换,例如‘2.0’转换成‘2.00元’
MainActivity.class 程序入口,用于测试控件
相关类详细介绍:
PIEChatView.class全部代码:
package cn.com.cg.piechatview;
import java.util.ArrayList;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.os.Handler;
import android.graphics.Paint.Style;
import android.util.AttributeSet;
import android.view.View;
import cn.com.core.util.DeviceUtil;
import cn.com.core.util.StringUtil;
/**
* 饼状图控件,包括相关文字描述信息
* 主体概括:将圆形饼状图放置在屏幕右侧,文字描述放在左侧,同时限制文字描述的长度,防止文字超长导致遮盖现象
* 设计思路:
* 1.环形饼状图的绘制其实是多个扇形的拼接过程,中间的白色圆实际上是最后覆盖上去的,这里实际上只需要计算扇形的起始角度和便宜角度即可
* 2.左边的描述无非时在指定的坐标上绘制圆形或者文字
* 控件说明:
* 1.展示多种类型的资金,根据资金占比的不同,在饼状图上呈现不同的区块
* 2.最多限制五种类型,当大于五种类型时,会将第五种及后续的类型合并为一种类型,取名“其他”,并将这些类型的金额一起合并为“其他”类型的总金额
* 3.当前饼状图做了一个渲染的动态效果,即:将任意区块分成50等份,通过handler的延时达到动态绘制的效果
* 4.当前饼状图要求传入的数据类型为一个ArrayList
* 5.在xml布局文件引用该控件时,设置的width和height对应的是饼状图的半径,例如设置为50dp,如果设置为match_parent或者wrap_content,则默认直径为屏幕宽度一半减去左右间距
* @author chenguo
*
*/
public class PIEChatView extends View {
private Context context;//上下文对象
private String xmlW;//xml布局中设定的饼状图宽度
private String xmlH;//xml布局中设定的饼状图高度
private int screenWidth;//屏幕宽度
private int screenHeight;//屏幕高度
private double outerR;//控件最外圆半径
private double innerR;//控件最内圆半径
private ArrayList
private ArrayList
private int itemCount;//item统计数
private static final int maxItem=5;//最多五个item,多出的item将全部合并到第五个item中
private ArrayList
private double itemMargin;//饼状图每个item之间的间距
private double siiMargin=10;//次内圆距离内圆的间距
private double csiMargin=10;//中圆距次内圆的间距
private double socMargin=10;//次外圆距离中圆的间距
private double osoMargin=100;//外圆距离次外圆的间距
private double tbSpace=60;//控件的上下左右内间距
private double textMargin=60;//描述的字的行间距
private Paint paint;//画笔对象
private int scaleType=6;//所有计算保留的小数位数,这里保留6为小数,达到比较精确的值
protected int number=1;//渲染次数,做一个延时效果
protected boolean isFinish;//渲染是否完成,是否渲染到第最后一个item
private RectF outerRectF;//外圆矩阵
private RectF souterRectF;//次外圆矩阵
private RectF centerRectF;//中圆矩阵
private RectF sinnerRectF;//次内圆矩阵
private RectF innerRectF;//内圆矩阵
private float textSize=30;//描述字体大小
private float textDefualtSize=15;//默认大小
private Handler handler=new Handler() {
@SuppressLint("HandlerLeak")
public void handleMessage(android.os.Message msg) {
if (number < 50) {
number++;
invalidate();
handler.sendEmptyMessageDelayed(1, 100);
} else {
isFinish = true;
invalidate();
// number=1;
}
};
};
public PIEChatView(Context context, AttributeSet attrs) {
this(context, attrs , 0);
// TODO Auto-generated constructor stub
}
public PIEChatView(Context context) {
this(context,null);
// TODO Auto-generated constructor stub
}
@SuppressWarnings("unused")
public PIEChatView(Context context,AttributeSet attrs,int def){
super(context,attrs,def);
this.context=context;
//设置控件半径值,根据屏幕分辨率大小设置
for(int i =0 ;i < attrs.getAttributeCount();i++){
if("layout_height".equals(attrs.getAttributeName(i))){
xmlW = attrs.getAttributeValue(i);
}else if("layout_width".equals(attrs.getAttributeName(i))){
xmlH = attrs.getAttributeValue(i);
}
}
toSetR();
}
private void toSetR() {
// TODO Auto-generated method stub
screenWidth=DeviceUtil.getMetricsWidth(context);//屏幕宽度
screenWidth=(int)(screenWidth-tbSpace);
screenHeight=DeviceUtil.getMetricsHeight(context);//屏幕高度
initR();
outerRectF = new RectF((float)(screenWidth-2*tbSpace-2*outerR),(float)tbSpace,(float)(screenWidth-tbSpace),(float)(outerR*2+2*tbSpace));
souterRectF =new RectF((float)(screenWidth-2*tbSpace-2*outerR+osoMargin),(float)(tbSpace+osoMargin),(float)(screenWidth-tbSpace-osoMargin),(float)(outerR*2+2*tbSpace-osoMargin));
centerRectF = new RectF((float)(screenWidth-2*tbSpace-2*outerR+osoMargin+socMargin),(float)(tbSpace+osoMargin+socMargin),(float)(screenWidth-tbSpace-osoMargin-socMargin),(float)(outerR*2+2*tbSpace-osoMargin-socMargin));
sinnerRectF=new RectF((float)(screenWidth-2*tbSpace-2*outerR+osoMargin+socMargin+csiMargin),(float)(tbSpace+osoMargin+socMargin+csiMargin),(float)(screenWidth-tbSpace-osoMargin-socMargin-csiMargin),(float)(outerR*2+2*tbSpace-osoMargin-socMargin-csiMargin));
innerRectF = new RectF((float)(screenWidth-2*tbSpace-2*outerR+osoMargin+socMargin+csiMargin+siiMargin),(float)(tbSpace+osoMargin+socMargin+csiMargin+siiMargin),(float)(screenWidth-tbSpace-osoMargin-socMargin-csiMargin-siiMargin),(float)(outerR*2+2*tbSpace-osoMargin-socMargin-csiMargin-siiMargin));
}
private void initR() {
// TODO Auto-generated method stub
float width=0;
float height=0;
try {
if ("-1".equals(xmlW)||"-2".equals(xmlW)||"-1".equals(xmlH)||"-2".equals(xmlH)){//宽度被设定为MachParent或者wrapcontent或者高度被设定为MachParent或者wrapcontent
outerR=DeviceUtil.getMetricsWidth(context)/2-2*tbSpace;
}else {
width=Float.valueOf(xmlW.substring(0,xmlW.length()-3)).floatValue();
height=Float.valueOf(xmlH.substring(0,xmlH.length()-3)).floatValue();
}
} catch (Exception e) {
// TODO: handle exception
outerR=350;
}
if (width==height&&width==0) {
}else if (width>=height&&width!=0) {
width=DeviceUtil.dip2px(context, height);
outerR=width;
}else if (height>width) {
width=DeviceUtil.dip2px(context, width);
outerR=width;
}
innerR=outerR-osoMargin-socMargin-csiMargin-siiMargin;
if (innerR<0) {
innerR=0;
}
initArrR();
}
/**
* 初始化各个半径,内圆,次内圆,中圆,次外圆,外圆,目前初始化五个半径
*/
private void initArrR() {
// TODO Auto-generated method stub
arrR=new ArrayList
arrR.add(innerR);
arrR.add(innerR+siiMargin);
arrR.add(innerR+siiMargin+csiMargin);
arrR.add(innerR+siiMargin+csiMargin+socMargin);
arrR.add(innerR+siiMargin+csiMargin+socMargin+osoMargin);
itemMargin=(arrR.get(1)-arrR.get(0))/2;
}
public void initPIEChatView(ArrayList
if (numArr!=null) {
valueArr=getValueArr(numArr);//得到角度的集合
}else {
itemCount=0;
}
//重新绘制饼状图
if (valueArr.size()!=0&&valueArr.size()-1!=0) {
textMargin=PIEChatUtils.div(arrR.get(arrR.size()-1)*2,valueArr.size()-1,4);
}
invalidate();
}
private ArrayList
// TODO Auto-generated method stub
double count = 0;
if (numArr.size()>maxItem) {
//将numArr集合的第五个及后边的item合并成一个item
numArr=mergedItem(numArr);
}
itemArr=numArr;
itemCount=numArr.size();
for (int i = 0; i < numArr.size(); i++) {
count+=numArr.get(i).getValue();
}
if (count==0) {
PIEItem pie = new PIEItem();
pie.setName("暂无数据");
pie.setValue(1);
numArr.add(pie);
}
//最终item的数值对应的圆环的占比
double angleScale=360-itemCount*itemMargin;
double perLength=PIEChatUtils.div(angleScale,count,scaleType);//item的每个单位所占角度,保留6为小数,达到比较精确位数
ArrayList
for (int j = 0; j < numArr.size(); j++) {
arr.add(PIEChatUtils.mul(numArr.get(j).getValue(),perLength));
}
return arr;
}
/**
* 将numArr的第五个item及其后边的item合并成第五个item
* @param numArr
* @return
*/
private ArrayList
// TODO Auto-generated method stub
double count=0;
for (int i = maxItem-1; i < numArr.size(); i++) {
count+=numArr.get(i).getValue();
}
PIEItem item=new PIEItem();
item.setName("其他");
item.setValue(count);
numArr.set(maxItem-1, item);
ArrayList
for (int i = 0; i < maxItem; i++) {
arr.add(numArr.get(i));
}
return arr;
}
// 获取笔
private Paint getPaint() {
if (paint == null) {
paint = new Paint();
}
return paint;
}
// 修改笔的颜色
private Paint getShadeColorPaint() {
Shader mShader = new LinearGradient(300, 50, 300, 400,
new int[] { Color.parseColor("#55FF7A00"), Color.TRANSPARENT }//通过调整该颜色,从而调整折现到X坐标直接的阴影
, null, Shader.TileMode.CLAMP);
// 新建一个线性渐变,前两个参数是渐变开始的点坐标,第三四个参数是渐变结束的点的坐标。连接这2个点就拉出一条渐变线了,玩过PS的都懂。然后那个数组是渐变的颜色。下一个参数是渐变颜色的分布,如果为空,每个颜色就是均匀分布的。最后是模式,这里设置的是循环渐变
getPaint().setShader(mShader);
return getPaint();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension((int)(DeviceUtil.getMetricsWidth(context)),(int)(2*outerR+3*tbSpace));
}
@Override
protected void onLayout(boolean changed, int left, int top, int right,
int bottom) {
// TODO Auto-generated method stub
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
//初始化画笔
getPaint().reset();
getPaint().setAntiAlias(true);//抗锯齿效果
getPaint().setStyle(Style.FILL);
//绘制
if (valueArr!=null&&valueArr.size()>0) {//当有有效的数据时渲染
getPaint().setStrokeWidth(1);
float count=0;
//绘制饼状图
for(int i=0;i<valueArr.size();i++){
//绘制饼状图右边的描述
if (i==0) {
getPaint().setColor(Color.parseColor("#ff76bff0"));//黄色
}else if (i==1) {
getPaint().setColor(Color.parseColor("#ff3352cc"));//红色
}else if (i==2) {
getPaint().setColor(Color.parseColor("#fff46060"));//深蓝色
}else if (i==3) {
getPaint().setColor(Color.parseColor("#ffffd322"));//浅蓝色
}else {
getPaint().setColor(Color.parseColor("#ffa755dd"));//紫色
}
getPaint().setTextSize(textSize);
canvas.drawCircle((float)tbSpace, (float)(1.5*tbSpace+i*textMargin), (float)10, paint);
canvas.drawText(itemArr.get(i).getName().length()>6?itemArr.get(i).getName().substring(0,6)+"...":itemArr.get(i).getName(),(float)(2*tbSpace), (float)(1.5*tbSpace+PIEChatUtils.div(textSize, 3, 4)+i*textMargin), paint);
canvas.drawText(StringUtil.getAmountResult(itemArr.get(i).getValue()+""),(float)(2*tbSpace+180), (float)(1.5*tbSpace+PIEChatUtils.div(textSize, 3, 4)+i*textMargin), paint);
getPaint().setTextSize(textDefualtSize);
if (i==0) {
canvas.drawArc(outerRectF, 0,valueArr.get(i).floatValue()/50*number,true, paint);
getPaint().setColor(Color.WHITE);
canvas.drawArc(souterRectF, 0,valueArr.get(i).floatValue()/50*number,true, paint);
getPaint().setColor(Color.RED);
canvas.drawArc(centerRectF, 0,valueArr.get(i).floatValue()/50*number,true, paint);
getPaint().setColor(Color.parseColor("#ff6699cc"));
canvas.drawArc(sinnerRectF, 0,valueArr.get(i).floatValue()/50*number,true, paint);
getPaint().setColor(Color.parseColor("#ffccffcc"));
canvas.drawArc(innerRectF, 0,valueArr.get(i).floatValue()/50*number,true, paint);
getPaint().setColor(Color.WHITE);
canvas.drawArc(souterRectF, 0,360,true, paint);
}else if (i==valueArr.size()-1) {
canvas.drawArc(outerRectF, count,valueArr.get(i).floatValue()/50*number,true, paint);
getPaint().setColor(Color.WHITE);
canvas.drawArc(souterRectF, count-(float)itemMargin,(valueArr.get(i).floatValue()+(float)itemMargin*2)/50*number,true, paint);
getPaint().setColor(Color.RED);
canvas.drawArc(centerRectF, count-(float)itemMargin,(valueArr.get(i).floatValue()+(float)itemMargin*2)/50*number,true, paint);
getPaint().setColor(Color.parseColor("#ff6699cc"));
canvas.drawArc(sinnerRectF, count-(float)itemMargin,(valueArr.get(i).floatValue()+(float)itemMargin*2)/50*number,true, paint);
getPaint().setColor(Color.parseColor("#ffccffcc"));
canvas.drawArc(innerRectF, count-(float)itemMargin,(valueArr.get(i).floatValue()+(float)itemMargin*2)/50*number,true, paint);
//isFinish=true;
getPaint().setColor(Color.WHITE);
canvas.drawArc(souterRectF, 0,360,true, paint);
}else{
canvas.drawArc(outerRectF, count,valueArr.get(i).floatValue()/50*number,true, paint);
getPaint().setColor(Color.WHITE);
canvas.drawArc(souterRectF, count-(float)itemMargin,(valueArr.get(i).floatValue()+(float)itemMargin)/50*number,true, paint);
getPaint().setColor(Color.RED);
canvas.drawArc(centerRectF, count-(float)itemMargin,(valueArr.get(i).floatValue()+(float)itemMargin)/50*number,true, paint);
getPaint().setColor(Color.parseColor("#ff6699cc"));
canvas.drawArc(sinnerRectF, count-(float)itemMargin,(valueArr.get(i).floatValue()+(float)itemMargin)/50*number,true, paint);
getPaint().setColor(Color.parseColor("#ffccffcc"));
canvas.drawArc(innerRectF, count-(float)itemMargin,(valueArr.get(i).floatValue()+(float)itemMargin)/50*number,true, paint);
getPaint().setColor(Color.WHITE);
canvas.drawArc(souterRectF, 0,360,true, paint);
}
count+=valueArr.get(i).floatValue()+itemMargin;
}
//渲染到了最后一个item
if (isFinish) {
getPaint().setColor(Color.WHITE);
canvas.drawArc(souterRectF, 0,360,true, paint);
}else {
handler.sendEmptyMessage(1);
}
}
}
}
PIEChatUtils.class全部代码:
package cn.com.cg.piechatview;
import android.annotation.SuppressLint;
import android.content.Context;
import android.icu.math.BigDecimal;
@SuppressLint("NewApi")
public class PIEChatUtils {
//两个Double数相加
public static Double add(Double v1,Double v2){
BigDecimal b1 = new BigDecimal(v1.toString());
BigDecimal b2 = new BigDecimal(v2.toString());
return b1.add(b2).doubleValue();}
public static Double sub(Double v1,Double v2){
BigDecimal b1 = new BigDecimal(v1.toString());
BigDecimal b2 = new BigDecimal(v2.toString());
return b1.subtract(b2).doubleValue();
}
public static Double mul(Double v1,Double v2){
BigDecimal b1 = new BigDecimal(v1.toString());
BigDecimal b2 = new BigDecimal(v2.toString());
return b1.multiply(b2).doubleValue();
}
public static Double div(Double v1,Double v2,int scale){
BigDecimal b1 = new BigDecimal(v1.toString());
BigDecimal b2 = new BigDecimal(v2.toString());
return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();}
/**
* 保留两位小数
* @return
*/
public static String getTwoDecimal(double d){
// DecimalFormat df = new DecimalFormat("#.00");
// df.format(d);
// return df.format(d);
return String.format("%.2f", d);
}
/**
* 保留六位小数
* @return
*/
public static String getSixDecimal(double d){
// DecimalFormat df = new DecimalFormat("#.00");
// df.format(d);
// return df.format(d);
return String.format("%.6f", d);
}
/**
* 保留三位小数
* @return
*/
public static String getThreeDecimal(double d){
// DecimalFormat df = new DecimalFormat("#.00");
// df.format(d);
// return df.format(d);
return String.format("%.3f", d);
}
public static double div(double a1, double b1, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("error");
}
BigDecimal a2 = new BigDecimal(Double.toString(a1));
BigDecimal b2 = new BigDecimal(Double.toString(b1));
return a2.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
PIEItem.class全部代码:
package cn.com.cg.piechatview;
/**
* 饼状图每个item的数值以及其名称
* @author chenguo
*
*/
public class PIEItem {
private double value;
private String name;
public double getValue() {
return value;
}
public void setValue(double value) {
this.value = value;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
DeviceUtil.class 部分方法:
/**
* @brief 获取手机屏幕尺寸 高度
* @param context
* 上下文
* @return int
*/
public static int getMetricsHeight(Context context) {
// String str = "";
DisplayMetrics dm = new DisplayMetrics();
dm = context.getResources().getDisplayMetrics();
int screenHeight = dm.heightPixels;// 屏幕高(像素,如:800px)
return screenHeight;
}
/**
* @brief 获取手机屏幕尺寸 宽度
* @param context
* 上下文
* @return int
*/
public static int getMetricsWidth(Context context) {
// String str = "";
DisplayMetrics dm = new DisplayMetrics();
dm = context.getResources().getDisplayMetrics();
int screenWidth = dm.widthPixels;// 屏幕高(像素,如:800px)
return screenWidth;
}
StringUtil.class部分方法:
// 返回小数点金额显示方法
public static String getAmountResult(String aString) {
aString = getdeleYuan(aString);
if (TextUtils.isEmpty(aString)) {
return "-";
}
if (".".equals(String.valueOf(aString.charAt(0)))) {
aString = "0" + aString;
}
if (!aString.contains(".")) {
aString += ".00";
}
if (".".equals(aString.subSequence(aString.length() - 2,
aString.length() - 1))) {
aString += "0";
}
aString = addComma(aString) + "元";
//aString = addComma(aString);
String str[] = aString.split("\\,");
if (str.length > 1) {
if ("-".equals(str[0])) {
String strr = "-";
for (int i = 1; i < str.length; i++) {
if (i == 1) {
strr += str[i];
} else {
strr += ",";
strr += str[i];
}
}
return strr;
} else {
return aString;
}
} else {
return aString;
}
}
MainActivity.class中使用控件:
//在拿到数据后调用该方法初始化控件,这里使用的是测试数据
private void initViews() {
// TODO Auto-generated method stub
pie_view=(PIEChatView)findViewById(R.id.pie_view);
numArr=getArr();
pie_view.initPIEChatView(numArr);
}
private ArrayList
// TODO Auto-generated method stub
ArrayList
PIEItem i1=new PIEItem();
i1.setName("账户余额");
i1.setValue(175);
PIEItem i2=new PIEItem();
i2.setName("聚能赚");
i2.setValue(130);
PIEItem i3=new PIEItem();
i3.setName("富利快线");
i3.setValue(101);
PIEItem i4=new PIEItem();
i4.setName("理点财");
i4.setValue(79);
PIEItem i5=new PIEItem();
i5.setName("总资产");
i5.setValue(13);
PIEItem i6=new PIEItem();
i6.setName("总资产");
i6.setValue(79);
PIEItem i7=new PIEItem();
i7.setName("总资产");
i7.setValue(80);
pieArr.add(i1);
pieArr.add(i2);
pieArr.add(i3);
pieArr.add(i4);
pieArr.add(i5);
pieArr.add(i6);
pieArr.add(i7);
return pieArr;
}
总结:该控件其实没有什么难度可言,就是一个角度的计算和绘制过程,其过程中还包括一些对数据的拆分、转换,细心一点就没啥大问题。