自定义View:饼图/扇形图(PieView)

来源:互联网 发布:java的贪吃蛇游戏 编辑:程序博客网 时间:2024/05/01 12:01

曰:这文章写得很不咋地,但是却是自己“开悟”的记录,不想浑浑噩噩,首先不去浑浑噩噩!

前两天看到朱凯大神发表了酝酿一整年的大作:《HenCoder:给高级 Android 工程师的进阶手册》,作为一个码农不敢妄看高级之物,但看在朱凯大神久处于朱大嫂淫威之下,关顾一下以示支持,不曾想到大神的文章是以细微处见真知,回到基础知识上,真是久旱逢甘露,挣扎已久的心突然静了下来,慢慢找回“多敲代码少BiBi”的正经路上…

这饼图view是在做《Android 开发进阶: 自定义 View 1-1 绘制基础》的练习时突发奇想来的(说是突发奇想是因为浮躁久了,不愿自己思考…),本来在练习画饼图,一个个画不难,突然想到,是否可以写一个通用的view,只要输入一组数据,就可以画出相应的饼图,但又一想,网上好看又好用的轮子那么多,为什么要自找麻烦,但又又一想,自己多久没思考了!?什么都是“有的用拿来就用”,明明知道对自己有益的做法,都因为自己的懒惰而不愿动手去做,拿“太麻烦”、“太难”等等等等来推脱自己…

灵光一闪,说干就干!

首先:理清思路

样图

看图,首先将饼图切割成几个模块:扇形图、线、文字
完了…(这思路有点那个…)

 @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);        drawSectors(canvas);        drawLines(canvas);        drawTexts(canvas);    }
// 模拟数据    private Map<String, Float> mDataMap = new LinkedHashMap<>(); // 需要按顺序,所以用 LinkedHashMap    {        mDataMap.put("Froyo", 2f);        mDataMap.put("Gingerbread", 6f);        mDataMap.put("ice Cream Sandwich", 5f);        mDataMap.put("Jelly Bean", 50f);        mDataMap.put("KitKat", 80f);        mDataMap.put("Lollipop", 110f);        mDataMap.put("Marshmallow", 40f);    }

模块一:扇形图

