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 (本地下载)
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。