1. 程式人生 > >自定義View繪製餅狀圖和環狀圖

自定義View繪製餅狀圖和環狀圖

最近工作中遇到一個需求,就是將不同年齡段資料以餅狀圖或者環狀圖的形式展示出來。於是利用android自定義的知識封裝一個自定義View,方便日後使用,特此記錄。
 

效果圖如下

1.餅狀圖

在這裡插入圖片描述

1.環狀圖

在這裡插入圖片描述

主要強調以下3部分

1.value中新增attr.xml屬性檔案
2.資料來源
3.自定義餅圖或者環形圖
 

1.value中新增attr.xml屬性檔案

value/attr_sector.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <
declare-styleable
name="SectorView">
<!-- 圓的半徑 --> <attr name="min_circle_radio" format="float"/> <!-- 內圓的顏色 --> <attr name="min_circle_color" format="color"/> <!-- 扇形半徑 --> <attr name="sector_radio" format="float"/>
<!-- 扇形分幾段 --> <attr name="sector_part_num" format="integer"/> <!--描述文字顏色--> <attr name="sector_desc_text_color" format="integer"/> <!--描述文字大小--> <attr name="sector_desc_text_size" format="float"/> </declare-styleable
>
</resources>

之所以編寫attr.xml檔案,原因有二:
(1).為了更直觀的瞭解自定義View涉及到的屬性引數,方便管理
(2).在自定義View檔案中,封裝了一些通用的介面(eg.設定描述文字的字型顏色,內圓的半徑和顏色等等),在設定之前,往往會初始化一些預設值。這就用到我們的attr屬性了。
 

2.資料來源
public class AgeEntry {

    private int totalPart;
    private int childTotalIn;
    private int youthTotalIn;
    private int middleTotalIn;
    private int oldTotalIn;

    public int getChildTotalIn() {
        return childTotalIn;
    }

    public void setChildTotalIn(int childTotalIn) {
        this.childTotalIn = childTotalIn;
    }

    public int getYouthTotalIn() {
        return youthTotalIn;
    }

    public void setYouthTotalIn(int youthTotalIn) {
        this.youthTotalIn = youthTotalIn;
    }

    public int getMiddleTotalIn() {
        return middleTotalIn;
    }

    public void setMiddleTotalIn(int middleTotalIn) {
        this.middleTotalIn = middleTotalIn;
    }

    public int getOldTotalIn() {
        return oldTotalIn;
    }

    public void setOldTotalIn(int oldTotalIn) {
        this.oldTotalIn = oldTotalIn;
    }

    public int getTotalPart() {
        return totalPart;
    }

    public void setTotalPart(int totalPart) {
        this.totalPart = totalPart;
    }

    @Override
    public String toString() {
        return "AgeEntry{" +
                "totalPart=" + totalPart +
                ", childTotalIn=" + childTotalIn +
                ", youthTotalIn=" + youthTotalIn +
                ", middleTotalIn=" + middleTotalIn +
                ", oldTotalIn=" + oldTotalIn +
                '}';
    }
}

待展示的資料,你完全可以按照自己的需求去定義。這些資料最終都需要你去換算成佔比,用於圖形的繪製。

 

3.自定義餅圖或者環形圖(完整檔案)

public class SectorView extends View {

    private static final String TAG = "SectorView";

    //圓心座標
    private int mHeight, mWidth;
    private int centerX, centerY;

    //畫筆
    private Paint mPaint;
    private Paint mTextPaint;

    //描述文字
    private float mDescTextSize;
    private int mDescTextColor;

    //內圓
    private float mMinCircleRadio;
    private int mSectorNum;

    //扇形
    private int mMinCircleColor;
    private float mSectorRadio;

    //是否顯示裡面的圓
    private boolean isShowInnerCircle = false;

    //資料
    private float[] mAgeLevelPercent = new float[4];
    private String[] mAgeDesc = new String[4];

    //age年齡段對應的顏色
    private int[] mAgeColors = {
            Color.parseColor("#67E5E5"),
            Color.parseColor("#8BB6F6"),
            Color.parseColor("#C29DFC"),
            Color.parseColor("#E5E570"),
    };


