自定义View之扇形统计图
大家好,今天我们一起来做个扇形统计图,目前我在自定义view方面还是一个小白,如有错误的地方,请大家及时指出,我们共同成长。
国际惯例,先上UI妹子做的效果图:

在上我们自己绘制的效果图:


这里,我们就要一步一步的去实现它了!!!
思路
1.自定义属性。
2.测量宽和高(规定我们所需要的区域)。
3.制图(绘制扇形、绘制扇形上的线段、绘制文本)。
下面我们就根据以上3个步骤去实现它。
1.自定义属性
在value文件夹下面新建attrs.xml
<resources>
<declare-styleable name="CircleView">
<!--字体大小-->
<attr name="circle_text_size" format="dimension"></attr>
<!--同心圆之间的距离-->
<attr name="concentric_circle_space" format="dimension"></attr>
<!--外圈和屏幕直接的距离-->
<attr name="out_screen_space" format="dimension"></attr>
<!--外圈画笔宽度-->
<attr name="out_paint_width" format="dimension"></attr>
</declare-styleable>
</resources>
然后在重写的构造方法里面去获取我们自定义的属性并且给定他们初始值。
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
textSize=array.getDimension(R.styleable.CircleView_circle_text_size,
dip2px(context, 14));
concentricCircleSpace=array.getDimension(R.styleable.CircleView_concentric_circle_space,
dip2px(context, 120));
outScreenSpace=array.getDimension(R.styleable.CircleView_out_screen_space,
dip2px(context, 110));
outPaintWidth=array.getDimension(R.styleable.CircleView_out_paint_width,
dip2px(context, 2));
array.recycle();
/**
* 初始化数据
*/
//外层弧
mPaintOut = new Paint();
mPaintOut.setAntiAlias(true);
mPaintOut.setColor(getResources().getColor(R.color.gray));
mPaintOut.setStyle(Paint.Style.STROKE);
mPaintOut.setStrokeCap(Paint.Cap.ROUND);
mPaintOut.setStrokeWidth(outPaintWidth);
//内部扇形
mPaintCurrent = new Paint();
mPaintCurrent.setAntiAlias(true);
//设置为填充
mPaintCurrent.setStyle(Paint.Style.FILL);
mPaintCurrent.setStrokeCap(Paint.Cap.ROUND);
//字体
mPaintText = new Paint();
mPaintText.setAntiAlias(true);
mPaintText.setStyle(Paint.Style.STROKE);
mPaintText.setTextSize(textSize);
2测量
在onMeasure()方法里面进行测量,来规定我们需要的区域大小。这里为了方便后续的计算过程,我们将区域设置为正方形。
/**
* 测量所需区域的宽和高
*/
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
//正方形
int size = width > height ? height : width;
setMeasuredDimension(size, size);
3制图
制图我们又分为三部分,他们分别为:
1.绘制扇形
2.绘制线段(偏移线段和水平线段)
3.绘制文本
1.绘制扇形
看到设计图,是根据数值的大小颜色和扇形的角度都不一样,我们可以让它遍历去绘制,从0度开始,顺时针依次去绘制,直到绘制完成。
在绘制的时候,我们需要知道每一条数据所对应的角度,在这里双手奉上一张图,聪明的你们看过之后肯定会明白的。

通过上面这个公式,我们就可以计算出每条数据在一个圆中对应的角度,但是我们只是算出了每条数据所对应的扇形,怎么把他们集中展示在一个圆中呢?我们就可以以第一个扇形的角度为第二个扇形的起始角度,这样就把他们绘制在同一个圆中了。
//规定所需的区域大小,内层
RectF rCurrent = new RectF(interSpace, interSpace,
getWidth() - interSpace, getHeight() - interSpace);
mPaintCurrent.setColor(getResources().getColor(R.color.white));
canvas.drawRect(rCurrent, mPaintCurrent);
for (int i = 0; i < listData.size(); i++) {
mPaintCurrent.setColor(colors[i % colors.length]);
currentAngle = 360 * listData.get(i).getItemValue() / totalNum;
//画内部的扇形
canvas.drawArc(rCurrent, startAngle, currentAngle, true, mPaintCurrent);
startAngle = currentAngle + startAngle;
}
//规定所需的区域大小,外层
RectF rOut = new RectF(dirts, dirts,
getWidth() - dirts, getHeight() - dirts);
mPaintOut.setColor(Color.GRAY);
//画外层弧
canvas.drawArc(rOut, 0, 360, false, mPaintOut);
2.绘制线段
我们先回顾一下初中数学知识

我们可以在效果图上可以看到,每个线段的起点是该扇形弧的中点,所以我们的首要任务就是计算每个中点的坐标,然后根据这个坐标我们就可以依次算出偏移线段和水平线段以及文字的坐标。
2.1 计算扇形弧的中点坐标
这里我们就要用到我给的那张图里面的知识了,为了更好的理解,我在这里给一张示意图吧!!!双手奉上

由上图我们可以看出,我们现在要计算a点的坐标,在这个图中我们已知的数值有:半径r的大小,半径r与x轴形成的夹角ao的大小。
下面我们就计算点a的坐标:
横坐标:x1 = x0 + r * cos( ao * 2 * PI /360 )
纵坐标:y1 = y0 + r * sin( ao * 2 * PI /360 )
这里就有同学要问我了,为什么要乘(2 * PI / 360)呢,这是因为:
这两个函数中的 X 都是指的 “弧度” 而非 “角度”,弧度的计算公式为: 2 * PI / 360 * 角度;
30° 角度 的弧度 = 2 * PI / 360 * 30
因为我们算出的是关于直角坐标系中的坐标,但是我们是要把他放到屏幕的坐标系中,这里我又得献丑了,双手奉上美图。

