源码解析--PNChart

源码解析--PNChart_第1张图片
壁纸.jpg

前言:

PNChart是一个简单漂亮的iOS图表库,在github上面获得了8000多个star,建议先下载这个库配合本文的阅读。它支持以下图形的绘制:

  • PNCircleChart(环形图)
  • PNLineChart(折线图)
  • PNPieChart(饼图)
  • PNBarChart(柱状图)
  • PNRadarChart(雷达图)
  • PNScatterChart(散点图)

层次结构为:


源码解析--PNChart_第2张图片
层次结构图.png

我们开始吧:

PNCircleChart(环形图)

PNCircleChart和其他图不一样,它是直接继承UIView。为了方便讲解,我故意添加了背景色和渐变色。如图:

源码解析--PNChart_第3张图片
环形图.png

PNCircleChart主要组成部分:

@property (strong, nonatomic) UICountingLabel *countingLabel;//显示百分比文本
@property (nonatomic) CAShapeLayer *circle;//蓝色部分,部分被渐变绿色覆盖
@property (nonatomic) CAShapeLayer *gradientMask;//上图深绿色的部分
@property (nonatomic) CAShapeLayer *circleBackground;//上图灰色部分
源码解析--PNChart_第4张图片
PNCircleChart结构图.png

1.拿到图形的路径:

 UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2.0f, self.frame.size.height/2.0f)
                                                                  radius:(self.frame.size.height * 0.5) - ([_lineWidth floatValue]/2.0f)
                                                              startAngle:DEGREES_TO_RADIANS(startAngle)
                                                                endAngle:DEGREES_TO_RADIANS(endAngle)
                                                               clockwise:clockwise];

2.添加渐变颜色

 // Add gradient
        self.gradientMask = [CAShapeLayer layer];
        self.gradientMask.fillColor = [[UIColor clearColor] CGColor];
        self.gradientMask.strokeColor = [[UIColor blackColor] CGColor];
        self.gradientMask.lineWidth = _circle.lineWidth;
        self.gradientMask.lineCap = kCALineCapRound;
        CGRect gradientFrame = CGRectMake(0, 0, 2*self.bounds.size.width, 2*self.bounds.size.height);
        self.gradientMask.frame = gradientFrame;
        self.gradientMask.path = _circle.path;

        CAGradientLayer *gradientLayer = [CAGradientLayer layer];
        gradientLayer.startPoint = CGPointMake(0.5,1.0);
        gradientLayer.endPoint = CGPointMake(0.5,0.0);
        gradientLayer.frame = gradientFrame;
        UIColor *endColor = (_strokeColor ? _strokeColor : [UIColor greenColor]);
        NSArray *colors = @[
                            (id)endColor.CGColor,
                            (id)_strokeColorGradientStart.CGColor
                            ];
        gradientLayer.colors = colors;
        //如果不添加,你会发现self.gradientMask 添加在self上了,你可以试试
        [gradientLayer setMask:self.gradientMask];
        
        [_circle addSublayer:gradientLayer];

3.UICountingLabel类主要是来实现数字平滑变化的动画。利用CABasicAnimation来实现layer层的动画。动画的相关内容可以参考这里

PNLineChart(折线图)

源码解析--PNChart_第5张图片
折线图.png

1.减去左右黄色边距区域,拿到横轴作图区域

_chartCavanWidth = self.frame.size.width - _chartMarginLeft - _chartMarginRight; 

2.红色区域根据数组确定点横坐标,以及布局相关label。

[self.lineChart setXLabels:@[@"SEP 1",@"SEP 2",@"SEP 3",@"SEP 4",@"SEP 5",@"SEP 6",@"SEP 7"]];

3.拿到纵轴作图区域,布局相关label。

_chartCavanHeight = self.frame.size.height - _chartMarginBottom - _chartMarginTop;
[self.lineChart setYLabels:@[
            @"0 min",
            @"50 min",
            @"100 min",
            @"150 min",
            @"200 min",
            @"250 min",
            @"300 min",
            ]
         ];

4.根据提供的点的大小与刚刚计算的纵轴和横轴的值,计算出点具体的frame。然后根据点与点计算点与点的路径。存储下来。并利用UIBezierPathCAShapeLayer作动画。

self.lineChart.chartData = @[data01, data02];//在setter方法里面去做计算路径的操作
[self.lineChart strokeChart]; //画图

//计算x轴的点
int x = i * _xLabelWidth + _chartMarginLeft + _xLabelWidth / 2.0;
//计算y轴的点
int y = _chartCavanHeight - (innerGrade * _chartCavanHeight) - (_yLabelHeight / 2) + _chartMarginTop;      

