Android自定義View之扇形餅狀圖

前言:繼上次寫了自定義圓形進度條後,今天給大家帶來自定義扇形餅狀圖。先上效果圖:
水果大拼盤
是不是很炫?看上去還有點立體感。下面帶大家一起來瞧一瞧吧。

一、定義成員變量,重寫構造方法

看着這個效果圖,我們可以想象下接下來暫時會需要用到以下屬性:

    /**
     * 存放事物的品種與其對應的數量
     */
    private Map kindsMap = new LinkedHashMap<String, Integer>();
    /**
     *  存放顏色
     */
    private ArrayList<Integer> colors = new ArrayList<>();

    private Paint mPaint;//餅狀畫筆
    private Paint mTextPaint; // 文字畫筆
    private static final int DEFAULT_RADIUS = 200;
    private int mRadius = DEFAULT_RADIUS; //外圓的半徑
    private String centerTitle; //中間標題

然後重寫父類的構造方法,初始化畫筆:

 public PieChatView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        mTextPaint = new Paint();

        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL);

        mTextPaint.setColor(Color.BLACK);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setStyle(Paint.Style.STROKE);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    public PieChatView(Context context) {
        this(context, null, 0);

    }

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

一般都是一個參數和兩個參數的全部調用第三個參數的。我三個參數的構造方法中沒有去從xml文件中去獲取屬性了。我完全就用代碼實現了。也可以將一些屬性放在attrs.xml文件中,然後去獲取。自行選擇吧。

二、onMeasure()

這個方法,只要你以前寫過自定義View,基本上就是一樣的套路:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int wideSize = MeasureSpec.getSize(widthMeasureSpec);
        int wideMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int width, height;
        if (wideMode == MeasureSpec.EXACTLY) { //精確值 或matchParent
            width = wideSize;
        } else {
            width = mRadius * 2 + getPaddingLeft() + getPaddingRight();
            if (wideMode == MeasureSpec.AT_MOST) {
                width = Math.min(width, wideSize);
            }

        }

        if (heightMode == MeasureSpec.EXACTLY) { //精確值 或matchParent
            height = heightSize;
        } else {
            height = mRadius * 2 + getPaddingTop() + getPaddingBottom();
            if (heightMode == MeasureSpec.AT_MOST) {
                height = Math.min(height, heightSize);
            }

        }
        setMeasuredDimension(width, height);
        mRadius = (int) (Math.min(width - getPaddingLeft() - getPaddingRight(),
                height - getPaddingTop() - getPaddingBottom()) * 1.0f / 2);

    }

就是獲得系統測量好的寬高,和模式後分三種模式去討論。最終通過setMeasuredimension()確定寬高的值。寬高確定好了,那就可以確定下整個餅狀圖的半徑 mRadius了。取寬高中的較小的那個。

三、onDraw();

我們需要畫一個一個的扇形,還有將扇形從零到360的動畫效果,還有扇形中的文字,中間的文字,還有實現立體感的效果。

1.畫扇形。

畫一個扇形還是蠻容易的:通過畫布調用drawArc()方法話一個60度的扇形:
mPaint.setStyle(Paint.Style.FILL);
mTextPaint.setColor(Color.RED);
RectF oval = new RectF(-mRadius, -mRadius, mRadius, mRadius);
mCanvas.drawArc(oval, 0, 60, true, mPaint);

前面設置下畫筆的顏色,style爲實心,就這幾行代碼的事情。第一個參數:是一個正方形,表明在這個區域內畫扇形。第二個參數:從哪個角度開始畫,第三個參數:就是畫的角度度數,而不是畫到哪個角度去。第四個參數:是否以正方形中心爲圓心。我這樣一說,你可能就會有點懵逼了。你自己改爲false,看效果,就理解我說的了。效果如下:
這裏寫圖片描述
目前畫了一個扇形,如此多的扇形需要用戶去設置數據,來確定每個扇形的角度.

    public ArrayList<Integer> getColors() {
        return colors;
    }

    public void setColors(ArrayList<Integer> colors) {
        this.colors = colors;
    }
     public void setDataMap(LinkedHashMap<String, Integer> map) {
        this.kindsMap = map;
    }
     public String getCenterTitle() {
        return centerTitle;
    }

    public void setCenterTitle(String centerTitle) {
        this.centerTitle = centerTitle;
    }

