这里用一个案例来将之前学过的关于绘制相关的东东加强巩固一下,纯绘制,木有加点击效果,先来看下最终效果:

github中这种百分比饼图的效果非常非常之多,实际在项目中开发当产品有这样类似的需求时做为开发着的我们第一想法可能就是先去找开源的,然后基于开源的进行适当修改修改就变成自己的了,但是往往去修改开源的项目是比较费时的,而如果不了解其原理可能折腾半天最终发现还不如自己从头继承View按自己的思路来实现,所以有必要自己从头到尾一点点去实现类似的效果,当然上面的效果是比较一般的,重在综合练习,巩固基础,从图上可以看出主要是练习:圆弧的绘制、线的绘制、文字的绘制,话不多说下面看下怎么一点点去实现它:

饼状图的数据处理

对于这个百分比饼图的数据源应该是动态由用户决定的,所以首先将数据进行封装一下,如下:

然后搭建基础框架,新建一个自定义View类:

将其声明在布局文件中:

在Activity中使用View:

扇形的外接矩形的处理

在正式绘制之前,先来做一个分析:

而要绘制这个圆,实际上它是绘制在如下外切矩形之内的:

所以在正式绘制扇形之前首先得计算一下外接矩形的左上右下位置,但是有一个关键步骤需要首先去做,那就是关于绘制坐标系的问题,如下:

但是,这样计算这个外接矩形的坐标就比较麻烦,有没有简便一点的办法呢?可以将绘制坐标做如下移动:

那这样做的好处?好处大大滴,如下:

所以下面首先来将坐标移动到屏幕的中心,而由于我们自定义View在布局文件中的声明是:

那View中如何知道该控件的大小呢?这里学习一个新的api:

有了宽高之后,则可以移动绘制的坐标系了,这时就用到了之前学的api了:

接下来就是来计算我们要绘制的圆的半径了【因为只要确定了半径我们的矩形位置就可能确定了】,为了让圆刚好显示在屏幕上,应该这样来确定圆的半径:

1、首先取屏幕宽高的最少值;

对应代码:

2、确定圆的直径:在最少值中为了让其圆左右有一些间隙已变之后能有空间去在圆上绘制文本,则应该只取其长度的7成;

3、基于直径/2得到半径;

这时就可以定义外接矩形的坐标了:

扇形的绘制处理

有了外接矩形接着就开始扇形的绘制了,首先需要遍历数据一个个进行绘制,如下:

首先设置一下扇形的颜色,当然就需要用到Paint对象啦:

  1. public class PieView extends View {
  2.  
  3. /* 数据源,由外部传过来 */
  4. private List<PieEntity> pieEntities;
  5. /* 控件的大小 */
  6. private int height, width;
  7. /* 圆的半径 */
  8. private int radius;
  9. /* 扇形组成圆形的外接短形 */
  10. private RectF rectF;
  11. /* 绘制扇形的画笔 */
  12. private Paint paint;
  13.  
  14. public PieView(Context context) {
  15. this(context, null);
  16. }
  17.  
  18. public PieView(Context context, @Nullable AttributeSet attrs) {
  19. this(context, attrs, 0);
  20. }
  21.  
  22. public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  23. super(context, attrs, defStyleAttr);
  24. init();
  25. }
  26.  
  27. private void init() {
  28. rectF = new RectF();
  29.  
  30. paint = new Paint();
  31. paint.setAntiAlias(true);
  32. }
  33.  
  34. //当自定义控件的尺寸已经决定好的时候回调
  35. @Override
  36. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  37. super.onSizeChanged(w, h, oldw, oldh);
  38. //初始化控制的宽高
  39. this.width = w;
  40. this.height = h;
  41.  
  42. //为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
  43. int min = Math.min(this.width, this.height);
  44. this.radius = (int) ((min * 0.7f) / 2);//圆的半径取屏幕的7成
  45. rectF.left = -radius;
  46. rectF.top = -radius;
  47. rectF.right = radius;
  48. rectF.bottom = radius;
  49. }
  50.  
  51. @Override
  52. protected void onDraw(Canvas canvas) {
  53. super.onDraw(canvas);
  54. canvas.save();//由于用到了translate画片所以需要save一下
  55. //1、将画布的坐标系移到屏幕中间,这样方便去绘制圆
  56. canvas.translate(this.width / 2, this.height / 2);
  57. //2、绘制扇形
  58. drawPie(canvas);
  59. canvas.restore();
  60. }
  61.  
  62. private void drawPie(Canvas canvas) {
  63. for (int i = 0; i < pieEntities.size(); i++) {
  64. PieEntity pieEntity = pieEntities.get(i);
  65. paint.setColor(pieEntity.getColor());//设置扇形的颜色
  66. }
  67. }
  68.  
  69. /**
  70. * 设置饼图的源数据
  71. */
  72. public void setData(List<PieEntity> pieEntities) {
  73. this.pieEntities = pieEntities;
  74. }
  75. }

