饼状图

自定义控件之饼状图

绘制饼状图分析

  1. 定义一个起始角度
  2. 计算每块扇形的弧度
  3. 遍历数据,每一个起始角度,是上一个扇形的结束角度
  4. 扇形的外接矩形的左上右下不需要计算,移动坐标系到屏幕中间

绘制直线

直线的两个要素:

  1. 直线的起点,这里是每块扇形的弧的中心点
  2. 直线的终点,这里是连接圆心和起点的延长线上某个点

起点的计算:

  1. 获取对应扇形的弧度
  2. 扇形起始角度+弧度/2
  3. 半径*角度结果的余弦值,横坐标纵坐标同理

终点的计算:起点计算的第三步将半径增大即可

1
2
3
4
5
6
7
//绘制直线
//Math.toRadians(参数):指的是将参数的弧度转换为角度
float startX = (float) (radius * Math.cos(Math.toRadians(startAngle + sweepAngle / 2)));
float startY = (float) (radius * Math.sin(Math.toRadians(startAngle + sweepAngle / 2)));
float endX = (float) ((radius + 30) * Math.cos(Math.toRadians(startAngle + sweepAngle / 2)));
float endY = (float) ((radius + 30) * Math.sin(Math.toRadians(startAngle + sweepAngle / 2)));
canvas.drawLine(startX, startY, endX, endY, linePaint);

判断点击位置所在的扇形区域

将点击的坐标位置转化为以饼状图中心为原点的坐标,对坐标进行处理,将坐标转化为点击的角度,判断是否处于某一个饼状图所在的角度区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MathUtil {
public static float getTouchAngle(float x, float y) {
float touchAngle = 0;
if (x < 0 && y < 0) { //2 象限
touchAngle += 180;
} else if (y < 0 && x > 0) { //1象限
touchAngle += 360;
} else if (y > 0 && x < 0) { //3象限
touchAngle += 180;
}
//Math.atan(y/x) 返回正数值表示相对于 x 轴的逆时针转角,返回负数值则表示顺时针转角。
//返回值乘以 180/π,将弧度转换为角度。
touchAngle += Math.toDegrees(Math.atan(y / x));
if (touchAngle < 0) {
touchAngle = touchAngle + 360;
}
return touchAngle;
}
}

处理触摸事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//当用户与手机屏幕进行交互的时候(触摸)
//触摸事件处理
//1,按下去
//2,移动
//3,抬起
//参数:触摸事件,这个事件是由用户与屏幕交互产生的,这个事件包含上述三种情况
@Override
public boolean onTouchEvent(MotionEvent event) {
//获取用户对屏幕的行为
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
//做点击范围的认定
//获取用户点击的位置距当前视图的左边缘的距离
float x = event.getX();
float y = event.getY();
//将点击的x和y坐标转换为以饼状图为圆心的坐标
x = x - width / 2;
y = y - height / 2;
float touchAngle = MathUtil.getTouchAngle(x, y);
float touchRadius = (float) Math.sqrt(x * x + y * y);
//判断触摸的点距离饼状图圆心的距离<饼状图对应圆的圆心
if (touchRadius < radius) {
//说明是一个有效点击区域
//查找触摸的角度是否位于起始角度集合中
//binarySearch:参数2在参数1对应的集合中的索引
//未找到,则返回 -(和搜索的值附近的大于搜索值的正确值对应的索引值+1)
//{1,2,3}
//搜索1:返回值1在集合中对应的索引0
//1.2:返回值为 -(1+1) -2
//1.8:返回值 -(1+1) -2
int searchResult = Arrays.binarySearch(startAngles, touchAngle);
if (searchResult < 0) {
position = -searchResult - 1 - 1;
} else {
position = searchResult;
}
Log.i("test", "position:" + position);
//让onDraw方法重新调用:
//重绘
invalidate();
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return super.onTouchEvent(event);
}

定义基本信息载体,即javabean

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PieEntity {
//对应数据占用的份额
public float value;
//对应数据的颜色
public int color;
public PieEntity(float value, int color) {
this.value = value;
this.color = color;
}
}
private int[] colors = {0xFFCCFF00, 0xFF6495ED, 0xFFE32636, 0xFF800000, 0xFF808000, 0xFFFF8C69, 0xFF808080,0xFFE6B800, 0xFF7CFC00};

初始化三个画笔

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void init() {
rectF = new RectF();
//初始化path
path = new Path();
//初始化画笔
paint = new Paint();
paint.setAntiAlias(true);
//创建绘制直线对应的画笔
linePaint = new Paint();
linePaint.setColor(Color.BLACK);
linePaint.setAntiAlias(true);
linePaint.setStrokeWidth(3);
linePaint.setTextSize(20);
touchRectF = new RectF();
}

计算绘制饼状图的外接矩形的左上右下距离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//当自定义控件的尺寸已经决定好的时候回调
//这个方法是在onMeasure方法执行后执行,最终的测量结果已经产生
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//获得当前控件的宽高
this.width = w;
this.height = h;
//为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
int min = Math.min(w, h);
//获得饼状图的半径:宽度和高度的较小值/2*0.7
radius = (int) (min * 0.7f / 2);
//获取饼状图的内接矩形
rectF.left = -radius;
rectF.top = -radius;
rectF.right = radius;
rectF.bottom = radius;
touchRectF.left = -radius - 15;
touchRectF.top = -radius - 15;
touchRectF.right = radius + 15;
touchRectF.bottom = radius + 15;
}