5.PNLineChartData是关于PNLineChart的一个非常重要的类,它为PNLineChart提供线条相关的颜色,文本字体,点样式信息,比如:

typedef NS_ENUM(NSUInteger, PNLineChartPointStyle) {
    PNLineChartPointStyleNone = 0, //无
    PNLineChartPointStyleCircle = 1,//圆点
    PNLineChartPointStyleSquare = 3,//正方形点
    PNLineChartPointStyleTriangle = 4//三角形点
};
@property (nonatomic) BOOL showPointLabel; //当PNLineChartPointStyle不为PNLineChartPointStyleNone样式时,决定是否在点上面显示点信息的文本。比如上图绿色的区域。

PNBarChart(柱状图)

PNBarChart柱状图和PNLineChart是极其相似的,只不过在确定坐标系后利用去布局PNBar柱对象,PNBar对象负责每个柱对象的样式,颜色和动画。

源码解析--PNChart_第6张图片
柱状图.png

动画的实现:

-(void)addAnimationIfNeededWithProgressLine:(UIBezierPath *)progressline
{
    if (self.displayAnimated) {
        CABasicAnimation *pathAnimation = nil;
        
        if (_grade) {
            pathAnimation = [CABasicAnimation animationWithKeyPath:@"path"];
            pathAnimation.fromValue = (id)_chartLine.path;
            pathAnimation.toValue = (id)[progressline CGPath];
            pathAnimation.duration = 0.5f;
            pathAnimation.autoreverses = NO;
            pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
            [_chartLine addAnimation:pathAnimation forKey:@"animationKey"];
        }
        else {
            pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
            pathAnimation.duration = 1.0;
            pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
            pathAnimation.fromValue = @0.0f;
            pathAnimation.toValue = @1.0f;
            [_chartLine addAnimation:pathAnimation forKey:@"strokeEndAnimation"];
        }
        
        [self.gradientMask addAnimation:pathAnimation forKey:@"animationKey"];
    }
}

PNPieChart(饼图)

PNPieChart柱状可以显示当前模块所占百分比,也可以选择选择本身的值。如图(百分比):

源码解析--PNChart_第7张图片
饼图.png

1. PNPieChart初始化时会根据比较宽高德大小来以较小的设置直径,以免超出页面。

CGFloat minimal = (CGRectGetWidth(self.bounds) < CGRectGetHeight(self.bounds)) ? CGRectGetWidth(self.bounds) : CGRectGetHeight(self.bounds); //利用MIN宏会比三目运算更可读

2.PNPieChartDataItem类提供包装初始化数据,根据数据提供的itemValue计算出各个item所占比例。我们提供的初始化数据为:

 NSArray *items = @[[PNPieChartDataItem dataItemWithValue:10 color:PNLightGreen],
                           [PNPieChartDataItem dataItemWithValue:20 color:PNFreshGreen description:@"WWDC"],
                           [PNPieChartDataItem dataItemWithValue:40 color:PNDeepGreen description:@"GOOG I/O"],
                           [PNPieChartDataItem dataItemWithValue:30 color:PNMauve description:@"ATR"],
                           ];

3.计算半径,拿到准备动画绘制路径。

//计算半径以及借下来的lineWidth
self.outerCircleRadius = minimal / 2;
self.innerCircleRadius = minimal / 6;
CGFloat radius = _innerCircleRadius + (_outerCircleRadius - _innerCircleRadius) / 2;
CGFloat borderWidth = _outerCircleRadius - _innerCircleRadius;


- (CAShapeLayer *)newCircleLayerWithRadius:(CGFloat)radius
                               borderWidth:(CGFloat)borderWidth
                                 fillColor:(UIColor *)fillColor
                               borderColor:(UIColor *)borderColor
                           startPercentage:(CGFloat)startPercentage
                             endPercentage:(CGFloat)endPercentage{
    CAShapeLayer *circle = [CAShapeLayer layer];
    CGPoint center = CGPointMake(CGRectGetMidX(self.bounds),CGRectGetMidY(self.bounds));
    //从坐标轴-90°出发,拿到路径。
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center
                                                        radius:radius
                                                    startAngle:-M_PI_2
                                                      endAngle:M_PI_2 * 3
                                                     clockwise:YES];
    circle.fillColor   = fillColor.CGColor;
    circle.strokeColor = borderColor.CGColor;
    //根据strokeStart,strokeEnd绘制每个item所占的比例
    circle.strokeStart = startPercentage;
    circle.strokeEnd   = endPercentage;
    circle.lineWidth   = borderWidth;
    circle.path        = path.CGPath;
    
    return circle;
}

4.添加要显示的文本