接下来绘制扇形,具体如何绘制扇形这个在当时画人脸已经有说过绘制一个弧【http://www.cnblogs.com/webor2006/p/7341697.html】,这里需要用到Path了,而具体会用到Path中的这个方法:

而startAngle和sweepAngle这俩参数还是未知的,在正式调用绘制扇形之前先来解决这两个参数:

startAngle:这个默认从0度开始,所以先声明:

但是每次循环之后,起始角度则为当前扇形的终点角度,如下:

而一个扇形的结束角度是需要知道sweepAngle才能得到它,公式是:startAngle+sweepAngle,所以下面看下sweepAngle怎么计算。

sweepAngle:每个扇形的角度由应该为:当前扇形的比例(当前扇形数据中的值/整个数据值的总数) * 360度,而当前扇形数据中的值为:

而整个数据值的总和则应该进么数据遍历得到:

  1. public class PieView extends View {
  2.  
  3. /* 数据源,由外部传过来 */
  4. private List<PieEntity> pieEntities;
  5. /* 控件的大小 */
  6. private int height, width;
  7. /* 圆的半径 */
  8. private int radius;
  9. /* 扇形组成圆形的外接短形 */
  10. private RectF rectF;
  11. /* 绘制扇形的画笔 */
  12. private Paint paint;
  13. /* 总占比 */
  14. private int totalValue;
  15.  
  16. public PieView(Context context) {
  17. this(context, null);
  18. }
  19.  
  20. public PieView(Context context, @Nullable AttributeSet attrs) {
  21. this(context, attrs, 0);
  22. }
  23.  
  24. public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  25. super(context, attrs, defStyleAttr);
  26. init();
  27. }
  28.  
  29. private void init() {
  30. rectF = new RectF();
  31.  
  32. paint = new Paint();
  33. paint.setAntiAlias(true);
  34. }
  35.  
  36. //当自定义控件的尺寸已经决定好的时候回调
  37. @Override
  38. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  39. super.onSizeChanged(w, h, oldw, oldh);
  40. //初始化控制的宽高
  41. this.width = w;
  42. this.height = h;
  43.  
  44. //为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
  45. int min = Math.min(this.width, this.height);
  46. this.radius = (int) ((min * 0.7f) / 2);//圆的半径取屏幕的7成
  47. rectF.left = -radius;
  48. rectF.top = -radius;
  49. rectF.right = radius;
  50. rectF.bottom = radius;
  51. }
  52.  
  53. @Override
  54. protected void onDraw(Canvas canvas) {
  55. super.onDraw(canvas);
  56. canvas.save();//由于用到了translate画片所以需要save一下
  57. //1、将画布的坐标系移到屏幕中间,这样方便去绘制圆
  58. canvas.translate(this.width / 2, this.height / 2);
  59. //2、绘制扇形
  60. drawPie(canvas);
  61. canvas.restore();
  62. }
  63.  
  64. private void drawPie(Canvas canvas) {
  65. float startAngle = 0;
  66. for (int i = 0; i < pieEntities.size(); i++) {
  67. PieEntity pieEntity = pieEntities.get(i);
  68. paint.setColor(pieEntity.getColor());//设置扇形的颜色
  69. }
  70. }
  71.  
  72. /**
  73. * 设置饼图的源数据
  74. */
  75. public void setData(List<PieEntity> pieEntities) {
  76. this.pieEntities = pieEntities;
  77. for (PieEntity pieEntity : pieEntities) {
  78. this.totalValue += pieEntity.getValue();
  79. }
  80. }
  81. }

