數據統計之餅圖實現

上週接到產品經理的需求,要求做一個餅圖,以作爲會員的數據統計,原型如下圖:
這裏寫圖片描述

原型看起來不算複雜,但是作爲多年開發經驗的我,還是有點懶的,於是乎就去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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章