欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS实现带指引线的饼状图效果(不会重叠)

程序员文章站 2023-12-15 15:50:34
效果图 先上图(做出来的效果就是下图的样子) 1.效果图-w220 图中不论每个扇形多小,都可以从指引线处将指引的数据分割开来,不会重叠。 第一步...

效果图

先上图(做出来的效果就是下图的样子)

iOS实现带指引线的饼状图效果(不会重叠)

1.效果图-w220

图中不论每个扇形多小,都可以从指引线处将指引的数据分割开来,不会重叠。

第一步

需要给图中数据做个模型

@interface dvfoodpiemodel : nsobject
/**
 名称
 */
@property (copy, nonatomic) nsstring *name;

/**
 数值
 */
@property (assign, nonatomic) cgfloat value;

/**
 比例
 */
@property (assign, nonatomic) cgfloat rate;
@end

第二步

现在先把饼图中间的圆形做出来,这个没有什么难度,直接贴代码

在.h文件中

@interface dvpiecenterview : uiview 
@property (strong, nonatomic) uilabel *namelabel; 
@end

在.m文件中

@interface dvpiecenterview ()
@property (strong, nonatomic) uiview *centerview;
@end
@implementation dvpiecenterview
- (instancetype)initwithframe:(cgrect)frame {
 if (self = [super initwithframe:frame]) {
  self.backgroundcolor = [[uicolor whitecolor] colorwithalphacomponent:0.4];
  uiview *centerview = [[uiview alloc] init];
  centerview.backgroundcolor = [uicolor whitecolor];
  [self addsubview:centerview];
  self.centerview = centerview;
  uilabel *namelabel = [[uilabel alloc] init];
  namelabel.textcolor = [uicolor colorwithred:51/255.0 green:51/255.0 blue:51/255.0 alpha:1];
  namelabel.font = [uifont systemfontofsize:18];
  namelabel.textalignment = nstextalignmentcenter;
  self.namelabel = namelabel;
  [centerview addsubview:namelabel];
 }
 return self;
}


- (void)layoutsubviews {
 [super layoutsubviews];
 self.layer.cornerradius = self.frame.size.width * 0.5;
 self.layer.maskstobounds = true;
 self.centerview.frame = cgrectmake(6, 6, self.frame.size.width - 6 * 2, self.frame.size.height - 6 * 2);
 self.centerview.layer.cornerradius = self.centerview.frame.size.width * 0.5;
 self.centerview.layer.maskstobounds = true;
 self.namelabel.frame = self.centerview.bounds;
}

暴露的只有.h文件中的namelabel,需要中间显示文字时,给namelabel的text赋值就好了

第三步

现在就创建一个继承uiview的视图,用来画饼状图和指引线以及数据

在.h文件中需要有数据数组,还有中间显示的文字,以及一个draw方法(draw方法纯属个人习惯,在数据全部赋值完成后,调用该方法进行绘画)

@interface dvpiechart : uiview
/**
 数据数组
 */
@property (strong, nonatomic) nsarray *dataarray;
/**
 标题
 */
@property (copy, nonatomic) nsstring *title;
/**
 绘制方法
 */
- (void)draw;
@end

在调用draw方法前应确定数据全部赋值完成,绘制工作其实是在- (void)drawrect:(cgrect)rect方法中完成的,所以.h文件中的draw方法只是来调用系统方法的

在.m文件中,draw方法的实现

- (void)draw {
 [self.subviews makeobjectsperformselector:@selector(removefromsuperview)];
 [self setneedsdisplay];
}

[self setneedsdisplay];就是来调用drawrect方法的

[self.subviews makeobjectsperformselector:@selector(removefromsuperview)];这个方法是用来移除添加到piechart上的centerview,不然每次重绘时都会再次添加一个centerview

下面就是drawrect方法的实现

首先需要确定圆的半径,中心点和起始点

cgfloat min = self.bounds.size.width > self.bounds.size.height ? self.bounds.size.height : self.bounds.size.width;
cgpoint center = cgpointmake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5);
cgfloat radius = min * 0.5 - chart_margin;
cgfloat start = 0;
cgfloat angle = 0;
cgfloat end = start;

chart_margin是自己定义的一个宏,圆不能让视图的边形成切线,在此我把chart_margin设定为60
* 根据产品的需求,当请求回来的数据为空时,显示一个纯色的圆,不画指引线,所以在drawrect中分两种情况来实现

```objc
if (self.dataarray.count == 0) {

} else {

}
```
* 当dataarray的长度为0时

