PNChart 源碼解析

michael.l 7年前發布 | 10K 次閱讀 貝塞爾曲線 iOS開發 移動開發

一. 框架介紹

PNChart 是國內開發者開發的iOS圖表框架,現在已經7900多顆star了。它涵蓋了折線圖,餅圖,散點圖等圖表。圖表的可定制性很高,而且UI設計簡潔大方。

該框架分為兩層:視圖層和數據層。視圖層里有兩層繼承關系,第一層是所有類型圖表的父類 PNGenericChart ,第二層就是所有類型的圖表。提供一張圖來直觀感受一下:

層級圖

在這張圖里,需要注意以下幾點:

  1. 帶箭頭的線和不帶箭頭的線的區別。
  2. Data 類對應圖表的一組數據,因為當前類型的圖表支持多組數據(例如:餅狀圖沒有 Data 類,因為餅狀圖沒有多組數據,而折線圖 LineChart 是支持多組數據的,所以有 Data 類。
  3. Item 類負責將傳入圖表的某個真實值轉化為圖表中顯示的值,具體做法會在下文詳細講解。
  4. BarChart 類里面的每一根柱子都是 PNBar 的實例(該類型的圖表不在本篇講解的范圍之內)。

今天就來介紹一下該框架里的折線圖。一旦學會了折線圖的繪制,了解了繪圖原理,那么其他類型的圖表就可以觸類旁通。

上文提到過,該框架的折線圖是支持多組數據的,也就是在同一張圖表上顯示多條折線。先帶大家看一下效果圖:

折線圖

折線圖在效果上還是很簡潔美觀的(并支持動畫效果),如果現在的你還不知道如何使用 CAShapeLayer 和 UIBezierPath 畫圖并附加動畫效果,那么本篇源碼解析非常適合你。

閱讀本文之后,你可以掌握有關圖形繪制的相關知識,也可以掌握自定義各種圖形( UIView )的方法,而且你也應該有能力作出這樣的圖表,甚至更好!

在開始講解之前,我先粗略介紹一下利用 CAShapeLayer 畫圖的過程。這個過程有三個大前提:

  • 因為 UIView 是對 CALayer 的封裝,所以我們可以通過改變 UIView 所持有的 layer 屬性來直接改變 UIView 的顯示效果。
  • CAShapeLayer 是 CALayer 的子類。
  • CAShapeLayer 的使用是依賴于 UIBezierPath 的。 UIBezierPath 就是“路徑”,可以理解為形狀。不難理解,想象一下,如果我們想畫一個圖形,那么這個圖形的形狀(包括顏色)是必不可少的,而這個角色,就需要 UIBezierPath 來充當。

那么了這三個大前提,我們就可以知道如何畫圖了:

  1. 實例化一個 UIBezierPath ,并賦給 CAShapeLayer 實例的 path 屬性。
  2. 將這個 CAShapeLayer 的實例添加到 UIView 的 layer 上。

簡單的代碼演示上述過程:

UIBezierPath *path = [UIBezierPathbezierPath];
...自定義path...
CAShapeLayer *shapLayer = [CAShapeLayeralloc] init];
shapLayer.path = path;
[self.view.layeraddSubLayer:shapeLayer];

現在大致了解了畫圖的過程,我們來看一下該框架的作者是如何實現一個折線圖的吧!

二. 源碼解析

首先看一下整個繪制折線圖的步驟:

  1. 圖表的初始化。
  2. 獲取橫軸和縱軸的數據。
  3. 計算折線上所有拐點的x,y值。
  4. 計算每個拐點中間的圓圈的貝塞爾曲線(UIBezierPath)。
  5. 生成每個拐點上面的Label(可有可無)。
  6. 計算每條線段的貝塞爾曲線(UIBezierPath)。
  7. 將上面得到的貝塞爾曲線賦給每條線段和圓圈的layer(CAShapeLayer)。
  8. 繪制所有折線(所有線段+所有圓圈)。
  9. 添加動畫(可有可無)。
  10. 繪制x,y坐標軸。