再回到绘制方法来计算sweepAngle的值为:

这时startAngle和sweepAngle参数值都确定了,下面则可以进式进行扇形的绘制了:

  1. public class PieView extends View {
  2.  
  3. /* 数据源,由外部传过来 */
  4. private List<PieEntity> pieEntities;
  5. /* 控件的大小 */
  6. private int height, width;
  7. /* 圆的半径 */
  8. private int radius;
  9. /* 扇形组成圆形的外接短形 */
  10. private RectF rectF;
  11. /* 绘制扇形的画笔 */
  12. private Paint paint;
  13. /* 总占比 */
  14. private int totalValue;
  15. private Path path;
  16.  
  17. public PieView(Context context) {
  18. this(context, null);
  19. }
  20.  
  21. public PieView(Context context, @Nullable AttributeSet attrs) {
  22. this(context, attrs, 0);
  23. }
  24.  
  25. public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  26. super(context, attrs, defStyleAttr);
  27. init();
  28. }
  29.  
  30. private void init() {
  31. rectF = new RectF();
  32.  
  33. paint = new Paint();
  34. paint.setAntiAlias(true);
  35.  
  36. path = new Path();
  37. }
  38.  
  39. //当自定义控件的尺寸已经决定好的时候回调
  40. @Override
  41. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  42. super.onSizeChanged(w, h, oldw, oldh);
  43. //初始化控制的宽高
  44. this.width = w;
  45. this.height = h;
  46.  
  47. //为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
  48. int min = Math.min(this.width, this.height);
  49. this.radius = (int) ((min * 0.7f) / 2);//圆的半径取屏幕的7成
  50. rectF.left = -radius;
  51. rectF.top = -radius;
  52. rectF.right = radius;
  53. rectF.bottom = radius;
  54. }
  55.  
  56. @Override
  57. protected void onDraw(Canvas canvas) {
  58. super.onDraw(canvas);
  59. canvas.save();//由于用到了translate画片所以需要save一下
  60. //1、将画布的坐标系移到屏幕中间,这样方便去绘制圆
  61. canvas.translate(this.width / 2, this.height / 2);
  62. //2、绘制扇形
  63. drawPie(canvas);
  64. canvas.restore();
  65. }
  66.  
  67. private void drawPie(Canvas canvas) {
  68. float startAngle = 0;
  69. for (int i = 0; i < pieEntities.size(); i++) {
  70. PieEntity pieEntity = pieEntities.get(i);
  71. paint.setColor(pieEntity.getColor());//设置扇形的颜色
  72. float sweepAngle = (pieEntity.getValue() / totalValue) * 360;
  73. path.arcTo(rectF, startAngle, sweepAngle);
  74. canvas.drawPath(path, paint);
  75. }
  76. }
  77.  
  78. /**
  79. * 设置饼图的源数据
  80. */
  81. public void setData(List<PieEntity> pieEntities) {
  82. this.pieEntities = pieEntities;
  83. for (PieEntity pieEntity : pieEntities) {
  84. this.totalValue += pieEntity.getValue();
  85. }
  86. }
  87. }

但是startAngle在每次循环之后都需要进行改变,所以不要忘了去更改它的值:

下面开始运行看下效果:

咦~咋回事不是每个扇形都定义的不同的颜色了么,为啥都是同一个颜色呢,而且该绿色为我们定义的第一个颜色:

这里需要引入另外一个关于Path的API,下面直接写出:

那为什么要用它呢?因为path会记录上一次绘制的颜色,所以说这个一定得注意!!!下面再看一下效果:

呃,怎么还是有问题,这是由于我们在绘制Path时需要设置它的绘制坐标点,应该是(0,0)的位置,也就是每次都是从圆心进行绘制:

再次看效果:

OK,效果终于出来了,但是!!!对比最终效果来看:

那如何达到这样的效果呢?其实思路很简单,如下:

于是乎说干就干:

看下效果:

直线的绘制处理

