上周接到产品经理的需求,要求做一个饼图,以作为会员的数据统计,原型如下图:
原型看起来不算复杂,但是作为多年开发经验的我,还是有点懒的,于是乎就去GitHub上找相关项目,找到了几个项目,但做的饼图跟我们产品经理做的原型图还是有一定的差别的。按理说改吧改吧就好了,只是去年我帮一个朋友做了一个折线图(纯自己自定义控件),该项目demo已经上传到GitHub,地址https://github.com/huangxuanheng/brokenLine,觉得很多点和比例都需要计算,如果想要修改一点,就得弄清楚里面的点线坐标以及计算比例关系,很麻烦。与其去修改别人做好的饼图,再弄懂一些计算关系,还不如自己写一个自定义控件的饼图,这样所有的关系都可以清晰明了的弄清楚,并且以后想改动什么地方,都会非常的方便
好的,开始玩转自定义饼图控件
先来分析原型图
1.首先,这个饼图是由两个不同半径的同心圆组成,我们可以定义一个坐标轴,以cx,cy为两同心圆的圆心,radius为大圆半径,sRadius为小圆半径,绘制两个同心圆
2.数据部分的体现,是大圆-小圆的部分,而数据块,即数据所占百分比部分,是以为cx,cy圆心,从一个起点startAngle角度开始,以sweepAngle为角速度旋转的扇形块,大圆扇形块-小圆扇形块=数据块
3.数据块对应数据显示,是以对应直线,以斜率k=1或者k=-1,与大圆相交为转折点,延伸一段距离d后,取一个点,再以斜率k=0的直线相交,往圆心相反方向延伸,根据数据文本的长度来取该线段的长度l
图画的有点难看,能看就好
有了这些,就可以开始绘制了
步骤:
1.定义并初始化画笔
2.根据同心圆和相关圆半径等数据,计算出扇形的绘制位置以及开始绘制角度startAngle和角速度sweepAngle,绘制扇形
3.通过扇形的startAngle和绘制折线角速度sweepAngle,计算出扇形的大圆中间点,绘制线段d、l,绘制文字说明
4.绘制同心圆
1.定义并初始化画笔
public class PieChartView extends View {
Paint bCiclePaint; //绘制大圆的画笔
Paint sCiclePaint; //绘制小圆的画笔
Paint ringPaint; //绘制环的画笔
Paint brokenLinePaint; //绘制折线的画笔
Paint textPaint; //绘制文字的画笔
float radius; //大圆半径
int cx; //圆心x
int cy; //圆心y
float total; //总量,需要分割成为饼图的所有数据总数
boolean isStartAngle; //是否指定开始绘制第一个饼图块的角度
float startAngle; //开始绘制第一个饼图块的角度
int maxAngle=360; //圆的最大角度
public PieChartView(Context context) {
this(context,null);
}
public PieChartView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public PieChartView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
}
private void initPaint() {
bCiclePaint =new Paint();
bCiclePaint.setAntiAlias(true);
bCiclePaint.setStyle(Paint.Style.STROKE);//设置空心
bCiclePaint.setColor(Color.GRAY);
sCiclePaint=new Paint();
sCiclePaint.setStyle(Paint.Style.FILL);//设置空心
sCiclePaint.setAntiAlias(true);
sCiclePaint.setColor(Color.WHITE);
ringPaint=new Paint();
ringPaint.setAntiAlias(true);
ringPaint.setStyle(Paint.Style.FILL);
brokenLinePaint=new Paint();
brokenLinePaint.setAntiAlias(true);
brokenLinePaint.setColor(Color.GRAY);
textPaint=new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(Color.GRAY);
textPaint.setTextSize(UIUtils.getDimens(R.dimen.sp13));
}
}
//bean
public class PieChart {
private int id;
private String name; //文字说明
private float proport; //文字说明所占比例
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getProport() {
return proport;
}
public void setProport(float proport) {
this.proport = proport;
}
@Override
public String toString() {
return "PieChart{" +
"id=" + id +
", name='" + name + '\'' +
", proport=" + proport +
'}';
}
//子类可以通过重写该方法,返回显示在饼图的数量说明文字
public String getProportStr(){
return ((int)proport)+"人";
}
}
2.根据同心圆和相关圆半径等数据,计算出扇形的绘制位置以及开始绘制角度startAngle和角速度sweepAngle,绘制扇形
3.通过扇形的startAngle和绘制折线角速度sweepAngle,计算出扇形的大圆中间点,绘制线段d、l,绘制文字说明
MappieChartMap=new HashMap<>();
private void init() {
radius=getWidth()/5;
cx =getWidth()/2;
// cy =getPaddingTop()+getWidth()/3;
cy =getPaddingTop()+getHeight()/3;
LogUtil.i(this,"cx="+cx+",cy="+cy);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
init();
drawArc(canvas);
}
//绘制扇形区域
private void drawArc(Canvas canvas) {
if(pieChartMap.isEmpty()){
return;
}
RectF oval1=new RectF(cx-radius,cy-radius,cx+radius,cy+radius);
startAngle = getStartAngle();
float ss=0; //无用,数据用来打印测试的
for(Map.Entryentry:pieChartMap.entrySet()){
PieChart value = entry.getValue();
float sweepAngles = value.getProport() * maxAngle / total;
ss+=sweepAngles;
canvas.drawArc(oval1, startAngle, sweepAngles, true, getRingPaint());//小弧形
LogUtil.i(this,"startAngle="+startAngle+",sweepAngles="+sweepAngles);
//绘制折线,写数据
drawBrokenLine(canvas,startAngle,sweepAngles,maxAngle,value);
startAngle+=sweepAngles;
}
LogUtil.i(this,"ss="+ss);
}
private void drawBrokenLine(Canvas canvas, float startAngle, float sweepAngles,float maxAngle,PieChart entry) {
int k=1; //与圆相交直线的斜率
float d=100; //与圆相交的折线长度
float l; //线段l的长度,与d相交的折线长度,上下显示文字
//扇形开始角度+角速度的二分之一,即是画折线的起点处
float Q= (float) ((startAngle+sweepAngles/2)*Math.PI*2/maxAngle);
float sin= (float) Math.sin(Q);
float cos= (float) Math.cos(Q);
if(sin>0&&cos>0){
//第一象限
k=1;
}else if(sin>0&&cos<0){
//第二象限
k=-1;
}else if(sin<0&&cos<0){
//第三象限
k=1;
}else if(sin<0&&cos>0){
//第四象限
k=-1;
}
float x1= radius*cos+cx;
float y1=radius*sin+cy;
LogUtil.i(this,"cos="+cos+"sin="+sin+",radius*cos="+(radius*cos)+",radius*sin="+(radius*sin)+",x1="+x1+",y1="+y1);
//y=kx+b
float b=y1-k*x1;
//两点间距离公式计算出一个点
//根号b的平方-4ac
float a= (float) (1+Math.pow(k, 2));
//相当于一元二次方法中的b
float c= k*b - x1 - k*y1;
float sqrt = (float) Math.sqrt(Math.pow(c, 2) - a*(Math.pow(x1, 2) + Math.pow(b, 2) + Math.pow(y1, 2) - 2 * b * y1 - Math.pow(d, 2)));
float x= (-c+sqrt)/a;
float lx=0; //线段l的端点
float ly; //线段l的端点
String name = entry.getName();
String proport = entry.getProportStr();
Rect rectName=new Rect();
//获取文字的长度
textPaint.getTextBounds(name,0,name.length(),rectName);
Rect rectProport=new Rect();
textPaint.getTextBounds(proport,0,proport.length(),rectProport);
int widthName = rectName.width();
int widthProport = rectProport.width();
l=Math.max(widthName,widthProport);
if(sin>0&&cos>0){
//第一象限
if(x//在圆内
x=(-c-sqrt)/a;
}
lx=x+l;
}else if(sin>0&&cos<0){
//第二象限
if(x>x1){
//在圆内
x=(-c-sqrt)/a;
}
lx=x-l;
}else if(sin<0&&cos<0){
//第三象限
if(x>x1){
//在圆内
x=(-c-sqrt)/a;
}
lx=x-l;
}else if(sin<0&&cos>0){
//第四象限
if(x//在圆内
x=(-c-sqrt)/a;
}
lx=x+l;
}
float y=k*x+b;
ly=y;
LogUtil.i(this,"x="+x+",y="+y);
drawBrokenLine(canvas, x1, y1, x, y, lx, ly);
drawText(canvas, l, x, y, lx, ly, name, proport, widthName, widthProport,rectProport.height());
}
/**
* 绘制折线,与圆相交,拐出来显示文字说明
* @param canvas
* @param x1 折线的端点x,与圆相交
* @param y1 折线的端点y,与圆相交
* @param x 折线d的端点x,与线段l相交的点
* @param y 折线d的端点y,与线段l相交的点
* @param lx 折线的端点x
* @param ly 折线的端点y
*/
private void drawBrokenLine(Canvas canvas, float x1, float y1, float x, float y, float lx, float ly) {
//绘制折线与圆相交
canvas.drawLine(x1,y1,x,y,brokenLinePaint);
//绘制折线l与d相交
canvas.drawLine(lx,ly,x,y,brokenLinePaint);
}
/**
* 绘制比例说明文字
* @param canvas
* @param l 线段l文字长度,或者说是线段l的长度
* @param x 折线的拐点x
* @param y 折线的拐点y
* @param lx 折线的端点x
* @param ly 折线的端点y
* @param name 说明文字
* @param proport 说明文字的比例,显示在线段l下方
* @param widthName name的长度
* @param widthProport proport的长度
* @param dy 绘制文字的偏移量,距离折线的长度
*/
private void drawText(Canvas canvas, float l, float x, float y,
float lx, float ly, String name, String proport,
int widthName, int widthProport,int dy) {
// int dy=10; //绘制文字的偏移量,距离折线的长度
float tranlateName=(l-widthName)/2.0f;
float tranlateProport=(l-widthProport)/2.0f;
if(x2,textPaint);
canvas.drawText(proport,lx+tranlateProport,ly+dy,textPaint);
}else {
canvas.drawText(name,x+tranlateName,y-dy/2,textPaint);
canvas.drawText(proport,x+tranlateProport,y+dy,textPaint);
}
}
private Paint getRingPaint(){
Paint ringPaint=new Paint();
ringPaint.setAntiAlias(true);
ringPaint.setStyle(Paint.Style.FILL);
ringPaint.setColor(getRandomColor());
return ringPaint;
}
private int getRandomColor(){
Random random=new Random();
int r = 10+random.nextInt(200);
int g = 10+random.nextInt(200);
int b = 10+random.nextInt(200);
int rgb = Color.rgb(r,g,b);
return rgb;
}
4.绘制同心圆
//绘制圆
private void drawCircle(Canvas canvas) {
float sRadius=radius-radius/4;
LogUtil.i(this,"sRadius="+sRadius+",radius="+radius);
//1.绘制大圆
canvas.drawCircle(cx, cy,radius, bCiclePaint);
//2.绘制小圆
canvas.drawCircle(cx, cy,sRadius, sCiclePaint);
}
到这里,基本的饼图绘制就完成了
布局代码:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main"
android:layout_width="match_parent" android:layout_height="match_parent"
>
<com.ishow.huiyuantest.widget.PieChartView
android:id="@+id/pv"
android:layout_below="@id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
RelativeLayout>
activity代码:
public class MainActivity extends AppCompatActivity {
@Bind(R.id.pv)
PieChartView pv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UIUtils.registerApplication(getApplication());
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
initPieChart();
}
private void initPieChart() {
List pieChartList=new ArrayList<>();
PieChart pc=new PieChart();
pc.setId(1);
pc.setName("银卡会员");
pc.setProport(35);
pieChartList.add(pc);
pc=new PieChart();
pc.setId(2);
pc.setName("金卡会员");
pc.setProport(10);
pieChartList.add(pc);
pc=new PieChart();
pc.setId(3);
pc.setName("铜卡会员");
pc.setProport(95);
pieChartList.add(pc);
pc=new PieChart();
pc.setId(4);
pc.setName("普通会员");
pc.setProport(300);
pieChartList.add(pc);
pv.addPieCharData(pieChartList);
}
}
效果还是不错的
其实很多看起来复杂的控件,其实也并不是非常的复杂,只要我们把一些数学关系理清楚了,一切都变得简单起来
源码地址
http://download.csdn.net/detail/huangxuanheng/9822195