一. 框架介紹
PNChart是國內開發者開發的iOS圖表框架,現在已經7900多顆star了。它涵蓋了折線圖,餅圖,散點圖等圖表。圖表的可定製性很高,而且UI設計簡潔大方。
該框架分為兩層:檢視層和資料層。檢視層裡有兩層繼承關係,第一層是所有型別圖表的父類PNGenericChart
,第二層就是所有型別的圖表。提供一張圖來直觀感受一下:
在這張圖裡,需要注意以下幾點:
- 帶箭頭的線和不帶箭頭的線的區別。
Data
類對應圖表的一組資料,因為當前型別的圖表支援多組資料(例如:餅狀圖沒有Data
類,因為餅狀圖沒有多組資料,而折線圖LineChart
是支援多組資料的,所以有Data
類。Item
類負責將傳入圖表的某個真實值轉化為圖表中顯示的值,具體做法會在下文詳細講解。BarChart
類裡面的每一根柱子都是PNBar
的例項(該型別的圖表不在本篇講解的範圍之內)。
今天就來介紹一下該框架裡的折線圖。一旦學會了折線圖的繪製,瞭解了繪圖原理,那麼其他型別的圖表就可以觸類旁通。
上文提到過,該框架的折線圖是支援多組資料的,也就是在同一張圖表上顯示多條折線。先帶大家看一下效果圖:
折線圖在效果上還是很簡潔美觀的(並支援動畫效果),如果現在的你還不知道如何使用CAShapeLayer
和UIBezierPath
畫圖並附加動畫效果,那麼本篇原始碼解析非常適合你。
閱讀本文之後,你可以掌握有關圖形繪製的相關知識,也可以掌握自定義各種圖形(UIView
)的方法,而且你也應該有能力作出這樣的圖表,甚至更好!
在開始講解之前,我先粗略介紹一下利用CAShapeLayer
畫圖的過程。這個過程有三個大前提:
- 因為
UIView
是對CALayer
的封裝,所以我們可以通過改變UIView
所持有的layer
屬性來直接改變UIView
的顯示效果。 CAShapeLayer
是CALayer
的子類。CAShapeLayer
的使用是依賴於UIBezierPath
的。UIBezierPath
就是“路徑”,可以理解為形狀。不難理解,想象一下,如果我們想畫一個圖形,那麼這個圖形的形狀(包括顏色)是必不可少的,而這個角色,就需要UIBezierPath
來充當。
那麼了這三個大前提,我們就可以知道如何畫圖了:
- 例項化一個
UIBezierPath
,並賦給CAShapeLayer
例項的path
屬性。 - 將這個
CAShapeLayer
的例項新增到UIView
的layer
上。
簡單的程式碼演示上述過程:
1 2 3 4 5 |
UIBezierPath *path = [UIBezierPath bezierPath]; ...自定義path... CAShapeLayer *shapLayer = [CAShapeLayer alloc] init]; shapLayer.path = path; [self.view.layer addSubLayer:shapeLayer]; |
現在大致瞭解了畫圖的過程,我們來看一下該框架的作者是如何實現一個折線圖的吧!
二. 原始碼解析
首先看一下整個繪製折線圖的步驟:
- 圖表的初始化。
- 獲取橫軸和縱軸的資料。
- 計算折線上所有拐點的x,y值。
- 計算每個拐點中間的圓圈的貝塞爾曲線(UIBezierPath)。
- 生成每個拐點上面的Label(可有可無)。
- 計算每條線段的貝塞爾曲線(UIBezierPath)。
- 將上面得到的貝塞爾曲線賦給每條線段和圓圈的layer(CAShapeLayer)。
- 繪製所有折線(所有線段+所有圓圈)。
- 新增動畫(可有可無)。
- 繪製x,y座標軸。
在集合程式碼具體講解之前,我們要清楚三點(非常非常重要):
- 此折線圖框架是可以設定拐點的樣式的:可以設定為沒有樣式,也可以設定有樣式:圓圈,方塊,三角形。
- 如果沒有樣式,則是簡單的線段與線段的連線,在拐點處沒有任何其他控制元件。
- 如果是有樣式的,那麼這條折線裡的每條線段(在本篇文章裡統一說成線段)之間是分離的,因為線段中間有一個拐點控制元件。本篇文章介紹的是圓圈樣式(如上圖所示,拐點控制元件是一個圓圈)。
- 上文提到過,該折線圖框架可以在一張圖表裡同時顯示多條折線,也就是可以設定多組資料(一條折線對應一組資料)。因此,上面的3,4,5,6,7項都是用各自不同的一個陣列儲存的,陣列裡的每一個元素對應一條折線的資料。
- 既然同一個張圖表可以顯示多條折線:
- 那麼有些屬性就是這些折線共有的,比如橫座標的value,這些屬性儲存在
PNLineChart
的例項裡面。 - 有些屬性是每條折線私有的,比如每條折線的顏色,縱座標value等等,這些屬性儲存在
PNLineChartData
裡面。每一條折線對應一個PNLineChartData
例項。這些例項彙總到一個陣列裡面,這個陣列由PNLineChart
的例項管理。
- 那麼有些屬性就是這些折線共有的,比如橫座標的value,這些屬性儲存在
在充分了解了這三點之後,我們結合一下程式碼來看一下具體的實現:
1. 圖表的初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
- (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; ... } |
上面這段程式碼我刻意省去了其他一些基本的設定,突出了圖表佈局的設定。
佈局的設定是圖表繪製的前提,因為在最開始的時候,就應該計算出“畫布”,也就是圖表內容(不包括座標軸和座標label)的具體大小和位置(內邊距以內的部分)。
在這裡,我們需要獲取真正繪製圖表的畫布的寬高(
_chartCavanWidth
和_chartCavanHeight
)。而且,要留意的是_chartMarginLeft
在將來是要用作y軸Label的寬度,而_chartMarginBottom
在將來是要用作x軸Label的高度的。
用一張圖直觀看一下:
2. 獲取橫軸和縱軸的資料
現在畫布的位置和大小確定了,我們可以來看一下折線圖是怎麼畫的了。
整個圖表的繪製都基於三組資料(也可以是兩組,為什麼是兩組,我稍後會給出解釋),在講解該框架是如何利用這些資料之前,我們來看一下這些資料是如何傳進圖表的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
... //設定x軸的資料 [self.lineChart setXLabels:@[@"SEP 1", @"SEP 2", @"SEP 3", @"SEP 4", @"SEP 5", @"SEP 6", @"SEP 7"]]; //設定y軸的資料 [self.lineChart setYLabels:@[ @"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 = [PNLineChartData new]; data.pointLabelColor = [UIColor blackColor]; data.color = PNTwitterColor; data.alpha = 0.5f; data.itemCount = dataArray.count; data.inflexionPointStyle = PNLineChartPointStyleCircle; //這個block的作用是將上面的dataArray裡的每一個值傳給line chart。 data.getData = ^(NSUInteger index) { CGFloat yValue = [dataArray[index] floatValue]; return [PNLineChartDataItem dataItemWithY:yValue]; }; //因為只有一條折線,所以只有一組資料 self.lineChart.chartData = @[data]; //繪製圖表 [self.lineChart strokeChart]; //設定代理,響應點選 self.lineChart.delegate = self; [self.view addSubview:self.lineChart]; |
上面的程式碼我可以略去了很多多餘的設定,目的是突出圖表資料的設定。
不難看出,這裡有三個資料傳給了lineChart:
1.x軸的資料:
1 |
[self.lineChart setXLabels:@[@"SEP 1", @"SEP 2", @"SEP 3", @"SEP 4", @"SEP 5", @"SEP 6", @"SEP 7"]]; |
這段程式碼呼叫之後,實現了:
- 根據傳入的xLabel陣列裡元素的數量,內容寬度(
_chartCavanWidth
)和下邊距(_chartMarginBottom
),計算每個xlabel的size。- 根據xLabel所需要展示的內容(
NSString
)和寬度,例項化所有的xLabel(包括內容,位置)並顯示出來,最後儲存在_xChartLabels
裡面。
2.y軸的資料:
1 2 3 4 |
[self.lineChart setYLabels:@[ @"0",@"50",@"100",@"150",@"200",@"250",@"300", ] ]; |
這段程式碼呼叫之後,實現了:
- 根據傳入的yLabel陣列裡元素的數量,內容高度(
_chartCavanHeight
)和左邊距(_chartMarginLeft
),計算出每個ylabel的size。- 根據xLabel所需要展示的內容(
NSString
)和寬度,例項化所有的yLabel(包括內容,位置)並顯示出來,最後儲存在_yChartLabels
裡面。
3.一條折線上每個點的實際值:
1 2 3 4 5 6 7 8 |
NSArray *dataArray = @[@0.0, @180.1, @26.4, @202.2, @126.2, @167.2, @276.2]; data.getData = ^(NSUInteger index) { CGFloat yValue = [dataArray[index] floatValue]; return [PNLineChartDataItem dataItemWithY: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值。
1 2 3 |
//遍歷圖表裡每條折線 //還記得chartData屬性麼?它是用來儲存多組折線的資料的,在這裡只有一個折線,所以這個迴圈只迴圈一次) for (NSUInteger lineIndex = 0; lineIndex |
在這裡需要注意兩點:
- 這裡的
pathPoints
對應的是lineChart
的_pathPoints
屬性。它是一個二維陣列,儲存每條折線上所有點的CGPoint
。- y值的計算:是需要從y的真實值轉化為這個拐點在圖表裡的y座標,轉化方法的實現(仔細看幾遍就懂了):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (CGFloat)yValuePositionInLineChart:(CGFloat)y { CGFloat innerGrade;//真實的最大值與最小值的差 與 當前點與最小值的差 的比值 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)
1 2 |
//遍歷圖表裡每條折線 for (NSUInteger lineIndex = 0; lineIndex |
在這裡,
pointsPath
對應的是lineChart
的_pointsPath
屬性。它是一個一維陣列,儲存每條折線上的圓圈貝塞爾曲線(UIBezierPath)。
5. 生成每個拐點上面的Label(可有可無)
1 2 |
//遍歷圖表裡每條折線 for (NSUInteger lineIndex = 0; lineIndex |
注意,在這裡,這些label的實現是通過一個
CATextLayer
實現的,並不是生成一個個Label
放在陣列裡儲存,具體實現方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
- (CATextLayer *)createPointLabelFor:(CGFloat)grade pointCenter:(CGPoint)pointCenter width:(CGFloat)width withChartData:(PNLineChartData *)chartData { //grade:提供textLayer顯示的數值 //pointCenter:根據pointCenter算出textLayer的x,y //width:根據width得到textLayer的總寬度 //chartData:獲取chartData裡儲存的textLayer上應該儲存的字型大小和顏色 CATextLayer *textLayer = [[CATextLayer alloc] init]; [textLayer setAlignmentMode:kCAAlignmentCenter]; //設定textLayer的背景色 [textLayer setForegroundColor:[chartData.pointLabelColor CGColor]]; [textLayer setBackgroundColor:self.backgroundColor.CGColor]; //設定textLayer的字型大小和顏色 if (chartData.pointLabelFont != nil) { [textLayer setFont:(__bridge CFTypeRef) (chartData.pointLabelFont)]; textLayer.fontSize = [chartData.pointLabelFont pointSize]; } //設定textLayer的高度 CGFloat textHeight = (CGFloat) (textLayer.fontSize * 1.1); CGFloat textWidth = width * 8; CGFloat textStartPosY; textStartPosY = pointCenter.y - textLayer.fontSize; [self.layer addSublayer:textLayer]; //設定textLayer的文字顯示格式 if (chartData.pointLabelFormat != nil) { [textLayer setString:[[NSString alloc] initWithFormat:chartData.pointLabelFormat, grade]]; } else { [textLayer setString:[[NSString alloc] initWithFormat:_yLabelFormat, grade]]; } //設定textLayer的位置和scale(1x,2x,3x) [textLayer setFrame:CGRectMake(0, 0, textWidth, textHeight)]; [textLayer setPosition:CGPointMake(pointCenter.x, textStartPosY)]; textLayer.contentsScale = [UIScreen mainScreen].scale; return textLayer; } |
6. 計算每條線段的貝塞爾曲線(UIBezierPath)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
//遍歷圖表裡每條折線 for (NSUInteger lineIndex = 0; lineIndex *progressLines = [NSMutableArray new]; //chartPath(二維陣列):儲存所有折線上所有線段的貝塞爾曲線。現在只有一條折線,所以只有一個元素 [chartPath insertObject:progressLines atIndex:lineIndex]; //progressLinePaths的每個元素是一個字典,字典裡存放每一條線段的端點(from,to) NSMutableArray *> *progressLinePaths = [NSMutableArray new]; 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 = [NSValue valueWithCGPoint:CGPointMake(last_x1, last_y1)]; to = [NSValue valueWithCGPoint:CGPointMake(x1, y1)]; if(from != nil && to != nil) { //儲存每一段的端點 [progressLinePaths addObject:@{@"from": from, @"to":to}]; //儲存所有的端點 [lineStartEndPointsArray addObject:from]; [lineStartEndPointsArray addObject:to]; } //儲存所有折點的座標 [linePointsArray addObject:[NSValue valueWithCGPoint:CGPointMake(x, y)]]; //將當前的x轉化為下一個點的last_x(y也一樣) last_x = x; last_y = y; } } //pointsOfPath:儲存所有折線裡的所有線段兩端的端點 [pointsOfPath addObject:[lineStartEndPointsArray copy]]; //根據每一條線段的兩個端點,成生每條線段的貝塞爾曲線 for (NSDictionary *item in progressLinePaths) { NSArray *calculatedRanges = ... for (NSDictionary *range in calculatedRanges) { UIBezierPath *currentProgressLine = [UIBezierPath bezierPath]; [currentProgressLine moveToPoint:[range[@"from"] CGPointValue]]; [currentProgressLine addLineToPoint:[range[@"to"] CGPointValue]]; [progressLines addObject:currentProgressLine]; } } } |
7. 將上面得到的貝塞爾曲線賦給每條線段和圓圈的layer(CAShapeLayer)。
7.1 所有線段的layer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
- (void)populateChartLines { //遍歷每條線段 for (NSUInteger lineIndex = 0; lineIndex *progressLines = self.chartPath[lineIndex]; ... //_chartLineArray:二維陣列,裝載每個chartData對應的一個陣列。這個陣列的元素是這一條折線上所有線段對應的CAShapeLayer [self.chartLineArray[lineIndex] removeAllObjects]; NSUInteger progressLineIndex = 0;; //遍歷含有UIBezierPath物件元素的陣列。在每個迴圈裡新建一個CAShapeLayer物件,將UIBezierPath賦給它。 for (UIBezierPath *progressLinePath in progressLines) { PNLineChartData *chartData = self.chartData[lineIndex]; CAShapeLayer *chartLine = [CAShapeLayer layer]; ... //將當前線段的UIBezierPath賦給當前線段的CAShapeLayer chartLine.path = progressLinePath.CGPath; //新增layer [self.layer addSublayer:chartLine]; //儲存當前線段的layer [self.chartLineArray[lineIndex] addObject:chartLine]; progressLineIndex++; } } } |
7.2 所有圓圈的layer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (void)recreatePointLayers { - for (PNLineChartData *chartData in _chartData) { // create as many chart line layers as there are data-lines [self.chartLineArray addObject:[NSMutableArray new]]; // create point CAShapeLayer *pointLayer = [CAShapeLayer layer]; pointLayer.strokeColor = [[chartData.color colorWithAlphaComponent:chartData.alpha] CGColor]; pointLayer.lineCap = kCALineCapRound; pointLayer.lineJoin = kCALineJoinBevel; pointLayer.fillColor = nil; pointLayer.lineWidth = chartData.lineWidth; [self.layer addSublayer:pointLayer]; [self.chartPointArray addObject:pointLayer]; } } |
注意,這裡並沒有將所有圓圈的
UIBezierPath
賦給對應的layer
,而是在下一步,繪圖的時候做的。
8.繪製所有折線(所有線段+所有圓圈)&& 9. 新增動畫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
- (void)strokeChart { ... // 繪製所有折線(所有線段+所有圓圈) // 遍歷所有折線 for (NSUInteger lineIndex = 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; //新增動畫 [CATransaction begin]; for (NSUInteger index = 0; index |
這裡要注意兩點:
1.如果想給layer新增動畫,只需要例項化一個animation(在這裡是
CABasicAnimation
)並呼叫layer的addAnimation:
方法即可。我們看一下關於CABasicAnimation
的例項化程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- (CABasicAnimation *)pathAnimation { if (self.displayAnimated && !_pathAnimation) { _pathAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; //持續時間 _pathAnimation.duration = 1.0; //型別 _pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; _pathAnimation.fromValue = @0.0f; _pathAnimation.toValue = @1.0f; } if(!self.displayAnimated) { _pathAnimation = nil; } return _pathAnimation; } |
2.在這裡呼叫了
setNeedsDisplay
方法之後,會呼叫drawRect:
方法,在這個方法裡,完成了x,y座標軸的繪製:
10.繪製x,y座標軸
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
- (void)drawRect:(CGRect)rect { //繪製座標軸和背景豎線 if (self.isShowCoordinateAxis) { CGFloat yAxisOffset = 10.f; CGContextRef ctx = UIGraphicsGetCurrentContext(); UIGraphicsPopContext(); UIGraphicsPushContext(ctx); CGContextSetLineWidth(ctx, self.axisWidth); CGContextSetStrokeColorWithColor(ctx, [self.axisColor CGColor]); CGFloat xAxisWidth = CGRectGetWidth(rect) - (_chartMarginLeft + _chartMarginRight) / 2; CGFloat yAxisHeight = _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軸的小分割線 CGPoint point; for (NSUInteger i = 0; i |
到這裡,一張完整的圖表就可以畫出來了。但是當前繪製的圖表的折線都是直線,在上面還展示了一張曲線圖。那麼如果想繪製帶有曲線的折線圖應該怎麼做呢?對,就是在貝塞爾曲線上下功夫。
當我們獲取了所有線段的端點陣列後,我們可以通過他們繪製彎曲的貝塞爾曲線(注意:該方法是對應上面對第6項的下半部分:生成每一個線段對貝塞爾曲線):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
//_showSmoothLines是用來控制是否繪製曲線折線的開關屬性 if (self.showSmoothLines && chartData.itemCount >= 4) { for (NSDictionary *item in progressLinePaths) { ... for (NSDictionary *range in calculatedRanges) { UIBezierPath *currentProgressLine = [UIBezierPath bezierPath]; CGPoint segmentP1 = [range[@"from"] CGPointValue]; CGPoint segmentP2 = [range[@"to"] CGPointValue]; [currentProgressLine moveToPoint:segmentP1]; CGPoint midPoint = [PNLineChart midPointBetweenPoint1:segmentP1 andPoint2:segmentP2]; //以每條線段以中間點為分割點,分成兩組。每一組形成柔和的外凸曲線,而不是內凹 [currentProgressLine addQuadCurveToPoint:midPoint controlPoint:[PNLineChart controlPointBetweenPoint1:midPoint andPoint2:segmentP1]]; [currentProgressLine addQuadCurveToPoint:segmentP2 controlPoint:[PNLineChart controlPointBetweenPoint1:midPoint andPoint2:segmentP2]]; [progressLines addObject:currentProgressLine]; [progressLineColors addObject:range[@"color"]]; } } } |
注意一下生成彎曲的貝塞爾曲線的方法:controlPointBetweenPoint1:andPoint2
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//返回的點的x:是兩點的中間;返回的點的y:與第二個點保持一致 + (CGPoint)controlPointBetweenPoint1:(CGPoint)point1 andPoint2:(CGPoint)point2 { //線段兩端的中間點 CGPoint controlPoint = [self midPointBetweenPoint1:point1 andPoint2:point2]; //末端點 和 中間點y的差 CGFloat diffY = abs((int) (point2.y - controlPoint.y)); if (point1.y point2.y) //如果後端點更高 controlPoint.y -= diffY; return controlPoint; } |
OK,這樣一來,直線的曲線圖還有曲線的曲線圖就大概掌握了。不過還差一個東西,就是圖表對點選的響應。
我們需要思考一下:既然一張圖表裡可以顯示多條折線,所以,當手指點選圖表上的點以後,應該同時返回兩個資料:
- 點選了哪條折線上的這個點。
- 點選了這條折線上的哪個點。
該框架的作者很好地完成了這兩個任務,我們來看一下他是如何實現的:
響應點選的代理方法
點選了哪條折線的判斷
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)touchPoint:(NSSet *)touches withEvent:(UIEvent *)event { // Get the point user touched UITouch *touch = [touches anyObject]; CGPoint touchPoint = [touch locationInView:self]; for (NSUInteger p = 0; p *paths in _chartPath) { for (UIBezierPath *path in paths) { //如果當前點處於UIBezierPath曲線上 BOOL pointContainsPath = CGPathContainsPoint(path.CGPath, NULL, p1, NO); if (pointContainsPath) { //點選了某一條折線 [_delegate userClickedOnLinePoint:touchPoint lineIndex:lineIndex]; return; } } lineIndex++; } } } } } |
點選了哪個點的判斷
1 2 3 4 5 6 |
- (void)touchKeyPoint:(NSSet *)touches withEvent:(UIEvent *)event { // Get the point user touched UITouch *touch = [touches anyObject]; CGPoint touchPoint = [touch locationInView:self]; for (NSUInteger p = 0; p |
這下就完整了,一個帶有響應功能的圖表就做好啦!
關於自定義UIView
這裡只是將圖表的layer
加在了UIView
的layer上,那如果想完全自定義view的話,只需將圖表的layer
完全賦給UIView
的layer即可,這樣一來,想要畫出任意形狀的UIView
都可以。
三. 最後的話
關於圖表的繪製,相對貝塞爾曲線與CALayer
來說,資料的處理是一個比較麻煩的點。但是一旦學會了折線圖的繪製,瞭解了繪圖原理,那麼其他型別的圖表就可以觸類旁通。