Android 扇形統計圖的設計與編寫

先上圖,包含統計圖初始化時的動畫,點擊環形的效果

沒有數據時狀態

餅狀圖根據效果不同,調用的APi參數略微有差異,有些同學可能不想要中間的空白直接全部展示扇形,emmmm。這種需求比你現在看到的這個樣子 要簡單的多,看完這種樣式的,其他的實現方式也就懂了。

首先,實現思路不能落下:

1、在自定義View裏面初始化的時候(構造方法裏),

setLayerType(View.LAYER_TYPE_SOFTWARE, null);

這行代碼不能忘,這個是硬件加速,某些低端手機上如果不寫着一行,陰影部分無法展示,而且相鄰扇形圖無法無縫對接,即便看起來你的代碼並沒有什麼問題。

2、計算畫布的大小以及定位我們的統計圖位置

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
//View寬高
        mTotalWidth = w - getPaddingLeft() - getPaddingRight();
        mTotalHeight = h - getPaddingTop() - getPaddingBottom();
        //餅狀圖半徑
        mRadius = WindowsUtil.dp2px(178) / 2;
        //參與計算的扇形半徑(我們是畫一個空心圓,例如:畫圓的線的粗爲2,實際需要半徑爲10,如果輸入參數時半徑不減去線粗的1/2的話 看到的環形外圈半徑爲11,內圈半徑爲9)
        calculateRadius = mRadius - mRadius / 4;
        mRectF.left = -calculateRadius;
        mRectF.top = -calculateRadius;
        mRectF.right = calculateRadius;
        mRectF.bottom = calculateRadius;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
           //限定位置(原點爲中心點)
        canvas.translate(mTotalWidth / 2, mTotalHeight / 2);
        //繪製餅圖的每塊區域
        drawPiePath(canvas);

    }