然後通過遍歷,一 一畫出這些扇形:

 @Override
    protected void onDraw(Canvas mCanvas) {
        super.onDraw(mCanvas);
        mCanvas.translate((getWidth() + getPaddingLeft() - getPaddingRight()) / 2, (getHeight() + getPaddingTop() - getPaddingBottom()) / 2);

        paintPie(mCanvas);

    }
private void paintPie(final Canvas mCanvas) {
     if (kindsMap != null) {
            Set<Map.Entry<String, Integer>> entrySet = kindsMap.entrySet();
            Iterator<Map.Entry<String, Integer>> iterator = entrySet.iterator();
            int i = 0;
            float currentAngle = 0.0f;
            while (iterator.hasNext()) {
                Map.Entry<String, Integer> entry = iterator.next();
                int num = entry.getValue();
                float needDrawAngle = num * 1.0f / sum * 360;
                mPaint.setColor(colors.get(i));
                mCanvas.drawArc(oval, currentAngle, needDrawAngle - 1, true, mPaint);
                currentAngle = currentAngle + needDrawAngle;
                i++;
            }
        }  
        }

記得,要先將畫筆移到畫布中心,currentAngle:當前的角度值,needDrawAngle :需要畫多少角度。sum:數據的總個數,爲每一個數據相加的和,計算百分比。效果如圖:
這裏寫圖片描述

2、動畫效果實現

咋一看離我們的預期效果還差好遠。只有畫完以後的樣子,動畫都沒有。別急別急,動畫馬上就來:

 private void initAnimator() {
        ValueAnimator anim = ValueAnimator.ofFloat(0, 360);
        anim.setDuration(10000);
        anim.setInterpolator(new AccelerateDecelerateInterpolator());
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                animatedValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        anim.start();
    }

設置一個動畫,在10秒內,隨着時間的推移,animatedValue從0到360度發生變化,每一次監聽到animatedValue的值得改變,就去刷新View,執行ondraw();這個時候我們來修改下前面的paintPie();

if (Math.min(needDrawAngle-1, animatedValue - currentAngle) >= 0) {
     mPaint.setColor(colors.get(i));
     mCanvas.drawArc(oval, currentAngle, Math.min(needDrawAngle - 1, animatedValue - currentAngle), true, mPaint);
                }

先看效果:
這裏寫圖片描述

我在這個地方遇到了一個坑----ondraw()方法多次執行時,每一次執行完畢後會抹去上一次畫過的圖形.也就是說,我沒用動畫的時候,ondraw()方法就調用了一次,調用了while循環後,把所有的扇形畫出來了。用了動畫之後,animatedValue的值一改變,就會執行一次while循環。每次while循環都會重頭開始畫,我們只要保證每次while循環所畫的角度正好是animatedValue,就ok了。所以,在needDrawAngle-1小於animatedValue - currentAngle時,就把該部分扇形畫出來。在needDrawAngle-1大於animatedValue - currentAngle時,那就只畫animatedValue - currentAngle的角度。

之所以減一,是因爲扇形之間留一條白白的縫隙。

3.添加文字,中間標題,實現立體感

當某個扇形角度,不夠文字的寬度時,我們就會將文字畫在圓的外面。所以這個我們得添加一個屬性minAngle,角度最小值,添加getter和setter方法,讓使用者去設置。當needDrawAngle小於這個角度值時,就畫在外面。大於這個值就畫在扇形中央。

