雷達圖多用在游戲人物屬性分布、成績分布、個人畫像等諸多場景。傳統(tǒng)的雷達圖分為單個對象和多個對象,前者由一組連續(xù)的點構(gòu)成一個面,分別設(shè)置這個面的內(nèi)部填充顏色和輪廓渲染顏色即可,后者則是多個面的疊加,原理相同。
由于工作需要,需求方設(shè)計了一個分塊的雷達圖,在GitHub上發(fā)現(xiàn)一個不錯的基礎(chǔ)版本ZFChart(在此感謝此道友),修改了并添加了一些屬性和方法,制作了如下的實現(xiàn)形式:


首先,看一下ZFChat中的使用方法:
- (void)viewDidLoad{
[super viewDidLoad];
self.radarChart = [[ZFRadarChart alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT - NAVIGATIONBAR_HEIGHT)];
self.radarChart.dataSource = self;
self.radarChart.delegate = self;
self.radarChart.unit = @" €";
self.radarChart.itemFont = [UIFont systemFontOfSize:12.f];
self.radarChart.valueFont = [UIFont systemFontOfSize:12.f];
self.radarChart.polygonLineWidth = 2.f;
self.radarChart.valueType = kValueTypeDecimal;
self.radarChart.valueTextColor = ZFOrange;
[self.view addSubview:self.radarChart];
[self.radarChart strokePath];
}
#pragma mark - ZFRadarChartDataSource
- (NSArray *)itemArrayInRadarChart:(ZFRadarChart *)radarChart{
return @[@"item 1", @"item 2", @"item 3", @"item 4", @"item 5"];
}
- (NSArray *)valueArrayInRadarChart:(ZFRadarChart *)radarChart{
return @[@"4", @"10", @"4", @"9", @"7"];
}
- (CGFloat)maxValueInRadarChart:(ZFRadarChart *)radarChart{
return 10.f;
}
#pragma mark - ZFRadarChartDelegate
- (CGFloat)radiusForRadarChart:(ZFRadarChart *)radarChart{
return (SCREEN_WIDTH - 100) / 2;
}
這段代碼描述了單個對象雷達圖的創(chuàng)建、數(shù)據(jù)源和代理函數(shù)的實現(xiàn)。彩色雷達圖也基于同樣的實現(xiàn)方式,具體上則是,修改了其內(nèi)部的
三個子類:

這里先說下思路:
- 1.獲取各個頂點的坐標pointArray
- 2.將pointArray擴展成2倍頂點個數(shù)的extendArray
- 3.每次從extendArray中取出2個連續(xù)的點,與雷達圖圓心共同構(gòu)成一個三角形
- 4.使用預(yù)先設(shè)置好的顏色數(shù)組,填充每次構(gòu)成的三角形
- 5.設(shè)置一個從初始到完成的動畫效果
說明:2中的擴展方法:依次取相鄰pointArray中的兩個點,取其中二維空間的中間點。
代碼如下:
- (void)getDescribePoint{
[self.describePointArray removeAllObjects];
_startAngle = -90.f;
//獲取第一個item半徑
_currentRadius = [_radiusArray.firstObject floatValue];
// UIBezierPath * bezier = [UIBezierPath bezierPath];
// [bezier moveToPoint:CGPointMake(_polygonCenter.x, _polygonCenter.y - _currentRadius)];
[self.describePointArray addObject:[NSValue valueWithCGPoint:CGPointMake(_polygonCenter.x, _polygonCenter.y - _currentRadius)]];
for (NSInteger i = 1; i < _radiusArray.count; i++) {
_currentRadarAngle = _averageRadarAngle * i;
//計算每個item的角度
_endAngle = _startAngle + _averageRadarAngle;
//獲取當前item半徑
_currentRadius = [_radiusArray[i] floatValue];
if (_endAngle > -90.f && _endAngle <= 0.f) {
_endXPos = _polygonCenter.x + fabs(-(_currentRadius * ZFSin(_currentRadarAngle)));
_endYPos = _polygonCenter.y - fabs(_currentRadius * ZFCos(_currentRadarAngle));
}else if (_endAngle > 0.f && _endAngle <= 90.f){
_endXPos = _polygonCenter.x + fabs(-(_currentRadius * ZFSin(_currentRadarAngle)));
_endYPos = _polygonCenter.y + fabs(_currentRadius * ZFCos(_currentRadarAngle));
}else if (_endAngle > 90.f && _endAngle <= 180.f){
_endXPos = _polygonCenter.x - fabs(-(_currentRadius * ZFSin(_currentRadarAngle)));
_endYPos = _polygonCenter.y + fabs(_currentRadius * ZFCos(_currentRadarAngle));
}else if (_endAngle > 180.f && _endAngle < 270.f){
_endXPos = _polygonCenter.x - fabs(-(_currentRadius * ZFSin(_currentRadarAngle)));
_endYPos = _polygonCenter.y - fabs(_currentRadius * ZFCos(_currentRadarAngle));
}
// [bezier addLineToPoint:CGPointMake(_endXPos, _endYPos)];
//記錄下一個item開始角度
_startAngle = _endAngle;
[self.describePointArray addObject:[NSValue valueWithCGPoint:CGPointMake(_endXPos, _endYPos)]];
}
// [bezier closePath];
// return bezier;
}
說明:getDescribePoint函數(shù)為原fill函數(shù)的改寫,只保存各個頂點的坐標。
- (void)getExtendPoint {
NSInteger count = self.describePointArray.count;
for (int i=0; i<count; i++) {
[self.extendPointArray addObject:self.describePointArray[i]];
CGPoint point = [self.describePointArray[i] CGPointValue];
CGPoint pointNext = [self.describePointArray[(i+1) % count] CGPointValue];
CGFloat newX = point.x + (pointNext.x - point.x)/2.0;
CGFloat newY = point.y + (pointNext.y - point.y)/2.0;
CGPoint newPoint = CGPointMake(newX, newY);
[self.extendPointArray addObject:[NSValue valueWithCGPoint:newPoint]];
}
}
說明:getExtendPoint函數(shù)將最初的頂點數(shù)組擴展為2倍點的數(shù)組
- (CAShapeLayer *)drawTraiangleWithPoint:(CGPoint)point nextPoint:(CGPoint)nextPoint fillColor:(UIColor *)color{
// 三角形
CAShapeLayer * shapeLayer = [CAShapeLayer layer];
shapeLayer.fillColor = color.CGColor;
shapeLayer.strokeColor = nil;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineWidth = 1;
UIBezierPath *triangle = [UIBezierPath bezierPath];
[triangle moveToPoint:_polygonCenter];
[triangle addLineToPoint:point];
[triangle addLineToPoint:nextPoint];
[triangle closePath];
shapeLayer.path = triangle.CGPath;
if (_isAnimated) {
CABasicAnimation * fillAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
fillAnimation.duration = _animationDuration;
fillAnimation.fillMode = kCAFillModeForwards;
fillAnimation.removedOnCompletion = NO;
fillAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
fillAnimation.fromValue = (__bridge id)[self noFill].CGPath;
fillAnimation.toValue = (__bridge id)triangle.CGPath;
[shapeLayer addAnimation:fillAnimation forKey:@"animationDuration"];
}
return shapeLayer;
}
說明: drawTraiangleWithPoint:函數(shù)使用UIBezierPath繪制路徑,使用CAShapeLayer顯示效果,使用CABasicAnimation添加繪制過程動畫。由于是繪制彩色雷達圖,輪廓顏色與內(nèi)部填充顏色一致,故不需要單獨再繪制一次輪廓。如有需要可參考如下代碼(drawTraiangleWithPoint函數(shù)的微調(diào)):
- (CAShapeLayer *)drawTraiangleStrokeWithPoint:(CGPoint)point nextPoint:(CGPoint)nextPoint strokeColor:(UIColor *)color{
CAShapeLayer * shapeLayer = [CAShapeLayer layer];
shapeLayer.fillColor = nil;
shapeLayer.strokeColor = color.CGColor;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineWidth = 1;
UIBezierPath *triangle = [UIBezierPath bezierPath];
[triangle moveToPoint:_polygonCenter];
[triangle addLineToPoint:point];
[triangle addLineToPoint:nextPoint];
[triangle closePath];
shapeLayer.path = triangle.CGPath;
if (_isAnimated) {
CABasicAnimation * fillAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
fillAnimation.duration = _animationDuration;
fillAnimation.fillMode = kCAFillModeForwards;
fillAnimation.removedOnCompletion = NO;
fillAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
fillAnimation.fromValue = (__bridge id)[self noFill].CGPath;
fillAnimation.toValue = (__bridge id)triangle.CGPath;
[shapeLayer addAnimation:fillAnimation forKey:@"animationDuration"];
}
return shapeLayer;
}
最后便是調(diào)用位置的問題了:
- (void)strokePath{
[self removeAllSubLayers];
[self getDescribePoint];
[self getExtendPoint];
NSInteger count = self.extendPointArray.count;
for (NSInteger i=0; i<count; i++) {
CGPoint point = [self.extendPointArray[(i+1) % count] CGPointValue];
CGPoint nextPoint = [self.extendPointArray[(i+2) % count] CGPointValue];
UIColor *color = self.extendColorArray[i];
[self.layer addSublayer:[self drawTraiangleWithPoint:point nextPoint:nextPoint fillColor:color]];
// [self.layer addSublayer:[self drawTraiangleStrokeWithPoint:point nextPoint:nextPoint strokeColor:color]];
}
}
說明:strokePath函數(shù)為原函數(shù)對外的接口,此處不作修改,僅僅去除了原先的2個傳統(tǒng)的繪制函數(shù)的調(diào)用。
結(jié)尾:主要代碼和思路已交代完畢,具體代碼點擊這里。