绘制饼状图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
if (mDataList == null)
return;
//保存画布
canvas.save();
//屏幕画布到屏幕中间
canvas.translate(mTotalWidth / 2, mTotalHeight / 2);
//绘制饼图的每块区域
drawPie(canvas);
//还原画布
canvas.restore();
//5,绘制饼状图的步骤:
//起始的角度
float startAngle = 0;
//遍历数据绘制饼状图
for (int i = 0; i < mDataList.size(); i++) {
/************************①绘制扇形****************************/
//每个扇形的角度:数据值占总数据值的百分比
float sweepAngle = mDataList.get(i).getValue() / mTotalValue * 360 - 1;
//移动到初始点
mPath.moveTo(0, 0);
//绘制某一块扇形
mPath.arcTo(mRectF, startAngle, sweepAngle);
//设置颜色
mPaint.setColor(mDataList.get(i).getColor());
//绘制路径
canvas.drawPath(mPath, mPaint);
//重置路径:为了下次再绘制不会和上一次重叠
mPath.reset();
/***************************②绘制指示线*******************************/
//绘制线和文本
//确定直线的起始和结束的点的位置
float startLineX = (float) (mRadius * Math.cos(Math.toRadians(startAngle + sweepAngle / 2)));
float startLineY = (float) (mRadius * Math.sin(Math.toRadians(startAngle + sweepAngle / 2)));
float endLineX = (float) ((mRadius + 30) * Math.cos(Math.toRadians(startAngle + sweepAngle / 2)));
float endLineY = (float) ((mRadius + 30) * Math.sin(Math.toRadians(startAngle + sweepAngle / 2)));
canvas.drawLine(startLineX, startLineY, endLineX, endLineY, mLinePaint);
//记录下一次的起始角度为之前所有绘制的扇形弧度+1
startAngle += sweepAngle + 1;
/***************************③绘制文本*******************************/
float res = mDataList.get(i).getValue() / mTotalValue * 100;
//格式化数据,小数点后保留1位有效数字
String percent = String.format("%.1f", entities.get(i).value / totalValue * 100);
//将弧度转化为角度
float v = startAngle % 360;
//判断如果起始角度>90度并且小于270度
if (startAngle % 360.0 >= 90.0 && startAngle % 360.0 <= 270.0) {
Log.i("test", "resToRound:" + resToRound);
//将文本绘制到文本连接线终点的左边
canvas.drawText(resToRound + "%", endLineX - mTextPaint.measureText(resToRound + "%"), endLineY, mTextPaint);
} else {
//将文本绘制到文本连接线终点所在位置
canvas.drawText(resToRound + "%", endLineX, endLineY, mTextPaint);
}
/****************************************************************/
}

Math

Math类提供了常用的一些数学函数,如:三角函数、对数、指数等。一个数学公式如果想用代码表示,则可以将其拆分然后套用Math类下的方法即可。