/**     * 画扇形     **/    private void drawSectors(Canvas canvas) {        mPaint.setStyle(Paint.Style.FILL); // 填充模式        mSectorsDataList = calculateSectorsDatas();        SectorsData sectorsData;        for (int i = 0; i < mSectorsDataList.size(); i++) {            sectorsData = mSectorsDataList.get(i);            mPaint.setColor(mColors.get(i));            canvas.drawArc(sectorsData.left, sectorsData.top, sectorsData.right, sectorsData.bottom, sectorsData.startAngle, sectorsData.sweepAngle, true, mPaint);        }    }

这里的要点在于计算各个数据所占的比例,根据比例计算扫过的角度及作画(Draw)的坐标

/**     * 计算各扇形的坐标、角度     **/    private List<SectorsData> calculateSectorsDatas() {        List<SectorsData> sectorsDataList = new ArrayList<>();        float startAngle = 0; // 开始角度        float sweepAngle; // 扇形角度        float sum = 0;        for (String key : mDataMap.keySet()) {            sum += mDataMap.get(key);        }        float maxValue = getMaxValue(mDataMap);        for (String key : mDataMap.keySet()) {            // 突出最大的块            if (mDataMap.get(key) == maxValue) {                mSkewingLength = 30f;            } else {                mSkewingLength = 10f;            }            sweepAngle = (mDataMap.get(key) / sum) * 360;            SectorsData sectorsData = calculateDirectionCoord(startAngle, sweepAngle);            sectorsData.startAngle = startAngle;            sectorsData.sweepAngle = sweepAngle;            sectorsDataList.add(sectorsData);            startAngle += sectorsData.sweepAngle;        }        return sectorsDataList;    }
/**     * 扇形数据类     **/    private class SectorsData {        float left;        float top;        float right;        float bottom;        float startAngle;        float sweepAngle;        float middleAngle;        float sectorsX;        float sectorsY;    }

其中,饼形图的圆点坐标已此PieView的宽高决定,取中心点(可根据要求另取中心点)

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        // 获取当前view的宽高        mWidth = getMeasuredWidth();        mHeight = getMeasuredHeight();        // 扇形中心点        mSectorsX = mWidth / 2;        mSectorsY = mHeight / 2;    }

这里的难点是计算扇形的偏移量,从样图可看出,各扇形并不是一一相连,而是有一定的偏移量,偏移量暂且不表,先说说偏移方向,看图:
这里写图片描述

左图,如果扇形往不同方向偏移,就会造成某些饼图相叠,而某些扇形偏离主体较远,那么就看起来很丑,所以要规定某一个方向,而如果方向相同,那么就是整个图的移动,做不出想要的结果;
右图,如果朝每个扇形的中线方向移动,那么,每个扇形与相邻的两个扇形的距离都一样,就不会相叠,且偏移发散后的主体图形外观完整(偏移量不能过多)。

扇形的中心线角度由开始角度startAngle、扫过角度sweepAngle计算后可知。

 /**     * 根据扇形角度计算扇形偏移方向及最终坐标     */    private SectorsData calculateDirectionCoord(float startAngle, float sweepAngle) {        SectorsData sectorsData = new SectorsData();        sectorsData.middleAngle = (startAngle + sweepAngle / 2); // 中间角度,用于计算偏移方向角度        float skewingX; // 偏移x量        float skewingY; // 偏移Y量        // 已经斜边和角度: 角度的对边 = 斜边*sin角度 ; 角度邻边 = 斜边*cos角度        // TODO : 角度转弧度 π/180×角度 【Math.cos、Math.sin参数是弧度 !】        skewingX = (float) (mSkewingLength * Math.cos(sectorsData.middleAngle * Math.PI / 180));        skewingY = (float) (mSkewingLength * Math.sin(sectorsData.middleAngle * Math.PI / 180));        sectorsData.left = mSectorsX - mRadius + skewingX;        sectorsData.top = mSectorsY - mRadius + skewingY;        sectorsData.right = mSectorsX + mRadius + skewingX;        sectorsData.bottom = mSectorsY + mRadius + skewingY;        sectorsData.sectorsX = mSectorsX + skewingX;        sectorsData.sectorsY = mSectorsY + skewingY;        return sectorsData;    }

这里又有另外一个重点:偏移后的扇形圆点坐标会改变,那么偏移量是多少?即x、y改变了多少,怎么算?看图:
这里写图片描述
古语有云:已经斜边和角度,则角度的对边 = 斜边 * sin角度 ,角度的邻边 = 斜边 * cos角度
所以可求x,y的偏移量:

// 角度转弧度:π/180 × 角度 【Math.cos、Math.sin 参数是弧度 !】        skewingX = (float) (mSkewingLength * Math.cos(sectorsData.middleAngle * Math.PI / 180));        skewingY = (float) (mSkewingLength * Math.sin(sectorsData.middleAngle * Math.PI / 180));

这里要注意的是:Math.cos、Math.sin 计算时,参数是弧度,而不是角度(我就被这里坑了,一直计算出来的结果跟自己在计算机计算的结果不同,后面一查,呵呵)

这里提一下:为了突出最大的扇形块,获取一下最大的value值

/**     * 获取 map中 value的最大值     **/    private float getMaxValue(Map<String, Float> map) {        Float max = 0f;        for (Map.Entry<String, Float> entry : map.entrySet()) {            if (entry.getValue() > max) {                max = entry.getValue();            }        }        return max;    }
// 突出最大的块            if (mDataMap.get(key) == maxValue) {                mSkewingLength = 30f;            } else {                mSkewingLength = 10f;            }

模块二:线

画线和文字的思路与画扇形的思路一样,只是线的起始点要基于扇形,文字的落点要基于线。

 /**     * 画线     **/    private void drawLines(Canvas canvas) {        mPaint.setStyle(Paint.Style.STROKE);        mPaint.setStrokeWidth(3f);        mPaint.setColor(Color.WHITE);        mLinesDataList = calculateLinesDatas();        LinesData linesData;        for (int i = 0; i < mLinesDataList.size(); i++) {            linesData = mLinesDataList.get(i);            mPath.moveTo(linesData.startX, linesData.startY);            mPath.lineTo(linesData.turnX, linesData.turnY);            mPath.lineTo(linesData.endX, linesData.endY);            canvas.drawPath(mPath, mPaint);        }    }

线的终点 y 坐标跟线的拐点 / 转折点的 y 坐标一致,这样就能画出水平线;
线的终点 x 坐标以中心点 x 坐标为基点,这样就能做出样图的效果:各线终点保持在同一垂直线上,而根据中心线的角度不同,来计算是放在左边还是右边。

/**     * 计算各线的 Path     **/    private List<LinesData> calculateLinesDatas() {        List<LinesData> linesDataList = new ArrayList<>();        for (int i = 0; i < mDataMap.size(); i++) {            LinesData linesData = new LinesData();            SectorsData sectorsData = mSectorsDataList.get(i);            // 线的起点            float startX;            float startY;            startX = (float) (mRadius * Math.cos(sectorsData.middleAngle * Math.PI / 180));            startY = (float) (mRadius * Math.sin(sectorsData.middleAngle * Math.PI / 180));            linesData.startX = sectorsData.sectorsX + startX;            linesData.startY = sectorsData.sectorsY + startY;            // 线的转折点            float turnX;            float turnY;            turnX = (float) ((mRadius + 50) * Math.cos(sectorsData.middleAngle * Math.PI / 180));            turnY = (float) ((mRadius + 50) * Math.sin(sectorsData.middleAngle * Math.PI / 180));            linesData.turnX = sectorsData.sectorsX + turnX;            linesData.turnY = sectorsData.sectorsY + turnY;            // 线的终点            if (sectorsData.middleAngle > 90 && sectorsData.middleAngle < 270) {                linesData.endX = mSectorsX - mRadius - 100;            } else {                linesData.endX = mSectorsX + mRadius + 100;            }            linesData.endY = linesData.turnY;            linesData.middleAngle = sectorsData.middleAngle;            linesDataList.add(linesData);        }        return linesDataList;    }
 /**     * 线数据类     **/    private class LinesData {        float startX;        float startY;        float turnX;        float turnY;        float endX;        float endY;        float middleAngle;    }

模块三:文字

 /**     * 画文字     **/    private void drawTexts(Canvas canvas) {        mPaint.setStyle(Paint.Style.FILL);        mPaint.setColor(Color.WHITE);        mPaint.setTextSize(30f);        mTextsDataList = calculateTextsDatas();        TextsData textsData;        for (int i = 0; i < mTextsDataList.size(); i++) {            textsData = mTextsDataList.get(i);            mPaint.setTextAlign(textsData.PAINT_ALIGN);            canvas.drawText(textsData.name, textsData.startX, textsData.startY, mPaint);        }        mPaint.setTextAlign(Paint.Align.CENTER); // 设置坐标点在字符的中心        mPaint.setTextSize(60f);        canvas.drawText("饼图", mSectorsX, mSectorsY + mRadius / 2 * 3, mPaint);    }

这里提一点:
依照线的终点来画文字,画文字有一个位置属性可以设置(看源码),可设置从文字的左边 / 中间 / 右边开始画

mPaint.setTextAlign(Paint.Align.LEFT);mPaint.setTextAlign(Paint.Align.CENTER);mPaint.setTextAlign(Paint.Align.RIGHT);

根据中心线的角度不同,来计算是放在左边还是右边。

/**     * 计算各文字的坐标     **/    private List<TextsData> calculateTextsDatas() {        List<TextsData> textsDataList = new ArrayList<>();        for (int i = 0; i < mDataMap.size(); i++) {            TextsData textsData = new TextsData();            LinesData linesData = mLinesDataList.get(i);            // 根据线的终点计算文字的坐标            if (linesData.middleAngle > 90 && linesData.middleAngle < 270) {                textsData.startX = linesData.endX - 10;                textsData.PAINT_ALIGN = Paint.Align.RIGHT;            } else {                textsData.startX = linesData.endX + 10;                textsData.PAINT_ALIGN = Paint.Align.LEFT;            }            textsData.startY = linesData.turnY;            textsDataList.add(textsData);        }        List<String> nameList = new ArrayList(mDataMap.keySet());        for (int i = 0; i < nameList.size(); i++) {            textsDataList.get(i).name = nameList.get(i);        }        return textsDataList;    }
/**     * 文字数据类     **/    private class TextsData {        Paint.Align PAINT_ALIGN;        String name;        float startX;        float startY;    }

最终结果:

最终结果

这个view并不难,写得也不咋滴,还有待优化,但爱咋咋滴 …

完整.java文件下载

原创粉丝点击