```objc
if (self.dataarray.count == 0) {
 
 end = start + m_pi * 2;
 
 uicolor *color = color_array.firstobject;
 
 uibezierpath *path = [uibezierpath bezierpathwitharccenter:center radius:radius startangle:start endangle:end clockwise:true];
 
 [color set];
 
 //添加一根线到圆心
 [path addlinetopoint:center];
 [path fill];
 
}
```
> color_array是自己设定的一个宏定义,产品要求的饼图份数是6份,每份颜色一定,所以做一个宏定义存储一下(做成变量都是可以的,看自己代码风格)

``` objc
#define color_array @[\

[uicolor colorwithred:251/255.0 green:166.9/255.0 blue:96.5/255.0 alpha:1],
[uicolor colorwithred:151.9/255.0 green:188/255.0 blue:95.8/255.0 alpha:1],
[uicolor colorwithred:245/255.0 green:94/255.0 blue:102/255.0 alpha:1],
[uicolor colorwithred:29/255.0 green:140/255.0 blue:140/255.0 alpha:1],
[uicolor colorwithred:121/255.0 green:113/255.0 blue:199/255.0 alpha:1],
[uicolor colorwithred:16/255.0 green:149/255.0 blue:224/255.0 alpha:1]
]
```

* 当dataarray的长度不为0时

```objc

for (int i = 0; i < self.dataarray.count; i++) {
 dvfoodpiemodel *model = self.dataarray[i];
 cgfloat percent = model.rate;
 uicolor *color = color_array[i % 6];
 start = end;
 angle = percent * m_pi * 2;
 end = start + angle;
 uibezierpath *path = [uibezierpath bezierpathwitharccenter:center radius:radius startangle:start endangle:end clockwise:true];
 [color set];
 //添加一根线到圆心
 [path addlinetopoint:center];
 [path fill];
}
```

在else中这么做,就能绘制出各个扇形

* 在扇形绘画出来后,添加centerview
```objc
// 在中心添加label
dvpiecenterview *centerview = [[dvpiecenterview alloc] init];
centerview.frame = cgrectmake(0, 0, 80, 80);
cgrect frame = centerview.frame;
frame.origin = cgpointmake(self.frame.size.width * 0.5 - frame.size.width * 0.5, self.frame.size.height * 0.5 - frame.size.width * 0.5);
centerview.frame = frame;
centerview.namelabel.text = self.title;
[self addsubview:centerview];
```

第四步,绘画指引线和数据

绘制指引线,需要在画扇形时就确定几个数据,并根据这几种数据进行绘制

  • 各个扇形圆弧的中心点
  • 指引线的重点(效果图中有圆点的位置)
// 获取弧度的中心角度
cgfloat radiancenter = (start + end) * 0.5;
// 获取指引线的终点
cgfloat linestartx = self.frame.size.width * 0.5 + radius * cos(radiancenter);
cgfloat linestarty = self.frame.size.height * 0.5 + radius * sin(radiancenter);
cgpoint point = cgpointmake(linestartx, linestarty);

因为这个图刚刚做出来时是有重叠的,按产品需求进行更改,所以起的变量名称会有些歧义,不方便改了,我只能做好注释,大家以注释为准

如果按顺序进行绘制的话,那么很难让指引线的位置不重叠,所以从中间的一个数据先进行绘制,然后在绘制中间数据两侧的数据

那么,现在需要将上面需要确定的数据依次添加到一个数组中

例:原数据为@[@1, @2, @3, @4, @5, @6]

画指引线时则需要数据这样来弄@[@3, @2, @1, @4, @5, @6]

所以for循环中应该改成这个样子

注意,数据变更顺序了之后,绘制时模型数据和颜色数据也需要变更顺序

首先声明两个变量

@interface dvpiechart ()
@property (nonatomic, strong) nsmutablearray *modelarray;
@property (nonatomic, strong) nsmutablearray *colorarray;
@end

else中变成下面这个样子