接下来处理各个扇形上的直线绘制了,先看一下最终效果:

先来分析一下该直线的需求,先看下草图:

要求一:绘制直线的反向延长线经过圆心。

要求二:直线与圆的交点在对应扇形的中点处。

要求三:所有的直线的颜色一致,而不像扇形每块颜色不一样。

清求了需求之后,那想想如何去实现这种规则的直线呢?继续分析:

根据"两点成一线"原则,我们只要计算出来这两点那就好办了,如下:

而计算这两点需要用到我们在高中学习的三角函数的计算的知识,先来回顾一下,如今早已忘得差不多了,百度百科一下:

下面来看一下下图:

已知三角形的角度是θ,其斜边是r,那如何得到A坐标的x,y值呢?

x值:由于,所以x = cosθ * r;

y值:由于,所以y = sinθ * r;

有了上面的理论,下面通过草图来进一步分析:

①、直线的起点(与圆相交的点)的计算:

但是角度α目前是未知的,所以首要任务就是需要计算它,而目前已知弧度,很荣誉,android的Math工具类中可以根据弧度来计算,如下:

弧度α = startAngle + sweepAngle / 2;

x = radius * Math.cos(Math.toRadians(a));

y = radius * Math.sin(Math.toRadians(a));

【注】:传给Math.cos()中的参数为啥还要调用Math.toRadians(a)呢?因为这个类中参数要求的是弧度制,而不是我们数学中的角度制,所以需要用Math.toRadians转换一下。

②、直线的终点(向外延长的点)的计算:计算方法同起点,只是将原来的radius加长一点,这里我们用radius+3。

有了上面的分析,实现就比较easy了,下面开始编码:

  1. public class PieView extends View {
  2.  
  3. /* 数据源,由外部传过来 */
  4. private List<PieEntity> pieEntities;
  5. /* 控件的大小 */
  6. private int height, width;
  7. /* 圆的半径 */
  8. private int radius;
  9. /* 扇形组成圆形的外接短形 */
  10. private RectF rectF;
  11. /* 绘制扇形的画笔 */
  12. private Paint paint;
  13. /* 总占比 */
  14. private int totalValue;
  15. private Path path;
  16. /* 由于线条的颜色需要一致所以重新新建一个Paint专用于绘制直线 */
  17. private Paint linePaint;
  18.  
  19. public PieView(Context context) {
  20. this(context, null);
  21. }
  22.  
  23. public PieView(Context context, @Nullable AttributeSet attrs) {
  24. this(context, attrs, 0);
  25. }
  26.  
  27. public PieView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
  28. super(context, attrs, defStyleAttr);
  29. init();
  30. }
  31.  
  32. private void init() {
  33. rectF = new RectF();
  34.  
  35. paint = new Paint();
  36. paint.setAntiAlias(true);
  37.  
  38. path = new Path();
  39.  
  40. linePaint = new Paint();
  41. linePaint.setAntiAlias(true);
  42. linePaint.setColor(Color.BLACK);
  43. }
  44.  
  45. //当自定义控件的尺寸已经决定好的时候回调
  46. @Override
  47. protected void onSizeChanged(int w, int h, int oldw, int oldh) {
  48. super.onSizeChanged(w, h, oldw, oldh);
  49. //初始化控制的宽高
  50. this.width = w;
  51. this.height = h;
  52.  
  53. //为了防止绘制后超出屏幕区域,获取屏幕的宽高的较小值
  54. int min = Math.min(this.width, this.height);
  55. this.radius = (int) ((min * 0.7f) / 2);//圆的半径取屏幕的7成
  56. rectF.left = -radius;
  57. rectF.top = -radius;
  58. rectF.right = radius;
  59. rectF.bottom = radius;
  60. }
  61.  
  62. @Override
  63. protected void onDraw(Canvas canvas) {
  64. super.onDraw(canvas);
  65. canvas.save();//由于用到了translate画片所以需要save一下
  66. //1、将画布的坐标系移到屏幕中间,这样方便去绘制圆
  67. canvas.translate(this.width / 2, this.height / 2);
  68. //2、绘制扇形
  69. drawPie(canvas);
  70. canvas.restore();
  71. }
  72.  
  73. private void drawPie(Canvas canvas) {
  74. float startAngle = 0;
  75. for (int i = 0; i < pieEntities.size(); i++) {
  76. PieEntity pieEntity = pieEntities.get(i);
  77. paint.setColor(pieEntity.getColor());//设置扇形的颜色
  78. path.moveTo(0, 0);//需要将其移动到坐标系位置
  79. //其中减1是为了让各扇形区域之间有一个间隙
  80. float sweepAngle = (pieEntity.getValue() / totalValue) * 360 - 1;
  81. path.arcTo(rectF, startAngle, sweepAngle);
  82. canvas.drawPath(path, paint);
  83.  
  84. //绘制每个扇形对应的直线
  85. double a = Math.toRadians(startAngle + sweepAngle / 2);//将角度转化为弧度
  86. float startX = (float) (radius * Math.cos(a));
  87. float startY = (float) (radius * Math.sin(a));
  88. float endX = (float) ((radius + 30) * Math.cos(a));
  89. float endY = (float) ((radius + 30) * Math.sin(a));
  90. canvas.drawLine(startX, startY, endX, endY, linePaint);
  91.  
  92. //每一个扇形区域的起始点就是上一个扇形区域的终点
  93. startAngle += sweepAngle + 1;
  94. //在每次绘制扇形之后需要对path进行重置操作,这样就可以清除上一次绘制path使用的画笔的相关记录
  95. path.reset();
  96. }
  97. }
  98.  
  99. /**
  100. * 设置饼图的源数据
  101. */
  102. public void setData(List<PieEntity> pieEntities) {
  103. this.pieEntities = pieEntities;
  104. for (PieEntity pieEntity : pieEntities) {
  105. this.totalValue += pieEntity.getValue();
  106. }
  107. }
  108. }