    public SectorView(Context context) {
        super(context, null);
    }

    public SectorView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SectorView);
        //裡面圓半徑,預設200f
        mMinCircleRadio = a.getFloat(R.styleable.SectorView_min_circle_radio, 120f);
        //裡面圓的顏色,預設白色
        mMinCircleColor = a.getColor(R.styleable.SectorView_min_circle_color, Color.parseColor("#ffffff"));

        //扇形半徑,預設300f
        mSectorRadio = a.getFloat(R.styleable.SectorView_sector_radio, 300f);
        //扇形分幾段
        mSectorNum = a.getInt(R.styleable.SectorView_sector_part_num, 4);

        mDescTextSize = a.getFloat(R.styleable.SectorView_sector_desc_text_size, 40f);
        mDescTextColor = a.getInt(R.styleable.SectorView_sector_desc_text_color, Color.parseColor("#000000"));

        mPaint = new Paint();
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setAntiAlias(true);

        mTextPaint = new Paint();
        mTextPaint.setTextSize(40f);
        mTextPaint.setStrokeWidth(3);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(Color.BLACK);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        centerX = (getRight() - getLeft()) / 2;
        centerY = (getBottom() - getTop()) / 2;
        int min = mHeight > mWidth ? mWidth : mHeight;
        if (mSectorRadio > min / 2) {
            mSectorRadio = (int) ((min - getPaddingTop() - getPaddingBottom()) / 3.5);
        }
        canvas.save();
        drawCircle(canvas);
        canvas.restore();

        canvas.save();
        drawLineAndText(canvas);
        canvas.restore();
    }


    /**
     * 繪製線與文字
     *
     * @param canvas
     */
    private void drawLineAndText(Canvas canvas) {
        int start = 0;
        canvas.translate(centerX, centerY);
        mTextPaint.setStrokeWidth(4);

        for (int i = 0; i < mSectorNum; i++) {
            float angles = mAgeLevelPercent[i] * 360;
            drawLine(canvas, start, angles, mAgeDesc[i], mAgeColors[i]);
            start += angles;
        }
    }

    /**
     * 繪製線和文字
     *
     * @param canvas
     * @param start  繪製的起始角度
     * @param angles 資料塊佔用的角度(掃過的扇形弧度)
     * @param text   待繪製的描述文字
     * @param color
     */
    private void drawLine(Canvas canvas, int start, float angles, String text, int color) {
        mTextPaint.setColor(color);

        //參照數學公式::b = c *Math.cos(Math.toRadians(A)),其中Math.toRadians(A)::角度轉換成弧度
        float startX, startY;
        startX = (float) ((mSectorRadio - 20) * Math.cos(Math.toRadians(start + angles / 2)));
        startY = (float) ((mSectorRadio - 20) * Math.sin(Math.toRadians(start + angles / 2)));

        //折線的終點
        float stopX, stopY;
        stopX = (float) ((mSectorRadio + 40) * Math.cos(Math.toRadians(start + angles / 2)));
        stopY = (float) ((mSectorRadio + 40) * Math.sin(Math.toRadians(start + angles / 2)));
        canvas.drawLine(startX, startY, stopX, stopY, mTextPaint);

        //繪製橫線
        int endX;
        if (stopX > 0) {//判斷橫線是畫在左邊還是右邊
            endX = (centerX - getPaddingRight() - 20);
        } else {
            endX = (-centerX + getPaddingLeft() + 20);
        }
        canvas.drawLine(stopX, stopY, endX, stopY, mTextPaint);

        int dx = (int) (endX - stopX);//判斷文字繪製在左邊還是右邊

        //測量文字大小
        Rect rect = new Rect();
        mTextPaint.getTextBounds(text, 0, text.length(), rect);
        int w = rect.width();
        int h = rect.height();
        int offset = 20;//文字在橫線的偏移量
        canvas.drawText(text, 0, text.length(), dx > 0 ? stopX + offset : stopX - w - offset, stopY + h, mTextPaint);

        //測量百分比大小
        String percentage = angles / 3.60 + "";
        //控制百分比的位數
        percentage = percentage.substring(0, percentage.length() > 4 ? 4 : percentage.length()) + "%";
        mTextPaint.getTextBounds(percentage, 0, percentage.length(), rect);
        w = rect.width() - 10;

        //繪製百分比
        canvas.drawText(percentage, 0, percentage.length(), dx > 0 ? stopX + offset : stopX - w - offset, stopY - 5, mTextPaint);
    }

    /**
     * 繪製扇形
     *
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        RectF rect = new RectF((float) (centerX - mSectorRadio), centerY - mSectorRadio,
                centerX + mSectorRadio, centerY + mSectorRadio);
        int start = 0;
        for (int i = 0; i < mSectorNum; i++) {
            float angles = (mAgeLevelPercent[i] * 360);
            mPaint.setColor(mAgeColors[i]);//mAgeColors.length:::5
            canvas.drawArc(rect, start, angles, true, mPaint);
            start += angles;
        }

        //顯示內圓
        if (isShowInnerCircle) {
            mPaint.setColor(mMinCircleColor);
            canvas.drawCircle(centerX, centerY, mMinCircleRadio, mPaint);
        }

    }

    /**
     * 是否顯示裡面的圓
     *
     * @param isShowInnerCircle
     */
    public void showInnerCircle(boolean isShowInnerCircle) {
        this.isShowInnerCircle = isShowInnerCircle;
        invalidate();
    }

    /***
     * 設定裡面的圓的顏色
     * @param color
     */
    public void setInnerCircleColor(int color) {
        mMinCircleColor = color;
        invalidate();
    }

    /***
     * 設定裡面的圓的半徑
     * @param radio
     */
    public void setInnerCircleRadio(float radio) {
        mMinCircleRadio = radio;
        invalidate();
    }

    /***
     * 設定資料塊文字描述的字型大小
     * @param textSize
     */
    public void setDescTextSize(float textSize) {
        mDescTextSize = textSize;
        mTextPaint.setTextSize(mDescTextSize);
        invalidate();
    }

    /***
     * 設定描述文字的顏色
     * @param color
     */
    public void setDescTextColor(int color) {//todo:目前描述文字的顏色和扇形顏色是一致的
        mDescTextColor = color;
        mTextPaint.setColor(color);
        invalidate();
    }

    /***
     * 設定扇形的半徑大小
     * @param mSectorRadio
     */
    public void setSectorRadio(int mSectorRadio) {
        this.mSectorRadio = mSectorRadio;
        setDescTextSize(mSectorRadio / 6);
        invalidate();
    }

    /***
     * 設定資料
     * @param entry
     */
    public void setData(AgeEntry entry) {
        int childTotalIn = entry.getChildTotalIn();
        int youthTotalIn = entry.getYouthTotalIn();
        int middleTotalIn = entry.getMiddleTotalIn();
        int oldTotalIn = entry.getOldTotalIn();

        Log.d(TAG, " childTotalIn::" + childTotalIn + "  youthTotalIn::" + youthTotalIn + "  middleTotalIn::" + middleTotalIn + " oldTotalIn::" + oldTotalIn);
        int total = childTotalIn + youthTotalIn + middleTotalIn + oldTotalIn;
        mAgeLevelPercent[0] = (float) childTotalIn / total;
        mAgeLevelPercent[1] = (float)youthTotalIn / total;
        mAgeLevelPercent[2] = (float)middleTotalIn / total;
        mAgeLevelPercent[3] = (float)oldTotalIn / total;

        mAgeDesc[0] = "未成年";
        mAgeDesc[1] = "青年";
        mAgeDesc[2] = "中年";
        mAgeDesc[3] = "老年";

        for (int i = 0; i < 4; i++) {
            Log.d(TAG, "mAgeLevelPercent[" + i + "]= " + mAgeLevelPercent[i] + "\n" +
                    "mAgeDesc[" + i + "]= " + mAgeDesc[i]