nsmutablearray *pointarray = [nsmutablearray array];
nsmutablearray *centerarray = [nsmutablearray array];
self.modelarray = [nsmutablearray array];
self.colorarray = [nsmutablearray array];
for (int i = 0; i < self.dataarray.count; i++) {
 dvfoodpiemodel *model = self.dataarray[i];
 cgfloat percent = model.rate;
 uicolor *color = color_array[i];
 start = end;
 angle = percent * m_pi * 2;
 end = start + angle;
 uibezierpath *path = [uibezierpath bezierpathwitharccenter:center radius:radius startangle:start endangle:end clockwise:true]; 
 [color set];
 //添加一根线到圆心
 [path addlinetopoint:center];
 [path fill];
 // 获取弧度的中心角度
 cgfloat radiancenter = (start + end) * 0.5;
 // 获取指引线的终点
 cgfloat linestartx = self.frame.size.width * 0.5 + radius * cos(radiancenter);
 cgfloat linestarty = self.frame.size.height * 0.5 + radius * sin(radiancenter);
 cgpoint point = cgpointmake(linestartx, linestarty);
 if (i <= self.dataarray.count / 2 - 1) {
  [pointarray insertobject:[nsvalue valuewithcgpoint:point] atindex:0];
  [centerarray insertobject:[nsnumber numberwithfloat:radiancenter] atindex:0];
  [self.modelarray insertobject:model atindex:0];
  [self.colorarray insertobject:color atindex:0];
 } else {
  [pointarray addobject:[nsvalue valuewithcgpoint:point]];
  [centerarray addobject:[nsnumber numberwithfloat:radiancenter]];
  [self.modelarray addobject:model];
  [self.colorarray addobject:color];
 }
}

for循环中确定了需要的数据:

pointarray、centerarray、self.modelarray、self.colorarray

根据上面确定的数据来绘出指引线,逻辑比较复杂,写一个方法来绘制

- (void)drawlinewithpointarray:(nsarray *)pointarray centerarray:(nsarray *)centerarray

在for循环外调用

// 通过pointarray和centerarray绘制指引线
[self drawlinewithpointarray:pointarray centerarray:centerarray];

第五步

方法内部实现

需要确定的数据都有:

1.指引线长度

2.指引线起点、终点、转折点

3.指引线数据所占的rect范围(用于确定绘制下一个的时候是否有重叠)

下面直接贴出代码实现,注意看注释,我就不在代码外再写一遍了