编译运行:

文本的绘制处理

终于到最后一个步骤啦,坚持!!!先来分析一下最终效果的文本:

但是有个疑问?如果文本是绘制在直线的终点坐标处,按照惯例绘制都是基于左上角的点,那应该是长这样啊:

这里就有一个需要注意的点:对于文本的绘制它的基准点是左下角,而平常我们绘制其它的一些东东都是基于左上角,这个切记切记!!!

所以下面我们开始绘制文本:

编译运行:

字体貌似有点小,于是乎可以加大一点:

再来看一下效果:

呃~~貌似圆形左边的文字有点错乱了,对比一下最终效果图:

所以说需要对这种特殊区域进行一些文字处理,这里做一下规定:如果在90度~270度之间的文字让其绘制在直线左边:

所以需要加个角度的判断对其进行一些处理:

再次编译看效果:

是不是就跟最终的效果差不多啦!!!不过~~对于这个文本的处理还是有些差别?

不是说文字在90度与270度之间的文本都应该是在直线的左边么,那它为啥还是在直线的右边:

下面来打印一下日志,通过日志来解释:

编译运行,日志输出如下:

其实逻辑是没有错的,只是因为startAngle做了如下处理:

所以:

当然做得好一些的话应该是判断文字绘制的角度,也就是在startAngle基础之上减去sweepAngle/2的度数,修改代码如下:

再次运行:

ok,完美呈现!!