//畫文字
    private void drawText(Canvas mCanvas, float textAngle, String kinds, float needDrawAngle) {
        Rect rect = new Rect();
        mTextPaint.setTextSize(sp2px(15));
        mTextPaint.getTextBounds(kinds, 0, kinds.length(), rect);
        if (textAngle >= 0 && textAngle <= 90) { //畫布座標系第一象限(數學座標系第四象限)
            if (needDrawAngle < minAngle) { //如果小於某個度數,就把文字畫在餅狀圖外面
                mCanvas.drawText(kinds, (float) (mRadius * 1.2 * Math.cos(Math.toRadians(textAngle))), (float) (mRadius * 1.2 * Math.sin(Math.toRadians(textAngle)))+rect.height()/2, mTextPaint);
            } else {
                mCanvas.drawText(kinds, (float) (mRadius * 0.75 * Math.cos(Math.toRadians(textAngle))), (float) (mRadius * 0.75 * Math.sin(Math.toRadians(textAngle)))+rect.height()/2, mTextPaint);
            }
        } else if (textAngle > 90 && textAngle <= 180) { //畫布座標系第二象限(數學座標系第三象限)
            if (needDrawAngle < minAngle) {
                mCanvas.drawText(kinds, (float) (-mRadius * 1.2 * Math.cos(Math.toRadians(180 - textAngle))), (float) (mRadius * 1.2 * Math.sin(Math.toRadians(180 - textAngle)))+rect.height()/2, mTextPaint);
            } else {
                mCanvas.drawText(kinds, (float) (-mRadius * 0.75 * Math.cos(Math.toRadians(180 - textAngle))), (float) (mRadius * 0.75 * Math.sin(Math.toRadians(180 - textAngle)))+rect.height()/2, mTextPaint);
            }
        } else if (textAngle > 180 && textAngle <= 270) { //畫布座標系第三象限(數學座標系第二象限)
            if (needDrawAngle < minAngle) {
                mCanvas.drawText(kinds, (float) (-mRadius * 1.2 * Math.cos(Math.toRadians(textAngle - 180))), (float) (-mRadius * 1.2 * Math.sin(Math.toRadians(textAngle - 180)))+rect.height()/2, mTextPaint);
            } else {
                mCanvas.drawText(kinds, (float) (-mRadius * 0.75 * Math.cos(Math.toRadians(textAngle - 180))), (float) (-mRadius * 0.75 * Math.sin(Math.toRadians(textAngle - 180)))+rect.height()/2, mTextPaint);
            }
        } else { //畫布座標系第四象限(數學座標系第一象限)
            if (needDrawAngle < minAngle) {
                mCanvas.drawText(kinds, (float) (mRadius * 1.2 * Math.cos(Math.toRadians(360 - textAngle))), (float) (-mRadius * 1.2 * Math.sin(Math.toRadians(360 - textAngle)))+rect.height()/2, mTextPaint);
            } else {
                mCanvas.drawText(kinds, (float) (mRadius * 0.75 * Math.cos(Math.toRadians(360 - textAngle))), (float) (-mRadius * 0.75 * Math.sin(Math.toRadians(360 - textAngle)))+rect.height()/2, mTextPaint);
            }
        }

    }

