自定义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]);

        }

    }

}

 
上述自定义View中在绘制扇形图的标签线时候,需要应用的数学知识去确定折线绘制的起点和转折点。数学知识,此处不再赘叙。

 //参照数学公式::b = c *Math.cos(Math.toRadians(A)),其中Math.toRadians(A)::角度转换成弧度

实际上,在实现了上述效果饼状图和环形图效果之后,我并没有将其用在项目中去。因为我发现了更好的------MPAndroidChart开源库

MPAndroidChart非常强大,可以绘制折线图,柱状图,扇形图,饼状图等等,只有你想不到,没有它做不到的。
关于MPAndroidChart的使用推荐下面几个链接.
Android图表库MPAndroidChart(二)——线形图的方方面面,看完你会回来感谢我的
Android图表库MPAndroidChart(七)—饼状图可以再简单一点
等系列。(进去就有收获)
 
另外,关于MPAndroidChart在折线图和饼图使用一些需要注意的点。
eg:
1.折线图如何避免x轴左右尽头的坐标值被遮挡.
2.饼状图如何显示标签线

后续会拿出来记录一下。

最后附上示例:

Demo

猜你喜欢

转载自blog.csdn.net/zhangqunshuai/article/details/85705169