前言
最近项目中遇到一个需求,需要一个饼状图,显示百分比,点击每一个扇形区域可以切换下面列表的数据。拿到这个需求后首先想到了MPAndroidChart等第三方库,这个库中包含了各种各样的图表,冷静下来一想,整个项目中就这一个地方用到,那么引入这个库必然会增大项目的体积。所以呢,还是自己搞一个算了。、
效果图
先看一下 最终的效果图:
设计思路
看了效果图,是不是感觉还不错。 其实实现起来还是挺简单的,先来理清楚思路,思路理清楚了,相信你也可以的。
1.创建类集成View,实现onDraw。这里类名为MyPieChart。
2.将每个扇形封装成一个类,也可以说是对象,这里命名为PieEntry。其中包含三个元素:数值、颜色、是否被选中、起始角度、结束角度(用于点击事件)。
3.创建一个init方法来初始化Paint。并在构造方法中调用init方法
4.在onDraw方法中画图。也就是逐个的画扇形。
- 计算总值,也就是遍历List
,将每个PieEntry的数值相加。 - 获取中心点坐标和两个半径,一个是被选中的半径稍微大一点,另一个是未选中的坐标稍微小一点。
- 再次遍历List
画出每个扇形,在这个循环体内,我们需要计算出当前扇形的角度,然后累加的起始角度,作为下一个扇形的起始角度。并在这个循环体内画出扇形。然后画出外围显示的百分比。
5.重写onTouchEvent方法,拦截ACTION_DOWN状态。得到点击的坐标,判断该点是否小于半径,如果大于半径则不处理,如果小于半径则计算该点和圆心的连线与x正方形的夹角,最后再遍历List判断该夹角在那个扇形区域中,由此将点击时间回掉出去。
大题思路就是这样,文字描述可能还描述的不够明白,下面看代码怎么一步一步实现。
具体代码实现
1.创建MyPieChart类和PieEntry类。
public class MyPieChart extends View {
private List pieEntries;
private Paint paint; //画笔
private float centerX; //中心点 x坐标
private float centerY; //中心点 y坐标
private float radius; //未选中状态的半径
private float sRadius; //选中状态的半径
/**
* 每个扇形的对象
*/
public static class PieEntry {
private float number; //数值
private int colorRes; //颜色资源
private boolean selected; //是否选中
private float startC; //对应扇形起始角度
private float endC; //对应扇形结束角度
public PieEntry(int number, int colorRes, boolean selected) {
this.number = number;
this.colorRes = colorRes;
this.selected = selected;
}
public float getStartC() {
return startC;
}
public void setStartC(float startC) {
this.startC = startC;
}
public float getEndC() {
return endC;
}
public void setEndC(float endC) {
this.endC = endC;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean selected) {
this.selected = selected;
}
public float getNumber() {
return number;
}
public void setNumber(float number) {
this.number = number;
}
public int getColorRes() {
return colorRes;
}
public void setColorRes(int colorRes) {
this.colorRes = colorRes;
}
}
}
2. onDraw方法的具体实现。
这里是onDraw方法的具体实现,代码中有详细的注释。其中涉及到一些三角函数知识,如果看不明白的话,可以参考下面的图示。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//计算总值
int total = 0;
for (int i = 0; i < pieEntries.size(); i++) {
total += pieEntries.get(i).getNumber();
}
//刷新中心点 和半径
centerX = getPivotX();
centerY = getPivotY();
if (sRadius == 0) { //这里做个判断,如果没有通过setRadius方法设置半径,则半径为真个view最小边的一半
sRadius = (getWidth() > getHeight() ? getHeight() / 2 : getWidth() / 2);
}
//计算出两个状态的半径,这里二者相差5dp.
radius = sRadius - DensityUtils.dp2px(getContext(), 5);
//其实角度设置为0,即x轴正方形
float startC = 0;
//遍历List 开始画扇形
for (int i = 0; i < pieEntries.size(); i++) {
//计算当前扇形扫过的角度
float sweep = 360 * (pieEntries.get(i).getNumber() / total);
//设置当前扇形的颜色
paint.setColor(getResources().getColor(pieEntries.get(i).colorRes));
//判断当前扇形是否被选中,确定用哪个半径
float radiusT;
if (pieEntries.get(i).isSelected()) {
radiusT = sRadius;
} else {
radiusT = radius;
}
//画扇形的方法
RectF rectF = new RectF(centerX - radiusT, centerY - radiusT, centerX + radiusT, centerY + radiusT);
canvas.drawArc(rectF, startC, sweep, true, paint);
//下面是画扇形外围的 短线和百分数值。
float arcCenterC = startC + sweep / 2; //当前扇形弧线的中间点和圆心的连线 与 起始角度的夹角
float arcCenterX = 0; //当前扇形弧线的中间点 的坐标 x 以此点作为短线的起始点
float arcCenterY = 0; //当前扇形弧线的中间点 的坐标 y
float arcCenterX2 = 0; //这两个点作为短线的结束点
float arcCenterY2 = 0;
//百分百数字的格式
DecimalFormat numberFormat = new DecimalFormat("00.00");
paint.setColor(Color.BLACK);
//分象限 利用三角函数 来求出每个短线的起始点和结束点,并画出短线和百分比。
//具体的计算方法看下面图示介绍
if (arcCenterC >= 0 && arcCenterC < 90) {
arcCenterX = (float) (centerX + radiusT * Math.cos(arcCenterC * Math.PI / 180));
arcCenterY = (float) (centerY + radiusT * Math.sin(arcCenterC * Math.PI / 180));
arcCenterX2 = (float) (arcCenterX + DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180));
arcCenterY2 = (float) (arcCenterY + DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180));
canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint);
canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", arcCenterX2, arcCenterY2 + paint.getTextSize() / 2, paint);
} else if (arcCenterC >= 90 && arcCenterC < 180) {
arcCenterC = 180 - arcCenterC;
arcCenterX = (float) (centerX - radiusT * Math.cos(arcCenterC * Math.PI / 180));
arcCenterY = (float) (centerY + radiusT * Math.sin(arcCenterC * Math.PI / 180));
arcCenterX2 = (float) (arcCenterX - DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180));
arcCenterY2 = (float) (arcCenterY + DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180));
canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint);
canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2 + paint.getTextSize() / 2, paint);
} else if (arcCenterC >= 180 && arcCenterC < 270) {
arcCenterC = 270 - arcCenterC;
arcCenterX = (float) (centerX - radiusT * Math.sin(arcCenterC * Math.PI / 180));
arcCenterY = (float) (centerY - radiusT * Math.cos(arcCenterC * Math.PI / 180));
arcCenterX2 = (float) (arcCenterX - DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180));
arcCenterY2 = (float) (arcCenterY - DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180));
canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint);
canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", (float) (arcCenterX2 - paint.getTextSize() * 3.5), arcCenterY2, paint);
} else if (arcCenterC >= 270 && arcCenterC < 360) {
arcCenterC = 360 - arcCenterC;
arcCenterX = (float) (centerX + radiusT * Math.cos(arcCenterC * Math.PI / 180));
arcCenterY = (float) (centerY - radiusT * Math.sin(arcCenterC * Math.PI / 180));
arcCenterX2 = (float) (arcCenterX + DensityUtils.dp2px(getContext(), 10) * Math.cos(arcCenterC * Math.PI / 180));
arcCenterY2 = (float) (arcCenterY - DensityUtils.dp2px(getContext(), 10) * Math.sin(arcCenterC * Math.PI / 180));
canvas.drawLine(arcCenterX, arcCenterY, arcCenterX2, arcCenterY2, paint);
canvas.drawText(numberFormat.format(pieEntries.get(i).getNumber() / total * 100) + "%", arcCenterX2, arcCenterY2, paint);
}
//将每个扇形的起始角度 和 结束角度 放入对应的对象
pieEntries.get(i).setStartC(startC);
pieEntries.get(i).setEndC(startC + sweep);
//将当前扇形的结束角度作为下一个扇形的起始角度
startC += sweep;
}
}
扇形的绘画图解
扇形周围短线和百分比的绘制逻辑
3. onTouchEvent方法的具体实现。
该方法主要是监听点击事件,从获取哪个扇形被点击到了。
@Override
public boolean onTouchEvent(MotionEvent event) {
float touchX;
float touchY;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchX = event.getX(); //touch点的坐标
touchY = event.getY();
//判断touch点到圆心的距离 是否小于半径
if (Math.pow(touchX - centerX, 2) + Math.pow(touchY - centerY, 2) <= Math.pow(radius, 2)) {
//计算 touch点和圆心的连线 与 x轴正方向的夹角
float touchC = getSweep(touchX, touchY);
//遍历 List 判断touch点在哪个扇形中
for (int i = 0; i < pieEntries.size(); i++) {
if (touchC >= pieEntries.get(i).getStartC() && touchC < pieEntries.get(i).getEndC()) {
pieEntries.get(i).setSelected(true);
if (listener != null)
listener.onItemClick(i); //将被点击的扇形id回调出去
} else {
pieEntries.get(i).setSelected(false);
}
}
invalidate();//刷新画布
}
break;
}
return super.onTouchEvent(event);
}
/**
* 获取 touch点/圆心连线 与 x轴正方向 的夹角
*
* @param touchX
* @param touchY
*/
private float getSweep(float touchX, float touchY) {
float xZ = touchX - centerX;
float yZ = touchY - centerY;
float a = Math.abs(xZ);
float b = Math.abs(yZ);
double c = Math.toDegrees(Math.atan(b / a));
if (xZ >= 0 && yZ >= 0) {//第一象限
return (float) c;
} else if (xZ <= 0 && yZ >= 0) {//第二象限
return 180 - (float) c;
} else if (xZ <= 0 && yZ <= 0) {//第三象限
return (float) c + 180;
} else {//第四象限
return 360 - (float) c;
}
}
touch点和圆心连线 与 x轴正方向的夹角 计算逻辑
使用
大功告成,下面看一下怎么使用。
很简单,跟普通的view使用差不多
xml中设置控件的大小
在java代码中使用
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// TODO: add setContentView(...) invocation
setContentView(R.layout.xxx);
MyPieChart pieChart = (MyPieChart) findViewById(R.id.pie_chart);
pieChart.setRadius(DensityUtils.dp2px(getContext(), 75));
pieChart.setOnItemClickListener(new MyPieChart.OnItemClickListener() {
@Override
public void onItemClick(int position) {
}
});
List pieEntries = new ArrayList<>();
pieEntries.add(new MyPieChart.PieEntry(1, R.color.chart_orange, true));
pieEntries.add(new MyPieChart.PieEntry(2, R.color.chart_green, false));
pieEntries.add(new MyPieChart.PieEntry(3, R.color.chart_blue, false));
pieEntries.add(new MyPieChart.PieEntry(4, R.color.chart_purple, false));
pieEntries.add(new MyPieChart.PieEntry(5, R.color.chart_mblue, false));
pieEntries.add(new MyPieChart.PieEntry(6, R.color.chart_turquoise, false));
pieChart.setPieEntries(pieEntries);
}
Ok,自定义的饼状图完成。有哪些做的不合适的地方,希望大家多多指点。 有需要源码的小伙伴,留下邮箱即可。
补充
demo地址:https://github.com/chaohengxing/MyPieChartDemo.git