方法 功能描述
atan(y/x) 返回正数值表示相对于 x 轴的逆时针转角,返回负数值则表示顺时针转角
toDegrees() 角度转换成弧度
toRadians() 弧度转换成角度
cos() 余弦值
sin() 正弦值
tan() 正切
sqrt() 开平方
min() 最小值
max() 最大值
abs() 绝对值
pow(x,y) x的y次幂
floor() 向下取整
ceil() 向上取整
hypot(x, y) x和y平方和的二次方根
random() 随机返回[0,1)之间的无符号double值
rint() 返回最接近这个数的整数,如果刚好居中,则取偶数
round() 与rint相似,返回值为long
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
Math.abs(12.3); //12.3 返回这个数的绝对值
Math.abs(-12.3); //12.3
Math.copySign(1.23, -12.3); //-1.23,返回第一个参数的量值和第二个参数的符号
Math.copySign(-12.3, 1.23); //12.3
Math.signum(x); //如果x大于0则返回1.0,小于0则返回-1.0,等于0则返回0
Math.signum(12.3); //1.0
Math.signum(-12.3); //-1.0
Math.signum(0); //0.0
//指数
Math.exp(x); //e的x次幂
Math.expm1(x); //e的x次幂 - 1
Math.scalb(x, y); //x*(2的y次幂)
Math.scalb(12.3, 3); //12.3*2³
//取整
Math.ceil(12.3); //返回最近的且大于这个数的整数13.0
Math.ceil(-12.3); //-12.0
Math.floor(12.3); //返回最近的且小于这个数的整数12.0
Math.floor(-12.3); //-13.0
//x和y平方和的二次方根
Math.hypot(x, y); //√(x²+y²)
//返回概述的二次方根
Math.sqrt(x); //√(x) x的二次方根
Math.sqrt(9); //3.0
Math.sqrt(16); //4.0
//返回该数的立方根
Math.cbrt(27.0); //3
Math.cbrt(-125.0); //-5
//对数函数
Math.log(e); //1 以e为底的对数
Math.log10(100); //10 以10为底的对数
Math.log1p(x); //Ln(x+ 1)
//返回较大值和较小值
Math.max(x, y); //返回x、y中较大的那个数
Math.min(x, y); //返回x、y中较小的那个数
//返回 x的y次幂
Math.pow(x, y);
Math.pow(2, 3); //即2³ 即返回:8
//随机返回[0,1)之间的无符号double值
Math.random();
//返回最接近这个数的整数,如果刚好居中,则取偶数
Math.rint(12.3); //12.0
Math.rint(-12.3); //-12.0
Math.rint(78.9); //79.0
Math.rint(-78.9); //-79.0
Math.rint(34.5); //34.0
Math.rint(35.5); //36.0
Math.round(12.3); //与rint相似,返回值为long
//三角函数
Math.sin(α); //sin(α)的值
Math.cos(α); //cos(α)的值
Math.tan(α); //tan(α)的值
//求角
Math.asin(x/z); //返回角度值[-π/2,π/2] arc sin(x/z)
Math.acos(y/z); //返回角度值[0~π] arc cos(y/z)
Math.atan(y/x); //返回角度值[-π/2,π/2]
Math.atan2(y-y0, x-x0); //同上,返回经过点(x,y)与原点的的直线和经过点(x0,y0)与原点的直线之间所成的夹角
Math.sinh(x); //双曲正弦函数sinh(x)=(exp(x) - exp(-x)) / 2.0;
Math.cosh(x); //双曲余弦函数cosh(x)=(exp(x) + exp(-x)) / 2.0;
Math.tanh(x); //tanh(x) = sinh(x) / cosh(x);
//角度弧度互换
Math.toDegrees(angrad); //角度转换成弧度,返回:angrad * 180d / PI
Math.toRadians(angdeg); //弧度转换成角度,返回:angdeg / 180d * PI

角度与弧度

角的度量单位通常有两种,一种是角度制,另一种就是弧度制。

角度制,就是用角的大小来度量角的大小的方法。在角度制中,我们把周角的1/360看作1度,那么,半周就是180度,一周就是360度。由于1度的大小不因为圆的大小而改变,所以角度大小是一个与圆的半径无关的量。

弧度制,顾名思义,就是用弧的长度来度量角的大小的方法。单位弧度定义为圆周上长度等于半径的圆弧与圆心构成的角。由于圆弧长短与圆半径之比,不因为圆的大小而改变,所以弧度数也是一个与圆的半径无关的量。角度以弧度给出时,通常不写弧度单位,有时记为rad或R。

根据弧度的定义,以长为圆周长(2πr)的弧所对的圆心角为2π 弧度,半个圆周长的弧所对的圆心角为π 弧度。
于是,角度与弧度间换算关系就十分明了了。因为360度=2π,所以,1度=π/180≈0.01745弧度,1弧度=180/π≈57.3度。

源代码:https://github.com/JackChan1999/PieChart

坚持原创技术分享,您的支持将鼓励我继续创作!