由此图可以看出,该点在屏幕中所对应的坐标应为:
(x0=(圆心距离左边边的距离 + x1) , y0=(圆心距离上边的距离 + y1))
因为我们在测量的时候把我们需要的区域给规定为一个正方形,圆心正好在正方形的中心位置,这样我们就能算出圆心到四个边的距离了,他们为:
int left = getWidth() / 2;
int top = getWidth() / 2;
int right = getWidth() / 2;
int bottom = getWidth() / 2;
在直角坐标系中,我们分了四个象限,他们之间的值有正有负,这样我们就可以根据这个特性来进行处理了,就比如我们现在计算的坐标值在第三象限,对应的屏幕里面就应该显示到左下这块位置,即为屏幕宽度的一半减去改点的横坐标(取绝对值)为该点在屏幕中的横坐标,屏幕宽度的一半加上该点的纵坐标为该点在屏幕中的纵坐标。这样我们就可以算出每个点在屏幕中的坐标了。
//当前圆弧中点对应的角度
float angle = cAngle / 2 + sAngle;
float dx = (float) (Math.cos(2 * angle * Math.PI / 360) * radius);
float dy = (float) (Math.sin(2 * angle * Math.PI / 360) * radius);
2.2 绘制偏移线段
在计算出每个中点的坐标以后,我们只需让他偏移一点距离,绘制出一条线段即可,需要注意的是,在一四象限的往右偏移,在二三象限的往右偏移。
/**
* 第一象限 x > 0 && y < 0
* 第二象限 x < 0 && y < 0
* 第三象限 x < 0 && y > 0
* 第四象限 x > 0 && y > 0
*
* && 并且
* || 或者
*/
if (dx > 0 && dy < 0) {
//右上
startX = point + Math.abs(dx);
startY = point - Math.abs(dy);
endX = startX + offsetX;
endY = startY - offsetY;
} else if (dx < 0 && dy < 0) {
//左上
startX = point - Math.abs(dx);
startY = point - Math.abs(dy);
endX = startX - offsetX;
endY = startY - offsetY;
} else if (dx < 0 && dy > 0) {
//左下
startX = point - Math.abs(dx);
startY = point + Math.abs(dy);
endX = startX - offsetX;
endY = startY + offsetY;
} else if (dx > 0 && dy > 0) {
//右下
startX = point + Math.abs(dx);
startY = point + Math.abs(dy);
endX = startX + offsetX;
endY = startY + offsetY;
}
// 画偏移线
canvas.drawLine(startX, startY, endX, endY, mPaintOut);
2.3 绘制水平线段
绘制完偏移线段后我们就要绘制平行线段了,看效果图不难发现,水平线段的开始坐标即为偏移线段的结束坐标,这就好办了,我们就可以绘制出水平线段。
/**
* 第一象限 x > 0 && y < 0
* 第二象限 x < 0 && y < 0
* 第三象限 x < 0 && y > 0
* 第四象限 x > 0 && y > 0
*
* && 并且
* || 或者
*/
if (dx > 0 && dy < 0) {
//右上
parX = endX + offset;
parY = endY;
} else if (dx < 0 && dy < 0) {
//左上
parX = endX - offset;
parY = endY;
} else if (dx < 0 && dy > 0) {
//左下
parX = endX - offset;
parY = endY;
} else if (dx > 0 && dy > 0) {
//右下
parX = endX + offset;
parY = endY;
}
//画水平线
canvas.drawLine(endX, endY, parX, parY, mPaintOut);
至此,我们绘制线段的工作就做完了!!!
3.绘制文本
我们依旧看看效果图,发现文字是跟在水平线段的后面的,只不过稍微偏移了一点距离,相信聪明的你们肯定知道他的坐标了吧,这样我们也就绘制出文字了,这里还是需要注意的是一四象限往右绘制,二三象限往左绘制。
注意:由于文字的绘制是从左往右的,因此我们在左半部分绘制文字的时候,应该取文字的宽度 + 偏移量为起始点来绘制文字。
//文字宽度
float textWidth = mPaintText.measureText(title, 0, title.length());
/**
* 第一象限 x > 0 && y < 0
* 第二象限 x < 0 && y < 0
* 第三象限 x < 0 && y > 0
* 第四象限 x > 0 && y > 0
*
* && 并且
* || 或者
*/
if (dx > 0 && dy < 0) {
//右上
textX = parX + offsetText;
textY = parY;
} else if (dx < 0 && dy < 0) {
//左上
textX = parX - offsetText - textWidth;
textY = parY;
} else if (dx < 0 && dy > 0) {
//左下
textX = parX - offsetText - textWidth;
textY = parY;
} else if (dx > 0 && dy > 0) {
//右下
textX = parX + offsetText;
textY = parY;
}
//画文字
canvas.drawText(title, textX, textY, mPaintText);
做到这里,恭喜大家,我们的扇形统计图就最好了,赶紧运行到手机上来欣赏一下吧,哈哈
总结
经过上述的步骤后我们就绘制出一个炫酷的扇形统计图,不难发现,本次绘制的主要难点有一下几点:
1 绘制同心圆
2 计算扇形弧的中点坐标(重点)
3 把计算出来的坐标对应到屏幕坐标中
4 绘制线段和文本
通过这个列子我们可以看得出来,在复杂的view,它也是由许多个小view组合起来的,只要我们的把它一步一步的拆开,那么它就会变得非常简单了。
这个项目我已上传到github上了,还请各位小伙伴提出宝贵的意见,源码传送门:https://github.com/liuxinggen/CircleView