根据项目需求,需要写一个正三角形雷达图来展示个人信息对比度,本人最初版本是按照固定的正三角形进行绘制的,但是考虑到后续的扩展性,最终决定写成按数据展示的正多边形。刚开始感觉绘制正多边形时将问题想的很难,但是逐步思考与实践绘制后发现绘制正多边形是有一定的固定规律的。(个人感觉有些事往往是想着很难但是到实践的时候发现并没那么难),以下便是我的整体思路和代码。
将起始点定好,我将起始点的 x 坐标定在了当前 view 宽度中心处,y 坐标自定义。
正多边形是按照顺时针方向绘制的
@interface CYNEdgeView : UIView
//数据源
@property (nonatomic, strong)NSArray *infoArray;
@end
@interface CYNEdgeView ()
//中心角的一半(相邻两个点与中心点所成夹角的一半)
@property (nonatomic, assign)CGFloat z;
//半径(点到中心点的距离)
@property (nonatomic, assign)CGFloat r;
//n 边形
@property (nonatomic, assign)NSInteger n;
//雷达图背景数组
@property (nonatomic, strong)NSMutableArray *array;
//描绘点雷达图数组
@property (nonatomic, strong)NSMutableArray *edgeArray;
//x(起始点横坐标)
@property (nonatomic, assign)CGFloat x;
//y(起始点纵坐标)
@property (nonatomic, assign)CGFloat y;
//n 边形高度(通过计算可得)
@property (nonatomic, assign)CGFloat h;
@end
对view进行初始化
//起始点坐标 _x, _y
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_n = 0;
_z = 0;
_r = 0;
_x = frame.size.width / 2;
_y = 10;
_h = frame.size.height - 20;
_array = [NSMutableArray array];
_minArray = [NSMutableArray array];
_edgeArray = [NSMutableArray array];
}
return self;
}
计算过程中发现:
如果边数是偶数的正多边形,必然有一个点在中心点和起始点所连成的直线上,这个时候半径 _r 就是 _h 高度的一半,其余各点以此直线呈现左右对称排列;
如果边数是奇数的正多边形,必然存在一对相临点连成的直线与中心点和起始点所连成的直线成垂直状态,这个时候半径 _r 就是 _h / (1 + cos(_z)),其余各点以此直线呈现左右对称排列。
//对数据源进行处理
- (void)setInfoArray:(NSArray *)infoArray {
_infoArray = infoArray;
//计算出多边形的具体边数
_n = [_infoArray.firstObject count];
//计算出中心角的一半
_z = (360.0 / _n) / 2 * M_PI / 180.0;
//计算出半径
_r = _n % 2 == 0 ? _h / 2 : _h / (1 + cos(_z));
//removeAllObjects 是为了配合 drawRect 的 clear 方法。根据数据进行多次加载
[_array removeAllObjects];
[_edgeArray removeAllObjects];
//为左右点的分割点,偶数多边形与奇数多边形并不一致
NSInteger leftNum = _n % 2 == 0 ? (_n / 2 + 1) : ((_n + 1) / 2);
//描点方式 中 右 左(顺时针方向)
//计算雷达图背景的点坐标,分为三层。从外往内计算
for (NSInteger i = 3; i > 0; i--) {
NSMutableArray *backArray = [NSMutableArray array];
//没层有 _n 个元素
for (NSInteger j = 0; j < _n; j++) {
NSDictionary *backDic = @{@"percent" : [NSString stringWithFormat:@"%.2lf", (i / 3.0)]};
[backArray addObject:backDic];
}
[_array addObject:[self calculateXYWithNGraphicsArray:backArray leftNum:leftNum]];
}
//计算雷达图周围各点,_infoArray 数组是按照一定规则生成的。
//infoArray 数组为所画雷达图的层数,每一层分为若干个点,percent 在0~1之间
// _infoArray = @[@[@{@"percent" : @"0.2"}, @{@"percent" : @"0.3"}]];
for (NSInteger i = 0; i < _infoArray.count; i++) {
//edgeArray 是为了承接每层雷达图的坐标点
[_edgeArray addObject:[self calculateXYWithNGraphicsArray:_infoArray[i] leftNum:leftNum]];
}
//重绘
[self setNeedsDisplay];
}
对计算点进行封装
- (NSArray *)calculateXYWithNGraphicsArray:(NSArray *)array leftNum:(NSInteger) leftNum {
NSMutableArray *xyArray = [NSMutableArray array];
for (NSInteger i = 0; i < array.count; i++) {
//获取所需分数
CGFloat percent = [[array[i] objectForKey:@"percent"] floatValue];
CGFloat edgeX = _x;
CGFloat edgeY = _y;
if (i >= leftNum) {
//左半边各点
CGFloat z = (360 - i * 360.0 / _n ) / 2.0 * M_PI / 180.0;
CGFloat s = (180.0 - (360 - i * 360.0 / _n)) / 2.0 * M_PI / 180.0;
//计算当前点的坐标
edgeX -= _r * percent * sin(z) * 2 * sin(s);
edgeY += _r * (1 - percent) + _r * percent * sin(z) * 2 * cos(s);
}else {
//右半边各点
CGFloat z = (i * 360.0 / _n) / 2 * M_PI / 180.0;
CGFloat s = (180.0 - i * 360.0 / _n) / 2 * M_PI / 180.0;
//计算当前点的坐标
edgeX += _r * percent * sin(z) * 2 * sin(s);
edgeY += _r * (1 - percent) + _r * percent * sin(z) * 2 * cos(s);
}
//坐标点处理好后,添加到数组中
NSDictionary *edgeDic = @{@"x" : [NSString stringWithFormat:@"%.2lf", edgeX], @"y" : [NSString stringWithFormat:@"%.2lf", edgeY]};
[xyArray addObject:edgeDic];
}
return xyArray;
}
对雷达图进行绘制
//重写drawRect方法
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
//判断数组不为空时在进行绘制
if (_array.count > 0) {
CGContextRef context = UIGraphicsGetCurrentContext();
//可以用 clear 方法清空绘画内容以便于根据数据进行多次加载(**使用 clear 时需要将当前view的背景颜色设置成 [UIColor clearColor] 颜色,否则无论设置其它任何颜色背景都会变成黑色**)
//CGContextClearRect(context, rect);
//绘制雷达图背景
for (NSInteger i = 0; i < _array.count; i++) {
[self contextStrokeWithColorR:204 G:204 B:204 A:1 textArray:_array[i] context:context width:1];
}
//中心点到各点的连线
CGContextBeginPath(context);
CGContextMoveToPoint(context, _x, _y + _r);
for (NSInteger i = 0; i < _array.count; i++) {
CGContextAddLineToPoint(context, [[[_array.firstObject objectAtIndex:i] objectForKey:@"x"] floatValue], [[[_array.firstObject objectAtIndex:i] objectForKey:@"y"] floatValue]);
CGContextAddLineToPoint(context, _x, _y + _r);
}
CGContextStrokePath(context);
//绘制雷达图
for (NSInteger i = 0; i < _edgeArray.count; i++) {
if (i == 0) {
[self contextFillWithColorR:35 G:170 B:165 A:0.5 textArray:_edgeArray[i] context:context];
[self contextStrokeWithColorR:35 G:170 B:165 A:0.9 textArray:_edgeArray[i] context:context width:3];
[self contextRoundFillWithColorR:35 G:170 B:165 A:1 array:_edgeArray[i] context:context];
}else {
[self contextFillWithColorR:173 G:212 B:154 A:0.5 textArray:_edgeArray[i] context:context];
[self contextStrokeWithColorR:173 G:212 B:154 A:0.9 textArray:_edgeArray[i] context:context width:3];
[self contextRoundFillWithColorR:173 G:212 B:154 A:1 array:_edgeArray[i] context:context];
}
}
}
}
对绘制方法进行封装,因为雷达图的背景也调用该方法所以我将点和线进行分开封装。(封装方法根据个人习惯随意)
//线段
- (void)contextStrokeWithColorR:(CGFloat)r G:(CGFloat)g B:(CGFloat)b A:(CGFloat)a textArray:(NSArray *)array context:(CGContextRef)context width:(CGFloat)width {
CGContextBeginPath(context);
CGContextSetLineWidth(context, width);
CGContextSetRGBStrokeColor(context, r / 255.0, g / 255.0, b / 255.0, a);
CGContextMoveToPoint(context, [[array.firstObject objectForKey:@"x"] floatValue], [[array.firstObject objectForKey:@"y"] floatValue]);
for (NSInteger i = 0; i < array.count; i++) {
CGContextAddLineToPoint(context, [[array[i] objectForKey:@"x"] floatValue], [[array[i] objectForKey:@"y"] floatValue]);
}
CGContextAddLineToPoint(context, [[array.firstObject objectForKey:@"x"] floatValue], [[array.firstObject objectForKey:@"y"] floatValue]);
CGContextStrokePath(context);
}
//面
- (void)contextFillWithColorR:(CGFloat)r G:(CGFloat)g B:(CGFloat)b A:(CGFloat)a textArray:(NSArray *)array context:(CGContextRef)context {
CGContextBeginPath(context);
CGContextSetRGBFillColor(context, r / 255.0, g / 255.0, b / 255.0, a);
CGContextMoveToPoint(context, [[array.firstObject objectForKey:@"x"] floatValue], [[array.firstObject objectForKey:@"y"] floatValue]);
for (NSInteger i = 0; i < array.count; i++) {
CGContextAddLineToPoint(context, [[array[i] objectForKey:@"x"] floatValue], [[array[i] objectForKey:@"y"] floatValue]);
}
CGContextAddLineToPoint(context, [[array.firstObject objectForKey:@"x"] floatValue], [[array.firstObject objectForKey:@"y"] floatValue]);
CGContextFillPath(context);
}
//画圆(本来用图片代替圆点的,但因为层级结构问题,出现遮挡问题,所以添加了圆点的绘制)
- (void)contextRoundFillWithColorR:(CGFloat)r G:(CGFloat)g B:(CGFloat)b A:(CGFloat)a array:(NSArray *)array context:(CGContextRef)context {
CGFloat radius = 6;
for (NSInteger i = 0; i < array.count; i++) {
CGContextBeginPath(context);
CGContextSetRGBFillColor(context, r / 255.0, g / 255.0, b / 255.0, a);
CGContextAddArc(context, [[array[i] objectForKey:@"x"] floatValue], [[array[i] objectForKey:@"y"] floatValue], radius, 0, 2 * M_PI, 0);
CGContextFillPath(context);
}
}
本人写的一般,只希望能为写类似雷达图的小伙伴提供一点帮助,谢谢!