首先,畫文字,我們要知道文字的中心點座標。所以怎麼來求呢?看圖:
這裏寫圖片描述
通過這幅圖,大家應該知道了吧。唯一一點要說的就是畫圖的座標系和數學中的座標系不一樣。畫的時候還要分四個象限的情況去討論。有的人說,爲什麼不是在1/2r處,而是在0.75r處呢?因爲我們中心還要畫title的呀,對吧。畫在1/2處,不就覆蓋了嘛!
然後在中心畫文字咯。文字很容易。先畫個白色背景的圓半徑爲外圓半徑的一半,這樣就覆蓋了扇形中間的一部分。在內圓上再畫字。就OK了。
那立體效果怎麼實現呢?
也很簡單,可以仔細觀察,那部分好像是透明的。隱約能看見前面畫的扇形。所以我們在畫內園之前先畫個比內園稍微大一點的透明圓。設置畫筆的透明度就搞定。上代碼:

            while (iterator.hasNext()) {
                Map.Entry<String, Integer> entry = iterator.next();
                String kinds = entry.getKey();
                int num = entry.getValue();
                float needDrawAngle = num * 1.0f / sum * 360;
                String drawAngle = dff.format(needDrawAngle / 360 * 100);
                kinds = kinds + "," + drawAngle + "%";
                float textAngle = needDrawAngle / 2 + currentAngle;
                if (Math.min(needDrawAngle, animatedValue - currentAngle) >= 0) {
                    mPaint.setColor(colors.get(i));
                    mCanvas.drawArc(oval, currentAngle, Math.min(needDrawAngle - 1, animatedValue - currentAngle), true, mPaint);

                    mPaint.setColor(Color.WHITE);
                    mPaint.setAlpha(10);
                    mCanvas.drawCircle(0, 0, mRadius / 2 + dp2px(10), mPaint);
                    mPaint.setAlpha(255);
                    mCanvas.drawCircle(0, 0, mRadius / 2, mPaint);
                    drawCenterText(mCanvas, centerTitle, 0, 0, mTextPaint);
                    drawText(mCanvas, textAngle, kinds, needDrawAngle);
                }

                currentAngle = currentAngle + needDrawAngle;
                i++;
            }

該畫的畫完了。接下來就是一些優化和修改了。

四、優化

1.添加dp,sp,px轉化工具

/**
     * dp 2 px
     *
     * @param dpVal
     */
    protected int dp2px(int dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal, getResources().getDisplayMetrics());
    }

    /**
     * sp 2 px
     *
     * @param spVal
     * @return
     */
    protected int sp2px(int spVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                spVal, getResources().getDisplayMetrics());

    }

因爲圖中有很多地方接觸到文字,如果設置文字的時候用的是px,那麼在不同的機器上,顯示的文字大小將會差距很大。最好也提供一些方法讓使用者去設置這些字體的大小。

2.讓使用者去開啓動畫

因爲數據是使用者設置上去的,什麼時候開啓動畫,還得提供一個公開方法:

 public void startDraw() {
        if (kindsMap != null && colors != null && centerTitle != null) {
            initAnimator();
        }
    }

3.設置數據優化

可以知道,我用了兩個容器來裝數據。有沒有感覺到浪費資源?其實我們完全可以只用一個ArrayList即可。對吧。將所有數據定義爲一個實體的bean類。說到這,又遇到一個坑,我最開始使用hashMap去存數據的,優越hashMap存放數據是無序的,所以每次畫出來的扇形結構不一樣。你也許又會問我,怎麼顏色總是飄忽不定啊?因爲我是用了個for循環隨機生成的顏色,哈哈。

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        PieChatView pieChatView = (PieChatView) findViewById(R.id.pie);
        kindsMap.put("蘋果", 10);
        kindsMap.put("梨子", 30);
        kindsMap.put("香蕉", 10);
        kindsMap.put("葡萄", 30);
        kindsMap.put("哈密瓜", 10);
        kindsMap.put("獼猴桃",30);
        kindsMap.put("草莓", 10);
        kindsMap.put("橙子", 30);
        kindsMap.put("火龍果", 10);
        kindsMap.put("椰子", 20);
        for (int i = 1; i <= 40; i++){
            int r= (new Random().nextInt(100)+10)*i;
            int g= (new Random().nextInt(100)+10)*3*i;
            int b= (new Random().nextInt(100)+10)*2*i;
            int color = Color.rgb(r,g,b);
            if(Math.abs(r-g)>10&&Math.abs(r-b)>10&&Math.abs(b-g)>10){
                colors.add(color);
            }
        }
        pieChatView.setCenterTitle("水果大拼盤");
        pieChatView.setDataMap(kindsMap);
        pieChatView.setColors(colors);
        pieChatView.setMinAngle(50);
        pieChatView.startDraw();

到這裏,文章就結束了。最後附上代碼的github地址:
https://github.com/Demidong/ClockView
我寫的博客還不是很多,歡迎大家參與討論,留言,指正我的不足。

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