3、好了,直接說重點,看看 drawPiePath(canvas);方法裏怎麼操作,怎麼去繪製的

 /**
     * 繪製餅圖的每塊區域 和文本
     *
     * @param canvas
     */
    private void drawPiePath(Canvas canvas) {
        //起始地角度
        float startAngle = starAngle;
        if (mDataList != null && mDataList.size() > 0) {

            for (int i = 0; i < mDataList.size(); i++) {
                String name = mDataList.get(i).getName() + (int) mDataList.get(i).getValue();
                float sweepAngle = mDataList.get(i).getValue() / mTotalValue * 360;//每個扇形的角度
                sweepAngle = sweepAngle * percent;
                mPaint.setColor(mDataList.get(i).getColor());

                mLinePaint.setColor(mDataList.get(i).getColor());
                mTextPaint.setColor(mDataList.get(i).getColor());

                double x1, y1;
                int addLength = 0;
                if (i != 0) {
                    if (i > 3) {
                        for (int c = 0; c < maxListNumH; c++) {
                            if (lineAndTextHorizontalAddress[i - c] > 0) {
                                addLength += WindowsUtil.dp2px(mTextPaint.measureText(name) / (maxListNumH * 2));
                            }
                        }
                    }
                    if (mDataList.get(i).getQuadrant() == 1 || mDataList.get(i).getQuadrant() == 3) {
                        x1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.3);
                        y1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.4);
                    } else {
                        if (mDataList.get(i - 1).getQuadrant() != mDataList.get(i).getQuadrant()) {
                            x1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.3);
                            y1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.4);
                        } else {
                            if (lineAndTextHorizontalAddress[i - 1] == 0) {
                                x1 = mRadius + LineLength + addLength;
                                y1 = mRadius + LineLength;
                            } else {
                                x1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.3);
                                y1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.4);
                            }
                        }
                    }
                } else {
                    x1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.3);
                    y1 = mRadius + LineLength + lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name) * 0.4);
                }
                //處理註釋線的長度
                //確定直線的起始和結束的點的位置
                float pxs = (float) (mRadius * Math.cos(Math.toRadians(startAngle + sweepAngle / 2)));
                float pys = (float) (mRadius * Math.sin(Math.toRadians(startAngle + sweepAngle / 2)));
                float pxt = (float) ((x1) * Math.cos(Math.toRadians(startAngle + sweepAngle / 2)));
                float pyt = (float) ((y1) * Math.sin(Math.toRadians(startAngle + sweepAngle / 2)));

                //繪製註釋一線(斜線)
                canvas.drawLine(pxs, pys, pxt, pyt, mLinePaint);
                //繪製二線(水平線)和文本
                float stopX = 0, stopY = 0;

                if (mDataList.get(i).getQuadrant() == 2 || mDataList.get(i).getQuadrant() == 3) {//2 3 象限

                    float x = (pxt - horizontalLineLength - (lineAndTextHorizontalAddress[i] * (mTextPaint.measureText(name))) - addLength);
                    stopX = x;
                    stopY = pyt;
                    canvas.drawLine(pxt, pyt, x, pyt, mLinePaint);
                    canvas.drawText(name + "", stopX - mTextPaint.measureText(name), pyt, mTextPaint);
                    smallCircleRectF.set(stopX, stopY - 5, stopX + 10, stopY + 5);
                } else {
                    float x = (pxt + horizontalLineLength + (lineAndTextHorizontalAddress[i] * mTextPaint.measureText(name)) + addLength);
                    stopX = x;
                    stopY = pyt;
                    canvas.drawLine(pxt, pyt, x, pyt, mLinePaint);
                    canvas.drawText(name + "", stopX, pyt, mTextPaint);
                    smallCircleRectF.set(stopX - 10, stopY - 5, stopX, stopY + 5);
                }

                //畫扇形
                setTouchView(pxt, pyt);
                //被點擊狀態
                if (position - 1 == i) {
                    Paint paint = new Paint(mPaint);
                    paint.setStyle(Paint.Style.STROKE);
                    paint.setStrokeWidth(mRadius / 2);
                    if (haveShadow) {
                        paint.setShadowLayer(4, 3, 3, Color.GRAY);
                    }
                    //外部展示扇形
                    canvas.drawArc(mRectFTouch, startAngle, sweepAngle, false, paint);
                } else {
                    //外部展示扇形
                    canvas.drawArc(mRectF, startAngle, sweepAngle, false, mPaint);
                }
                Paint paintCircle = new Paint(mPaint);
                paintCircle.setStrokeWidth(10);
                paintCircle.setStyle(Paint.Style.FILL);
                //線頭的小圈圈
                canvas.drawArc(smallCircleRectF, 0, 360, true, paintCircle);
                angles[i] = startAngle;
                startAngle += sweepAngle;
                anglesEnds[i] = startAngle;
                float textWidth = paintCenterText.measureText(centerText);
                canvas.drawText(centerText, -textWidth / 2, -WindowsUtil.dp2px(12 / 2), paintCenterText);
                float textWidth1 = paintCenterText.measureText("" + (int) mTotalValue);
                canvas.drawText("" + (int) mTotalValue, -textWidth1 / 2, WindowsUtil.dp2px(14), paintCenterText);
            }
        } else {
            mPaint.setStrokeWidth(mRadius / 2);
            mPaint.setColor(initColor);
            canvas.drawArc(mRectF, 0, 360, false, mPaint);
            float textWidth = paintCenterText.measureText(centerText);
            canvas.drawText(centerText, -textWidth / 2, -WindowsUtil.dp2px(12 / 2), paintCenterText);
            float textWidth1 = paintCenterText.measureText("" + (int) mTotalValue);
            canvas.drawText("" + (int) mTotalValue, -textWidth1 / 2, WindowsUtil.dp2px(14), paintCenterText);
        }
    }

e m m m m m,代碼有點長,囉哩囉嗦的,實際上是因爲有許多我們需要去注意的效果、判斷,所以看起來變量多了一些,而且,悄悄地跟你們說,,我原來計劃讓整個扇形轉起來,想大轉盤那樣,並且數據不會重疊(相鄰扇形角度過於小的時候,扇形注視數據文本會重疊),後來發現這個過程存在很多問題,項目也着急上線,就暫時沒實現 正如代碼中你看見的,我把整個座標軸分成了四個象限(初中數學),因爲考慮到數據重疊,相鄰象限的折現需要朝不同的方向延伸,我把每個象限又分成兩個區間。設置一個基礎長度x,如果出現同一個區間的扇形相鄰,並且同時小於a度(一般寫個15就可以),那麼他們的註釋線可能會重疊(我說的註釋線 與折現是一個東西,並且折現分爲兩段,與不平行的,與扇形角度相關的 叫第一段,平行的嗎,與註釋文本挨着的叫第二段)

好了,概念中的名稱我們知道了,基本的劃分理念(八個區間)也知道了,代碼思路就好理解多了

 4、我們來說一下API,說完API的使用方式,那麼直接上代碼,有些小夥伴比較懶,不想看原理與步驟,那就看看API的意思:

canvas.drawArc(mRectFTouch, startAngle, sweepAngle, false, paint);