Graphic系统综合练习案例-绘制饼状图的更多相关文章

  1. 用PNChart绘制饼状图简介

    写在前面 最近做的小Demo中有一个绘制饼状图的需求.在开始实现之前上网了解了一下现有的一些绘制图形的第三方库,相应的库还是有挺多的,PNChart便是其中一个.PNChart是一个90后的中国boy ...

  2. 第166天:canvas绘制饼状图动画

    canvas绘制饼状图动画 1.HTML <!DOCTYPE html> <html lang="en"> <head> <meta ch ...

  3. Canvas(3)---绘制饼状图

    Canvas(3)---绘制饼状图 有关canvas之前有写过两篇文章 1.Canvas(1)---概述+简单示例 2.Canvas(2)---绘制折线图 在绘制饼状图之前,我们先要理解什么是圆弧,如 ...

  4. canvas动态绘制饼状图,

    当我们使用Echrts很Highcharts的时候,总是觉得各种统计图表是多么神奇,今天我就用现代浏览器支持的canvas来绘制饼状统计图,当然仅仅是画出图并没什么难度,但是统计图一般都有输入,根据不 ...

  5. [canvas]用canvas绘制饼状图

    折线图之后又来饼状图啦~\(≧▽≦)/~啦啦啦 <!DOCTYPE html> <html lang="en"> <head> <meta ...

  6. IOS之以UIBezierPath绘制饼状图

    1.绘制的饼状图是通过多个扇形拼和而成,绘制一个扇形也是比较简单的,核心代码如下: 先画一条圆弧,再画半径,接着再画一条圆弧,最后闭合路径: UIBezierPath*  aPath = [[UIBe ...

  7. [Echarts]用Echarts绘制饼状图

    在项目网站的网页中,有这样一幅图: 心血来潮,想使用百度Echarts来绘制一下,可是没能绘制得完全一样,Echarts饼状图的label不能在图形下面放成一行,最后的效果是这样子的: 鼠标移动到it ...

  8. d3绘制饼状图

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. matplotlib绘制饼状图

    源自http://blog.csdn.net/skyli114/article/details/77508430?ticket=ST-41707-PzNbUDGt6R5KYl3TkWDg-passpo ...

随机推荐

  1. beSTORM之网络协议Fuzz入门教程

    转载自FreeBuf.COM 本文将以SNMP协议为例介绍如何使用beSTORM进行网络协议Fuzz. 实验环境 Windows 7 X64 (IP:192.168.0.123) beSTORM 3. ...

  2. kubernetes资源调度

    kubernetes默认情况下创建pod调度是由kubernetes scheduler来管理的,但显然有时候还是需要人为介入.根据目前的kubernetes版本来说,有两种自定义资源调度的方式:No ...

  3. VS.2017下载安装_ZC

    ZC:20190623 1.我现在下载的是 社区版 C++的桌面开发 和 C#的开发,下载 文件压缩为:移动硬盘“H:\ZC_IDE\VC\vs2017_cs_cpp(community).rar” ...

  4. TF-IDF算法之关键词提取

    (注:本文转载自阮一峰老师的博文,原文地址:http://www.ruanyifeng.com/blog/2013/03/tf-idf.html) 这个标题看上去好像很复杂,其实我要谈的是一个很简单的 ...

  5. MemCache服务安装配置及windows下修改端口号

    简述:memcached 开源的分布式缓存数据系统.高性能的NOSQL Linux 一.环境配置与安装 01.编译准备环境 yum install -y gcc make cmake autoconf ...

  6. java中类加载的全过程及内存图分析

    类加载机制: jvm把class文件加载到内存,并对数据进行校验.解析和初始化,最终形成jvm可以直接使用的java类型的过程. (1)加载 将class文件字节码内容加载到内存中,并将这些静态数据转 ...

  7. [LuoguP3064][USACO12DEC]伊斯坦布尔的帮派Gangs of Istanbull(加强版)_线段树_贪心

    伊斯坦布尔的帮派Gangs of Istanbull 题目链接:https://www.luogu.org/problem/P3064 数据范围:略. 题解: 这个题其实分为两问,第一问是$YES$. ...

  8. 剑指offer38:输入一棵二叉树,求该树的深度

    1 题目描述 输入一棵二叉树,求该树的深度.从根结点到叶结点依次经过的结点(含根.叶结点)形成树的一条路径,最长路径的长度为树的深度. 2 思路和方法 深度优先搜索,每次得到左右子树当前最大路径,选择 ...

  9. 【静态延迟加载】self关键字和static关键字的区别

    先来看下代码,从代码中发现问题.解决问题 //先实现一个手机工厂类 class Phone{ public static function setBrand(){ echo "Main Ph ...

  10. 空间变换网络(STN)原理+2D图像空间变换+齐次坐标系讲解

    空间变换网络(STN)原理+2D图像空间变换+齐次坐标系讲解 2018年11月14日 17:05:41 Rosemary_tu 阅读数 1295更多 分类专栏: 计算机视觉   版权声明:本文为博主原 ...