for (int i = 0; i < _items.count; i++) {
        UILabel *descriptionLabel =  [self descriptionLabelForItemAtIndex:i];
        [_contentView addSubview:descriptionLabel];
        [_descriptionLabels addObject:descriptionLabel];
    }

5.点击事件

//拿到点击的坐标
CGPoint touchLocation = [touch locationInView:_contentView];
//根据点击的点的坐标位置来判断点击的哪块区域,做相应的判断
- (void)didTouchAt:(CGPoint)touchLocation

PNScatterChart(散点图)

源码解析--PNChart_第8张图片
散点图.png

1. PNScatterChart根据分别设置X,Y坐标的最小值,和最大值,间断数来确定X,Y的坐标轴。

//比如:x的间距:(100 - 20)/(6 - 1) 
[self.scatterChart setAxisXWithMinimumValue:20 andMaxValue:100 toTicks:6];
        [self.scatterChart setAxisYWithMinimumValue:30 andMaxValue:50 toTicks:5];

2,然后将点绘制在坐标轴中,这个和折线图思路是一致的。

PNRadarChart(雷达图)

源码解析--PNChart_第9张图片
雷达图

1. PNRadarChartDataItem类提供包装初始化数据,首先根据item个数决定每个item的角度。

//初始化数据
 NSArray *items = @[[PNRadarChartDataItem dataItemWithValue:3 description:@"Art"],
                           [PNRadarChartDataItem dataItemWithValue:2 description:@"Math"],
                           [PNRadarChartDataItem dataItemWithValue:8 description:@"Sports"],
                           [PNRadarChartDataItem dataItemWithValue:5 description:@"Literature"],
                           [PNRadarChartDataItem dataItemWithValue:4 description:@"Other"],
                           ];

 for (int i=0;i<_chartData.count;i++) {
        PNRadarChartDataItem *item = (PNRadarChartDataItem *)[_chartData objectAtIndex:i];
        [descriptions addObject:item.textDescription];
        [values addObject:[NSNumber numberWithFloat:item.value]];
        CGFloat angleValue = (float)i/(float)[_chartData count]*2*M_PI;
        [angles addObject:[NSNumber numberWithFloat:angleValue]];
    }

2.拿到最大的值(我们这里是8),根据PNRadarChartLabelStyle来计算margin。然后计算出每小格的单位长度_lengthUnit。然后根据angleValue_lengthUnit计算出每个层5个点的坐标放在_pointsToWebArrayArray

  //拿到最大的值
  _maxValue = [self getMaxValueFromArray:values];
    CGFloat margin = 0;
    if (_labelStyle==PNRadarChartLabelStyleCircle) {
        margin = MIN(_centerX , _centerY)*3/10;
    }else if (_labelStyle==PNRadarChartLabelStyleHorizontal) {
        margin = [self getMaxWidthLabelFromArray:descriptions withFontSize:_fontSize];
    }
    CGFloat maxLength = ceil(MIN(_centerX, _centerY) - margin);
    int plotCircles = (_maxValue/_valueDivider);
    if (plotCircles > MAXCIRCLE) {
        NSLog(@"Circle number is higher than max");
        plotCircles = MAXCIRCLE;
        _valueDivider = _maxValue/plotCircles;
    }
    _lengthUnit = maxLength/plotCircles;
    NSArray *lengthArray = [self getLengthArrayWithCircleNum:(int)plotCircles];

    //get all the points and plot
    for (NSNumber *lengthNumber in lengthArray) {
        CGFloat length = [lengthNumber floatValue];
        [_pointsToWebArrayArray addObject:[self getWebPointWithLength:length angleArray:angles]];
    }

3.根据values数组里面的itemValue值和角度来计算点的坐标放在_pointsToPlotArray 里面。

 int section = 0;
    for (id value in values) {
        CGFloat valueFloat = [value floatValue];
        if (valueFloat>_maxValue) {
            NSString *reason = [NSString stringWithFormat:@"Value number is higher than max -value: %f - maxValue: %f",valueFloat,_maxValue];
            @throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
            return;
        }
        
        CGFloat length = valueFloat/_maxValue*maxLength;
        CGFloat angle = [[angles objectAtIndex:section] floatValue];
        CGFloat x = _centerX +length*cos(angle);
        CGFloat y = _centerY +length*sin(angle);
        NSValue* point = [NSValue valueWithCGPoint:CGPointMake(x, y)];
        [_pointsToPlotArray addObject:point];
        section++;
    }

4.根据最大值和角度设置lable"

    [self drawLabelWithMaxLength:maxLength labelArray:descriptions angleArray:angles];

PNChartDelegate

PNGenericChart的点击事件会通过PNChartDelegate协议接口给暴露出来。

你可能感兴趣的:(源码解析--PNChart)