這行代碼是畫扇形的核心代碼裏面各個參數大家應該知道是什麼吧都,上源碼來認真的複習一下

 /**
     * <p>
     * Draw the specified arc, which will be scaled to fit inside the specified oval.繪製指定的弧,將縮放到指定的橢圓內。
     * </p>
     * <p>
     * If the start angle is negative or >= 360, the start angle is treated as start angle modulo360.
    如果起始角爲負或>= 360,則將起始角視爲起始角模360
     * </p>
     * <p>
     * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs
     * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is
     * negative, the sweep angle is treated as sweep angle modulo 360
     * 如果掃描角爲>= 360,則完全繪製橢圓。注意,這與SkPath::arcTo略有不同,後者處理掃角模360。如果掃描角爲負,則將掃描角視爲掃描角模360</p>
     * <p>
     * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0  degrees (3 o'clock on a watch.)
     *圓弧是順時針畫的。一個0度的角對應於一個0的幾何角
(表上3點鐘) </p>
     *
     * @param oval The bounds of oval used to define the shape and size of the arc
橢圓的邊界,用來定義弧的形狀和大小
     * @param startAngle Starting angle (in degrees) where the arc begins
起始角度
     * @param sweepAngle Sweep angle (in degrees) measured clockwise
結束角度
     * @param useCenter If true, include the center of the oval in the arc, and close it if it is   being stroked. This will draw a wedge
如果爲true,則將橢圓的中心包含在弧中,如果false,則將其關閉。這將畫出一個楔子
(具體的結果大家可以試一下)
     * @param paint The paint used to draw the arc
畫筆 沒什麼可說的,承載了很多責任
     */
    public void drawArc(@NonNull RectF oval, float startAngle, float sweepAngle, boolean useCenter,
            @NonNull Paint paint) {
        super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
    }

RectF oval, float startAngle, float sweepAngle, boolean useCenter,

第一個參數RectF oval,限定畫的扇形在座標軸上的位置與大小,我們的點擊突出效果就是靠改變他的參數它來實現的

到了這裏不得不說一個效果————陰影效果,給畫筆設置

paint.setShadowLayer(float radius ,float dx,float dy,int color);

在圖形下面設置陰影層,產生陰影效果,radius爲陰影的角度,dx和dy爲陰影在x軸和y軸上的距離,color爲陰影的顏色

這行代碼,就會有投影,emmmm 太簡單了,想當初爲了這個陰影效果,我是爬遍了各種技術帖子,都計劃自己去畫陰影了,幸虧靈機一動看了一下Pain 的各個參數的介紹,

說到了Pain。那麼我們把另一個使用到的相關的屬性也說一下(控制 畫扇形還是畫圓弧)

        paintCenterText.setStyle(Paint.Style.FILL);    //設置畫筆爲填充
        mPaint.setStyle(Paint.Style.STROKE);//不填充
        設置畫筆的樣式, FILL,FILL_OR_STROKE,或STROKE

這裏送大家一個鏈接,關於Paint的方法介紹,https://blog.csdn.net/lpjishu/article/details/84716912,這位大神寫的還不錯

Api知道了,大致的實現思路知道了,剩下的就是代碼以及計算了,

計算主要用到了餘弦定理,高中數學忘了的童鞋可以回去複習一下了,因爲這個忘了的話,根本沒法去計算(勾股定理已經不夠用了)

使用方式

 pieChart = findViewById(R.id.pc_devices);
//設置點擊扇形之後的處理模式
        pieChart.setTouchStyle(PieChartConstant.TOUCH_TYPE_MOVE);

//可以自己去模擬數據
for(int i=0;i<z;i++){
//數量除了展示之外還會涉及到具體的計算(扇形比重)
  arrayList.add(new PieDataEntity(“名稱”
                                , “數量”, color));
}
//arrayList 可以穿入null
  pieChart.setDataList(arrayList);
                    pieChart.startAnimation(2000);

這裏我先列出所有涉及到的計算:

1、點擊環形之後的偏移量

2、註釋線的長度計算,文本的位置

3、扇形的中線長度位置

4、每一個扇形的的起始與中止位置

 

扇形的api大家知道怎麼用了,然後點擊扇形之後的突出實現原理(畫突出的扇形的時候,圓心按照扇形的中心角度方向去偏移)大家也明白了,比較有難點的,或者說比較複雜的,是註釋線的長度適配,錯位適配法

代碼鏈接:扇形統計圖代碼下載

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