曰:这文章写得很不咋地,但是却是自己“开悟”的记录,不想浑浑噩噩,首先不去浑浑噩噩!
前两天看到朱凯大神发表了酝酿一整年的大作:《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并不难,写得也不咋滴,还有待优化,但爱咋咋滴 …