数据统计之饼图实现

上周接到产品经理的需求,要求做一个饼图,以作为会员的数据统计,原型如下图:
这里写图片描述

原型看起来不算复杂,但是作为多年开发经验的我,还是有点懒的,于是乎就去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,绘制文字说明

  Map<Integer,PieChart>pieChartMap=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.Entry<Integer,PieChart>entry: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<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>x1){
                //在圆内
                x=(-c-sqrt)/a;
            }
            lx=x-l;
        }else if(sin<0&&cos>0){
            //第四象限
            if(x<x1){
                //在圆内
                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(x<cx){
            canvas.drawText(name,lx+tranlateName,ly-dy/2,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);
    }

到这里,基本的饼图绘制就完成了

布局代码:

<?xml version="1.0" encoding="utf-8"?>
<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<PieChart> 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

猜你喜欢

转载自blog.csdn.net/huangxuanheng/article/details/70430439