当前位置: 移动技术网 > 移动技术>移动开发>IOS > iOS实现带指引线的饼状图效果(不会重叠)

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

2019年07月24日  | 移动技术网移动技术  | 我要评论

效果图

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


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 ()

总结

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网