- (void)drawlinewithpointarray:(nsarray *)pointarray centerarray:(nsarray *)centerarray {
 // 记录每一个指引线包括数据所占用位置的和(总体位置)
 cgrect rect = cgrectzero;
 // 用于计算指引线长度
 cgfloat width = self.bounds.size.width * 0.5;
 for (int i = 0; i < pointarray.count; i++) {
  // 取出数据
  nsvalue *value = pointarray[i];
  // 每个圆弧中心店的位置
  cgpoint point = value.cgpointvalue;
  // 每个圆弧中心点的角度
  cgfloat radiancenter = [centerarray[i] floatvalue];
  // 颜色(绘制数据时要用)
  uicolor *color = self.colorarray[i % 6];
  // 模型数据(绘制数据时要用)
  dvfoodpiemodel *model = self.modelarray[i];
  // 模型的数据
  nsstring *name = model.name;
  nsstring *number = [nsstring stringwithformat:@"%.2f%%", model.rate * 100];

  // 圆弧中心点的x值和y值
  cgfloat x = point.x;
  cgfloat y = point.y;
  
  // 指引线终点的位置(x, y)
  cgfloat startx = x + 10 * cos(radiancenter);
  cgfloat starty = y + 10 * sin(radiancenter);
  
  // 指引线转折点的位置(x, y)
  cgfloat breakpointx = x + 20 * cos(radiancenter);
  cgfloat breakpointy = y + 20 * sin(radiancenter);
  
  // 转折点到中心竖线的垂直长度(为什么+20, 在实际做出的效果中,有的转折线很丑,+20为了美化)
  cgfloat margin = fabs(width - breakpointx) + 20;
  
  // 指引线长度
  cgfloat linewidth = width - margin;
  
  // 指引线起点(x, y)
  cgfloat endx;
  cgfloat endy;
  
  // 绘制文字和数字时,所占的size(width和height)
  // width使用linewidth更好,我这么写固定值是为了达到产品要求
  cgfloat numberwidth = 80.f;
  cgfloat numberheight = 15.f;
  
  cgfloat titlewidth = numberwidth;
  cgfloat titleheight = numberheight;
  
  // 绘制文字和数字时的起始位置(x, y)与上面的合并起来就是frame
  cgfloat numberx;// = breakpointx;
  cgfloat numbery = breakpointy - numberheight;
  
  cgfloat titlex = breakpointx;
  cgfloat titley = breakpointy + 2;
  
  
  // 文本段落属性(绘制文字和数字时需要)
  nsmutableparagraphstyle * paragraph = [[nsmutableparagraphstyle alloc]init];
  // 文字靠右
  paragraph.alignment = nstextalignmentright;
  
  // 判断x位置,确定在指引线向左还是向右绘制
  // 根据需要变更指引线的起始位置
  // 变更文字和数字的位置
  if (x <= width) { // 在左边
   
   endx = 10;
   endy = breakpointy;
   
   // 文字靠左
   paragraph.alignment = nstextalignmentleft;
   
   numberx = endx;
   titlex = endx;
   
  } else { // 在右边
   
   endx = self.bounds.size.width - 10;
   endy = breakpointy;
   
   numberx = endx - numberwidth;
   titlex = endx - titlewidth;
  }
  
  
  if (i != 0) {
   
   // 当i!=0时,就需要计算位置总和(方法开始出的rect)与rect1(将进行绘制的位置)是否有重叠
   cgrect rect1 = cgrectmake(numberx, numbery, numberwidth, titley + titleheight - numbery);
   
   cgfloat margin = 0;
   
   if (cgrectintersectsrect(rect, rect1)) {
    // 两个面积重叠
    // 三种情况
    // 1. 压上面
    // 2. 压下面
    // 3. 包含
    // 通过计算让面积重叠的情况消除
    if (cgrectcontainsrect(rect, rect1)) {// 包含
     
     if (i % self.dataarray.count <= self.dataarray.count * 0.5 - 1) {
      // 将要绘制的位置在总位置偏上
      margin = cgrectgetmaxy(rect1) - rect.origin.y;
      endy -= margin;
     } else {
      // 将要绘制的位置在总位置偏下
      margin = cgrectgetmaxy(rect) - rect1.origin.y;
      endy += margin;
     }
     
     
    } else { // 相交
     
     if (cgrectgetmaxy(rect1) > rect.origin.y && rect1.origin.y < rect.origin.y) { // 压在总位置上面
      margin = cgrectgetmaxy(rect1) - rect.origin.y;
      endy -= margin;
      
     } else if (rect1.origin.y < cgrectgetmaxy(rect) && cgrectgetmaxy(rect1) > cgrectgetmaxy(rect)) { // 压总位置下面
      margin = cgrectgetmaxy(rect) - rect1.origin.y;
      endy += margin;
     }
     
    }
   }
   titley = endy + 2;
   numbery = endy - numberheight;
   
   
   // 通过计算得出的将要绘制的位置
   cgrect rect2 = cgrectmake(numberx, numbery, numberwidth, titley + titleheight - numbery);
   
   // 把新获得的rect和之前的rect合并
   if (numberx == rect.origin.x) {
    // 当两个位置在同一侧的时候才需要合并
    if (rect2.origin.y < rect.origin.y) {
     rect = cgrectmake(rect.origin.x, rect2.origin.y, rect.size.width, rect.size.height + rect2.size.height);
    } else {
     rect = cgrectmake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height + rect2.size.height);
    }
   }
   
  } else {
   rect = cgrectmake(numberx, numbery, numberwidth, titley + titleheight - numbery);
  }

  // 重新制定转折点
  if (endx == 10) {
   breakpointx = endx + linewidth;
  } else {
   breakpointx = endx - linewidth;
  }
  
  breakpointy = endy;
  //1.获取上下文
  cgcontextref ctx = uigraphicsgetcurrentcontext();
  //2.绘制路径
  uibezierpath *path = [uibezierpath bezierpath];
  [path movetopoint:cgpointmake(endx, endy)];
  [path addlinetopoint:cgpointmake(breakpointx, breakpointy)];
  [path addlinetopoint:cgpointmake(startx, starty)];
  cgcontextsetlinewidth(ctx, 0.5);
  //设置颜色
  [color set];
  //3.把绘制的内容添加到上下文当中
  cgcontextaddpath(ctx, path.cgpath);
  //4.把上下文的内容显示到view上(渲染到view的layer)(stroke fill)
  cgcontextstrokepath(ctx);

  // 在终点处添加点(小圆点)
  // movepoint,让转折线指向小圆点中心
  cgfloat movepoint = -2.5;
  uiview *view = [[uiview alloc] init];
  view.backgroundcolor = color;
  [self addsubview:view];
  cgrect rect = view.frame;
  rect.size = cgsizemake(5, 5);
  rect.origin = cgpointmake(startx + movepoint, starty - 2.5);
  view.frame = rect;
  view.layer.cornerradius = 2.5;
  view.layer.maskstobounds = true;

  //指引线上面的数字
  [name drawinrect:cgrectmake(numberx, numbery, numberwidth, numberheight) withattributes:@{nsfontattributename:[uifont systemfontofsize:9.0], nsforegroundcolorattributename:color,nsparagraphstyleattributename:paragraph}];
  
  // 指引线下面的title
  [number drawinrect:cgrectmake(titlex, titley, titlewidth, titleheight) withattributes:@{nsfontattributename:[uifont systemfontofsize:9.0],nsforegroundcolorattributename:color,nsparagraphstyleattributename:paragraph}];
 } 
}

附github地址:https://github.com/firemou/dvpiechart (本地下载

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。

上一篇:

下一篇: