前言

最近项目中遇到一个需求,需要一个饼状图,显示百分比,点击每一个扇形区域可以切换下面列表的数据。拿到这个需求后首先想到了MPAndroidChart等第三方库,这个库中包含了各种各样的图表,冷静下来一想,整个项目中就这一个地方用到,那么引入这个库必然会增大项目的体积。所以呢,还是自己搞一个算了。、

效果图

先看一下 最终的效果图:

android绘制简单饼状图 安卓饼状图_Math

设计思路

看了效果图,是不是感觉还不错。 其实实现起来还是挺简单的,先来理清楚思路,思路理清楚了,相信你也可以的。

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;
}
}

扇形的绘画图解

android绘制简单饼状图 安卓饼状图_连线_02

扇形周围短线和百分比的绘制逻辑

android绘制简单饼状图 安卓饼状图_连线_03

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轴正方向的夹角 计算逻辑

android绘制简单饼状图 安卓饼状图_android 封装view_04

使用

大功告成,下面看一下怎么使用。很简单,跟普通的view使用差不多。

xml中设置控件的大小

android:id="@+id/pie_chart"
android:layout_width="match_parent"
android:layout_height="190dp" />
在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,自定义的饼状图完成。有哪些做的不合适的地方,希望大家多多指点。