自定义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文件下载
- 自定义View:饼图/扇形图(PieView)
- Android自定义view实现扇形
- 自定义view之扇形统计图
- 自定义View之案列篇(二):扇形菜单
- Android 自定义view之扇形菜单(上)
- Android 自定义view之扇形菜单(中)
- 自定义PieView实战
- 自定义饼状图控件PieView
- Android自定义View之扇形饼状图
- Android自定义View之扇形饼状图
- 自定义view-仿移动扇形进度条
- Android自定义View之直方图和扇形图——ChartView
- Android菜鸟练习第四课 自定义PieView实现饼图效果
- Android自定义View-绘制扇形实现圆形进度
- Android开发-自定义View-AndroidStudio(三)扇形多级菜单
- Android 自定义View 慢慢画一个不同颜色扇形的圆,点击圆上不同颜色扇形区域返回不同颜色
- 自定义扇形菜单
- android 自定义扇形
- Linux操作系统故障排除-centos6口令丢失
- 小电容,大作用(一)
- #Verilog 设计和验证基础知识
- 自定义ProgressDialog
- 百度笔试题:malloc/free与new/delete的区别 .
- 自定义View:饼图/扇形图(PieView)
- html table 单元格合并
- angular限制input框输入金额(是小数的话只保留两位小数点)
- Java基础补习Day3
- AQS源码分析之独占锁和共享锁
- three.js的各种材质
- 多年iOS开发经验总结(一)
- Linux下查看MySQL的安装路径
- eclipse反编译插件安装