在集合代碼具體講解之前,我們要清楚三點(非常非常重要):

  1. 此折線圖框架是可以設置拐點的樣式的:可以設置為沒有樣式,也可以設置有樣式:圓圈,方塊,三角形。
    • 如果沒有樣式,則是簡單的線段與線段的連接,在拐點處沒有任何其他控件。
    • 如果是有樣式的,那么這條折線里的每條線段(在本篇文章里統一說成線段)之間是 分離的 ,因為線段中間有一個拐點控件。本篇文章介紹的是圓圈樣式(如上圖所示,拐點控件是一個圓圈)。
  2. 上文提到過,該折線圖框架可以在一張圖表里同時顯示多條折線,也就是可以設置多組數據(一條折線對應一組數據)。因此,上面的3,4,5,6,7項都是用各自不同的一個數組保存的,數組里的每一個元素對應一條折線的數據。
  3. 既然同一個張圖表可以顯示多條折線:
    • 那么有些屬性就是這些折線共有的,比如橫坐標的value,這些屬性保存在 PNLineChart 的實例里面。
    • 有些屬性是每條折線私有的,比如每條折線的顏色,縱坐標value等等,這些屬性保存在 PNLineChartData 里面。每一條折線對應一個 PNLineChartData 實例。這些實例匯總到一個數組里面,這個數組由 PNLineChart 的實例管理。

在充分了解了這三點之后,我們結合一下代碼來看一下具體的實現:

1. 圖表的初始化

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
 
    if (self) {
        [self setupDefaultValues];
    }
 
    return self;
}
 

  • (void)setupDefaultValues {       [super setupDefaultValues];       ...       //四個內邊距     _chartMarginLeft = 25.0;     _chartMarginRight = 25.0;     _chartMarginTop = 25.0;     _chartMarginBottom = 25.0;       ...       //真正繪制圖表的畫布(CavanWidth)的寬高     _chartCavanWidth = self.frame.size.width - _chartMarginLeft - _chartMarginRight;     _chartCavanHeight = self.frame.size.height - _chartMarginBottom - _chartMarginTop;     ... } </code></pre>

    上面這段代碼我刻意省去了其他一些基本的設置,突出了圖表布局的設置。

    布局的設置是圖表繪制的前提,因為在最開始的時候,就應該計算出“畫布”,也就是圖表內容(不包括坐標軸和坐標label)的具體大小和位置(內邊距以內的部分)。

    在這里,我們需要獲取真正繪制圖表的畫布的寬高( _chartCavanWidth 和 _chartCavanHeight )。而且,要留意的是 _chartMarginLeft 在將來是要用作y軸Label的寬度,而 _chartMarginBottom 在將來是要用作x軸Label的高度的。

    用一張圖直觀看一下:

    整個控件的大小和畫布的大小

    2. 獲取橫軸和縱軸的數據

    現在畫布的位置和大小確定了,我們可以來看一下折線圖是怎么畫的了。

    整個圖表的繪制都基于三組數據(也可以是兩組,為什么是兩組,我稍后會給出解釋),在講解該框架是如何利用這些數據之前,我們來看一下這些數據是如何傳進圖表的:

        ...
        //設置x軸的數據
        [self.lineChartsetXLabels:@[@"SEP 1", @"SEP 2", @"SEP 3", @"SEP 4", @"SEP 5", @"SEP 6", @"SEP 7"]];
     
        //設置y軸的數據
        [self.lineChartsetYLabels:@[
                    @"0",@"50",@"100",@"150",@"200",@"250",@"300",
                    ]
        ];
     
        // Line Chart
        //設置每個點的y值
        NSArray *dataArray = @[@0.0, @180.1, @26.4, @202.2, @126.2, @167.2, @276.2];
        PNLineChartData *data = [PNLineChartDatanew];
        data.pointLabelColor = [UIColorblackColor];
        data.color = PN推terColor;
        data.alpha = 0.5f;
        data.itemCount = dataArray.count;
        data.inflexionPointStyle = PNLineChartPointStyleCircle;
     
        //這個block的作用是將上面的dataArray里的每一個值傳給line chart。
        data.getData = ^(NSUIntegerindex) {
            CGFloatyValue = [dataArray[index] floatValue];
            return [PNLineChartDataItemdataItemWithY:yValue];
        };
     
        //因為只有一條折線,所以只有一組數據
        self.lineChart.chartData = @[data];
     
        //繪制圖表
        [self.lineChartstrokeChart];
     
        //設置代理,響應點擊
        self.lineChart.delegate = self;
     
        [self.viewaddSubview:self.lineChart];
    

    上面的代碼我可以略去了很多多余的設置,目的是突出圖表數據的設置。

    不難看出,這里有三個數據傳給了lineChart:

    1.x軸的數據:

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

    這段代碼調用之后,實現了:

    1. 根據傳入的xLabel數組里元素的數量,內容寬度( _chartCavanWidth )和下邊距( _chartMarginBottom ),計算每個xlabel的size。
    2. 根據xLabel所需要展示的內容( NSString )和寬度,實例化所有的xLabel(包括內容,位置)并顯示出來,最后保存在 _xChartLabels 里面。

    2.y軸的數據:

    [self.lineChartsetYLabels:@[
                    @"0",@"50",@"100",@"150",@"200",@"250",@"300",
                    ]
        ];
    

    這段代碼調用之后,實現了:

    1. 根據傳入的yLabel數組里元素的數量,內容高度( _chartCavanHeight )和左邊距( _chartMarginLeft ),計算出每個ylabel的size。
    2. 根據xLabel所需要展示的內容( NSString )和寬度,實例化所有的yLabel(包括內容,位置)并顯示出來,最后保存在 _yChartLabels 里面。

    3.一條折線上每個點的實際值:

    NSArray *dataArray = @[@0.0, @180.1, @26.4, @202.2, @126.2, @167.2, @276.2];
     
    data.getData = ^(NSUIntegerindex) {
            CGFloatyValue = [dataArray[index] floatValue];
            return [PNLineChartDataItemdataItemWithY:yValue];
        };
     
    self.lineChart.chartData = @[data];
    

    著重講一下block:為什么不直接把這個數組( dataArray )作為line chart的屬性傳進去呢?我認為作者是想提供一個接口給用戶一個自己轉化y值的機會。

    像上文所說的,這里1,2是屬于 lineChart 的數據,它適用于這張圖表上所有的折線的。而3是屬于某一條折線的。

    現在回答一下為什么可以只傳入兩組數據:因為y軸數據可以由每個點的實際值數組得出。可以簡單想一下,我們可以獲取這些真實值里面的最大值,然后將它n等分,就自然得到了y軸數據了。

    我們已經布局了x軸和y軸的所有label,現在開始真正計算圖表的數據了。

    注意:下面要介紹的3,4,5,6項都是在同一方法中計算出來,為了避免代碼過長,我將每個部分分解開來做出解釋。因為在同一方法里,所以這些涉及到for循環的語句是一致的。

    整個圖表的繪制都是依賴于數據的處理,所以3,4,5,6項也是理解該框架的一個關鍵!

    首先,我們需要計算每個數據點(拐點)的準確位置:

    3. 計算折線上所有拐點的x,y值。

    //遍歷圖表里每條折線
    //還記得chartData屬性么?它是用來保存多組折線的數據的,在這里只有一個折線,所以這個循環只循環一次)
    for (NSUIntegerlineIndex = 0; lineIndex 
    

    在這里需要注意兩點:

    1. 這里的 pathPoints 對應的是 lineChart 的 _pathPoints 屬性。它是一個二維數組,保存每條折線上所有點的 CGPoint 。
    2. y值的計算:是需要從y的真實值轉化為這個拐點在圖表里的y坐標,轉化方法的實現(仔細看幾遍就懂了):
    - (CGFloat)yValuePositionInLineChart:(CGFloat)y {
     
        CGFloatinnerGrade;//真實的最大值與最小值的差 與 當前點與最小值的差 的比值
     
        if (!(_yValueMax - _yValueMin)) {
     
            //特殊情況:當_yValueMax和_yValueMin相等的時候
            innerGrade = 0.5;
     
        } else {
     
            innerGrade = ((CGFloat) y - _yValueMin) / (_yValueMax - _yValueMin);
        }
     
        //innerGrade 與畫布的高度(_chartCavanHeight)相乘,就能得出在畫布中的高度
        return _chartCavanHeight - (innerGrade * _chartCavanHeight) - (_yLabelHeight / 2) + _chartMarginTop;
    }
    

    4. 計算每個拐點中間的圓圈的貝塞爾曲線(UIBezierPath)

    //遍歷圖表里每條折線
    for (NSUIntegerlineIndex = 0; lineIndex 
    

    在這里, pointsPath 對應的是 lineChart 的 _pointsPath 屬性。它是一個一維數組,保存每條折線上的圓圈貝塞爾曲線(UIBezierPath)。

    5. 生成每個拐點上面的Label(可有可無)

    //遍歷圖表里每條折線
    for (NSUIntegerlineIndex = 0; lineIndex 
    

    注意,在這里,這些label的實現是通過一個 CATextLayer 實現的,并不是生成一個個 Label 放在數組里保存,具體實現方法如下:

    - (CATextLayer *)createPointLabelFor:(CGFloat)gradepointCenter:(CGPoint)pointCenterwidth:(CGFloat)widthwithChartData:(PNLineChartData *)chartData {
     
        //grade:提供textLayer顯示的數值
        //pointCenter:根據pointCenter算出textLayer的x,y
        //width:根據width得到textLayer的總寬度
        //chartData:獲取chartData里保存的textLayer上應該保存的字體大小和顏色
     
        CATextLayer *textLayer = [[CATextLayeralloc] init];
        [textLayersetAlignmentMode:kCAAlignmentCenter];
     
        //設置textLayer的背景色
        [textLayersetForegroundColor:[chartData.pointLabelColorCGColor]];
        [textLayersetBackgroundColor:self.backgroundColor.CGColor];
     
        //設置textLayer的字體大小和顏色
        if (chartData.pointLabelFont != nil) {
            [textLayersetFont:(__bridgeCFTypeRef) (chartData.pointLabelFont)];
            textLayer.fontSize = [chartData.pointLabelFontpointSize];
        }
     
        //設置textLayer的高度
        CGFloattextHeight = (CGFloat) (textLayer.fontSize * 1.1);
     
        CGFloattextWidth = width * 8;
        CGFloattextStartPosY;
     
        textStartPosY = pointCenter.y - textLayer.fontSize;
     
        [self.layeraddSublayer:textLayer];
     
        //設置textLayer的文字顯示格式
        if (chartData.pointLabelFormat != nil) {
            [textLayersetString:[[NSStringalloc] initWithFormat:chartData.pointLabelFormat, grade]];
        } else {
            [textLayersetString:[[NSStringalloc] initWithFormat:_yLabelFormat, grade]];
        }
     
        //設置textLayer的位置和scale(1x,2x,3x)
        [textLayersetFrame:CGRectMake(0, 0, textWidth, textHeight)];
        [textLayersetPosition:CGPointMake(pointCenter.x, textStartPosY)];
        textLayer.contentsScale = [UIScreenmainScreen].scale;
     
        return textLayer;
    }
    

    6. 計算每條線段的貝塞爾曲線(UIBezierPath)

    //遍歷圖表里每條折線
    for (NSUIntegerlineIndex = 0; lineIndex  *progressLines = [NSMutableArraynew];
     
        //chartPath(二維數組):保存所有折線上所有線段的貝塞爾曲線。現在只有一條折線,所以只有一個元素
        [chartPathinsertObject:progressLinesatIndex:lineIndex];
     
        //progressLinePaths的每個元素是一個字典,字典里存放每一條線段的端點(from,to)
        NSMutableArray *> *progressLinePaths = [NSMutableArraynew];
     
        int last_x = 0;
        int last_y = 0;
     
        //遍歷每條折線里的每一段
        for (NSUInteger i = 0; i  0) {
                //x,y的算法參考上文第三項
                // 計算index為0以后的點的位置
                float distance = (float) sqrt(pow(x - last_x, 2) + pow(y - last_y, 2));
                float last_x1 = last_x + (inflexionWidth / 2) / distance * (x - last_x);
                float last_y1 = last_y + (inflexionWidth / 2) / distance * (y - last_y);
                float x1 = x - (inflexionWidth / 2) / distance * (x - last_x);
                float y1 = y - (inflexionWidth / 2) / distance * (y - last_y);
     
                //當前線段的端點
                from = [NSValuevalueWithCGPoint:CGPointMake(last_x1, last_y1)];
                to = [NSValuevalueWithCGPoint:CGPointMake(x1, y1)];
     
     
                if(from != nil && to != nil) {
                    //保存每一段的端點
                    [progressLinePathsaddObject:@{@"from": from,  @"to":to}];
                    //保存所有的端點
                    [lineStartEndPointsArrayaddObject:from];
                    [lineStartEndPointsArrayaddObject:to];
                }
                //保存所有折點的坐標
                [linePointsArrayaddObject:[NSValuevalueWithCGPoint:CGPointMake(x, y)]];
                //將當前的x轉化為下一個點的last_x(y也一樣)
                last_x = x;
                last_y = y;
            }
        }
     
        //pointsOfPath:保存所有折線里的所有線段兩端的端點
        [pointsOfPathaddObject:[lineStartEndPointsArraycopy]];
     
        //根據每一條線段的兩個端點,成生每條線段的貝塞爾曲線
        for (NSDictionary *itemin progressLinePaths) {
            NSArray *calculatedRanges =
            ...
     
            for (NSDictionary *rangein calculatedRanges) {
     
                UIBezierPath *currentProgressLine = [UIBezierPathbezierPath];
                [currentProgressLinemoveToPoint:[range[@"from"] CGPointValue]];
                [currentProgressLineaddLineToPoint:[range[@"to"] CGPointValue]];
                [progressLinesaddObject:currentProgressLine];
     
            }
        }    
    }
    

    7. 將上面得到的貝塞爾曲線賦給每條線段和圓圈的layer(CAShapeLayer)。

    7.1 所有線段的layer:

    - (void)populateChartLines {
     
        //遍歷每條線段
        for (NSUIntegerlineIndex = 0; lineIndex  *progressLines = self.chartPath[lineIndex];
     
            ...
     
            //_chartLineArray:二維數組,裝載每個chartData對應的一個數組。這個數組的元素是這一條折線上所有線段對應的CAShapeLayer
            [self.chartLineArray[lineIndex] removeAllObjects];
     
            NSUIntegerprogressLineIndex = 0;;
     
            //遍歷含有UIBezierPath對象元素的數組。在每個循環里新建一個CAShapeLayer對象,將UIBezierPath賦給它。
            for (UIBezierPath *progressLinePathin progressLines) {
     
                PNLineChartData *chartData = self.chartData[lineIndex];
                CAShapeLayer *chartLine = [CAShapeLayerlayer];
     
                ...
     
                //將當前線段的UIBezierPath賦給當前線段的CAShapeLayer
                chartLine.path = progressLinePath.CGPath;
     
                //添加layer
                [self.layeraddSublayer:chartLine];
     
                //保存當前線段的layer
                [self.chartLineArray[lineIndex] addObject:chartLine];
                progressLineIndex++;
            }
        }
    }
    

    7.2 所有圓圈的layer:

    - (void)recreatePointLayers {
  •     for (PNLineChartData chartDatain _chartData) {           // create as many chart line layers as there are data-lines         [self.chartLineArrayaddObject:[NSMutableArraynew]];           // create point         CAShapeLayer pointLayer = [CAShapeLayerlayer];         pointLayer.strokeColor = [[chartData.colorcolorWithAlphaComponent:chartData.alpha] CGColor];         pointLayer.lineCap = kCALineCapRound;         pointLayer.lineJoin = kCALineJoinBevel;         pointLayer.fillColor = nil;         pointLayer.lineWidth = chartData.lineWidth;         [self.layeraddSublayer:pointLayer];         [self.chartPointArrayaddObject:pointLayer];     } } </code></pre>

    注意,這里并沒有將所有圓圈的 UIBezierPath 賦給對應的 layer ,而是在下一步,繪圖的時候做的。

    8.繪制所有折線(所有線段+所有圓圈)&& 9. 添加動畫

    - (void)strokeChart {
     
        ...
     
        // 繪制所有折線(所有線段+所有圓圈)
        // 遍歷所有折線
        for (NSUIntegerlineIndex = 0; lineIndex  *chartLines =self.chartLineArray[lineIndex];
     
            //當前折線的所有圓圈的CAShapeLayer
            CAShapeLayer *pointLayer = (CAShapeLayer *) self.chartPointArray[lineIndex];
     
            //開始繪制折線
            UIGraphicsBeginImageContext(self.frame.size);
     
            ...
     
            //當前折線的所有線段的UIBezierPath
            NSArray *progressLines = _chartPath[lineIndex];
     
            //當前折線的所有圓圈的UIBezierPath
            UIBezierPath *pointPath = _pointPath[lineIndex];
     
            //7.2將圓圈的UIBezierPath賦給了圓圈的CAShapeLayer
            pointLayer.path = pointPath.CGPath;
     
            //添加動畫
            [CATransactionbegin];
     
            for (NSUIntegerindex = 0; index 
    

    這里要注意兩點:

    1.如果想給layer添加動畫,只需要實例化一個animation(在這里是 CABasicAnimation )并調用layer的 addAnimation: 方法即可。我們看一下關于 CABasicAnimation 的實例化代碼:

    - (CABasicAnimation *)pathAnimation {
        if (self.displayAnimated && !_pathAnimation) {
            _pathAnimation = [CABasicAnimationanimationWithKeyPath:@"strokeEnd"];
            //持續時間
            _pathAnimation.duration = 1.0;
            //類型
            _pathAnimation.timingFunction = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseInEaseOut];
            _pathAnimation.fromValue = @0.0f;
            _pathAnimation.toValue = @1.0f;
        }
        if(!self.displayAnimated) {
            _pathAnimation = nil;
        }
        return _pathAnimation;
    }
    

    2.在這里調用了 setNeedsDisplay 方法之后,會調用 drawRect: 方法,在這個方法里,完成了x,y坐標軸的繪制:

    10.繪制x,y坐標軸

    - (void)drawRect:(CGRect)rect {
     
        //繪制坐標軸和背景豎線
        if (self.isShowCoordinateAxis) {
     
            CGFloatyAxisOffset = 10.f;
     
            CGContextRefctx = UIGraphicsGetCurrentContext();
            UIGraphicsPopContext();
            UIGraphicsPushContext(ctx);
            CGContextSetLineWidth(ctx, self.axisWidth);
            CGContextSetStrokeColorWithColor(ctx, [self.axisColorCGColor]);
     
            CGFloatxAxisWidth = CGRectGetWidth(rect) - (_chartMarginLeft + _chartMarginRight) / 2;
            CGFloatyAxisHeight = _chartMarginBottom + _chartCavanHeight;
     
            // 繪制xy軸
            CGContextMoveToPoint(ctx, _chartMarginBottom + yAxisOffset, 0);
            CGContextAddLineToPoint(ctx, _chartMarginBottom + yAxisOffset, yAxisHeight);
            CGContextAddLineToPoint(ctx, xAxisWidth, yAxisHeight);
            CGContextStrokePath(ctx);
     
            // 繪制y軸的箭頭
            CGContextMoveToPoint(ctx, _chartMarginBottom + yAxisOffset - 3, 6);
            CGContextAddLineToPoint(ctx, _chartMarginBottom + yAxisOffset, 0);
            CGContextAddLineToPoint(ctx, _chartMarginBottom + yAxisOffset + 3, 6);
            CGContextStrokePath(ctx);
     
            // 繪制x軸的箭頭
            CGContextMoveToPoint(ctx, xAxisWidth - 6, yAxisHeight - 3);
            CGContextAddLineToPoint(ctx, xAxisWidth, yAxisHeight);
            CGContextAddLineToPoint(ctx, xAxisWidth - 6, yAxisHeight + 3);
            CGContextStrokePath(ctx);
     
            //繪制x軸和y軸的label
            if (self.showLabel) {
     
                // 繪制x軸的小分割線
                CGPointpoint;
                for (NSUInteger i = 0; i 
    

    到這里,一張完整的圖表就可以畫出來了。但是當前繪制的圖表的折線都是直線,在上面還展示了一張曲線圖。那么如果想繪制帶有曲線的折線圖應該怎么做呢?對,就是在貝塞爾曲線上下功夫。

    當我們獲取了所有線段的端點數組后,我們可以通過他們繪制彎曲的貝塞爾曲線(注意:該方法是對應上面對第6項的下半部分:生成每一個線段對貝塞爾曲線):

    //_showSmoothLines是用來控制是否繪制曲線折線的開關屬性
    if (self.showSmoothLines && chartData.itemCount >= 4) {
     
        for (NSDictionary *itemin progressLinePaths) {
     
            ...
     
            for (NSDictionary *rangein calculatedRanges) {
     
                UIBezierPath *currentProgressLine = [UIBezierPathbezierPath];
                CGPointsegmentP1 = [range[@"from"] CGPointValue];
                CGPointsegmentP2 = [range[@"to"] CGPointValue];
     
                [currentProgressLinemoveToPoint:segmentP1];
     
                CGPointmidPoint = [PNLineChartmidPointBetweenPoint1:segmentP1andPoint2:segmentP2];
     
                //以每條線段以中間點為分割點,分成兩組。每一組形成柔和的外凸曲線,而不是內凹
                [currentProgressLineaddQuadCurveToPoint:midPoint
                                            controlPoint:[PNLineChartcontrolPointBetweenPoint1:midPointandPoint2:segmentP1]];
     
                [currentProgressLineaddQuadCurveToPoint:segmentP2
                                            controlPoint:[PNLineChartcontrolPointBetweenPoint1:midPointandPoint2:segmentP2]];
     
                [progressLinesaddObject:currentProgressLine];
                [progressLineColorsaddObject:range[@"color"]];
            }
        }
    }
    

    注意一下生成彎曲的貝塞爾曲線的方法: controlPointBetweenPoint1:andPoint2 :

    //返回的點的x:是兩點的中間;返回的點的y:與第二個點保持一致
  • (CGPoint)controlPointBetweenPoint1:(CGPoint)point1andPoint2:(CGPoint)point2 {       //線段兩端的中間點     CGPointcontrolPoint = [self midPointBetweenPoint1:point1andPoint2:point2];       //末端點 和  中間點y的差     CGFloatdiffY = abs((int) (point2.y - controlPoint.y));       if (point1.y  point2.y)     //如果后端點更高         controlPoint.y -= diffY;       return controlPoint; } </code></pre>

    OK,這樣一來,直線的曲線圖還有曲線的曲線圖就大概掌握了。不過還差一個東西,就是圖表對點擊的響應。

    我們需要思考一下:既然一張圖表里可以顯示多條折線,所以,當手指點擊圖表上的點以后,應該同時返回兩個數據:

    1. 點擊了哪條折線上的這個點。
    2. 點擊了這條折線上的哪個點。

    該框架的作者很好地完成了這兩個任務,我們來看一下他是如何實現的:

    響應點擊的代理方法

    點擊了哪條折線的判斷

    - (void)touchPoint:(NSSet *)toucheswithEvent:(UIEvent *)event {
        // Get the point user touched
        UITouch *touch = [touchesanyObject];
        CGPointtouchPoint = [touchlocationInView:self];
     
        for (NSUInteger p = 0; p  *pathsin _chartPath) {
                        for (UIBezierPath *pathin paths) {
                            //如果當前點處于UIBezierPath曲線上
                            BOOL pointContainsPath = CGPathContainsPoint(path.CGPath, NULL, p1, NO);
                            if (pointContainsPath) {
                                //點擊了某一條折線
                                [_delegateuserClickedOnLinePoint:touchPointlineIndex:lineIndex];
                                return;
                            }
                        }
                        lineIndex++;
                    }
                }
            }
        }
    }
    

    點擊了哪個點的判斷

    - (void)touchKeyPoint:(NSSet *)toucheswithEvent:(UIEvent *)event {
        // Get the point user touched
        UITouch *touch = [touchesanyObject];
        CGPointtouchPoint = [touchlocationInView:self];
     
        for (NSUInteger p = 0; p 
    

    這下就完整了,一個帶有響應功能的圖表就做好啦!

    關于自定義UIView

    這里只是將圖表的 layer 加在了 UIView 的layer上,那如果想完全自定義view的話,只需將圖表的 layer 完全賦給 UIView 的layer即可,這樣一來,想要畫出任意形狀的 UIView 都可以。

    三. 最后的話

    關于圖表的繪制,相對貝塞爾曲線與 CALayer 來說,數據的處理是一個比較麻煩的點。但是一旦學會了折線圖的繪制,了解了繪圖原理,那么其他類型的圖表就可以觸類旁通。

     

    來自:http://ios.jobbole.com/92898/

     

 本文由用戶 michael.l 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!