[深入淺出iOS庫]之圖形庫CorePlot
羅朝輝 (http://www.cnblogs.com/kesalin/)
本文遵循“署名-非商業用途-保持一致”創作公用協議
一,前言
Core Plot和s7Graph都是可在iOS平台下使用的開源矢量圖形庫,s7Graph功能相對比較簡單一些,在此就不介紹了。Core Plot 功能強大很多,我們可以利用它很方便地畫出復雜的曲線圖、柱狀圖和餅圖等等。下面我先來介紹如何在項目中配置使用 Core Plot 庫,然后通過一個曲線圖示例來演示如何使用它,最后結合示例介紹 Core Plot 的框架結構。
本文源代碼:https://github.com/kesalin/iOSSnippet/tree/master/CorePlotDemo
效果圖:
二,在iOS工程中配置 Core Plot
1,下載 Core Plot 庫
Core Plot 代碼倉庫托管在 Google Code 上,可通過 https://code.google.com/p/core-plot/ 獲取最新的庫和源碼(目前為1.1),在這里我們只關心 iOS平台:
解壓 CorePlot_1.1.zip,其目錄結構如下:
如果通過靜態鏈接方式引用CorePlot庫,那么我們只需要關心Binaries/iOS目錄即可,但是作為一個喜歡鑽研的程序員,Documentation和Source下面想必你也不會視而不見;如果通過引入工程文件方式引用CorePlot庫,那么我們需要引用Source/framework/CorePlot-CocoaTouch.xcodeproj。注意:CorePlot同時支持OS X 和 iOS 平台,CorePlot.xcodeproj 是OS X平台工程。此外,Source/examples 下的大量示例代碼絕對是不可忽視的第一手參考資料。
順便吐槽一句,iOS平台的開源庫很多,也會強大,但一如 iOS 系統自身,向后兼容性太差,用過Cocos 2D等庫的童鞋應該深有體會。CorePlot也不例外,以前用0.4版本的時候,類名前綴市 CP,現在都修改為 CPT(CorePlot-Touch)了。
2,在工程中添加 Core Plot 庫
下面,我將介紹如何用靜態鏈接方式引用 CorePlot 庫
a),新建 CorePlotDemo SingleView 工程,拷貝 Binaries/iOS 目錄到項目目錄下,並重命名 iOS 為 CorePlotLib,如圖所示:
b) 在工程中通過添加已存在文件將 CorePlotHeaders 目錄下的文件全部添加,並鏈接 libCorePlotTouch.a 靜態庫。如圖所示:
c) 因為CorePlot使用到 QuartzCore 庫,所以我們還需要連接 QuartzCore.framework。此外為了讓 XCode 裝載導入頭文件,需要設置編譯環境,在 build setting 中查找 other linker flags,添加 '-all_load -ObjC' 標志。至此設置工作完成。
三,使用 Core Plot 描繪曲線圖
1,Core Plot 要求它進行描繪的所在 view 類型必須為 CPTGraphHostingView 類型,所以在這里,我將設置 ViewController.nib 中 View 的類型為 CPTGraphHostingView。如圖所示:
2,修改 KSViewController.h 為:
#import <UIKit/UIKit.h> #import "CorePlot-CocoaTouch.h" @interface KSViewController : UIViewController<CPTPlotDataSource, CPTAxisDelegate> @end
如果你用過 NSTableView,那你應該很熟悉 CPTPlotDataSource 這種形式協議的用法, CPTPlotDataSource 為 CorePlot 提供數據源。在這里我還實現了 CPTAxisDelegate 協議,當我們想要對軸刻度的標簽進行一定的定制時,需要實現該協議。在本例中,要用不同的顏色對 y 軸方向大於等於0和小於0的刻度標簽進行區分,因此需要實現該協議。
3,聲明私有成員和方法
請參考文件:https://github.com/kesalin/iOSSnippet/blob/master/CorePlotDemo/CorePlotDemo/KSViewController.m
@interface KSViewController () { CPTXYGraph * _graph; NSMutableArray * _dataForPlot; } - (void)setupCoreplotViews; -(CPTPlotRange *)CPTPlotRangeFromFloat:(float)location length:(float)length; @end
在本例中,要描繪基於 xy 軸的圖形,因此,聲明了 CPTXYGraph 對象 _graph,然后聲明一個可變數字 _dataForPlot 為 Core Plot 提供數據。私有方法 setupCoreplotView 是本例的重點,所有的描繪設置都在這個函數中進行。CPTPlotRangeFromFloat:length:是一個輔助類以簡化 CPTPlotRange 的創建,其實現如下:
-(CPTPlotRange *)CPTPlotRangeFromFloat:(float)location length:(float)length { return [CPTPlotRange plotRangeWithLocation:CPTDecimalFromFloat(location) length:CPTDecimalFromFloat(length)]; }
為了支持旋轉操作,添加如下代碼(注意,本文只考慮了 iOS6 的旋轉,這又是 iOS 兼容性不太好的一大明證啊!):
#pragma mark - #pragma mark Rotation -(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { return YES; } -(BOOL)shouldAutorotate { return YES; }
4,實現 setupCoreplotViews
先來看效果說明圖,此圖非常重要,CorePlot常見的概念都在圖中有說明,后文會多次引用到該圖。
下面,來介紹重點內容,如何使用 CorePlot 對數據進行描繪。
- (void)setupCoreplotViews { CPTMutableLineStyle *lineStyle = [CPTMutableLineStyle lineStyle]; // Create graph from theme: 設置主題 // _graph = [[CPTXYGraph alloc] initWithFrame:CGRectZero]; CPTTheme * theme = [CPTTheme themeNamed:kCPTSlateTheme]; [_graph applyTheme:theme]; CPTGraphHostingView * hostingView = (CPTGraphHostingView *)self.view; hostingView.collapsesLayers = NO; // Setting to YES reduces GPU memory usage, but can slow drawing/scrolling hostingView.hostedGraph = _graph; _graph.paddingLeft = _graph.paddingRight = 10.0; _graph.paddingTop = _graph.paddingBottom = 10.0;
a)首先,創建了一個可編輯的線條風格 lineStyle,用來描述描繪線條的寬度,顏色和樣式等,這個 lineStyle 會被多次用到。
然后,創建基於 xy 軸的圖:CPTXYGraph,並設置其主題 CPTTheme,CorePlot 中的主題和日常軟件中的換膚概念差不多。目前支持五種主題:kCPTDarkGradientTheme, kCPTPlainBlackTheme, kCPTPlainWhiteTheme, kCPTSlateTheme,kCPTStocksTheme, 最后一種股票主題效果見上面的效果圖,而石板色主題 kCPTSlateTheme 效果見下面的效果圖。你可以修改此處的代碼嘗試不同的主題效果,^_^。將 hostingView的 hostedGraph與 _graph 關聯起來,也就是說:我們要在 View (CPTGraphHostingView)上畫一個基於xy軸的圖(CPTXYGraph)。至此,我們接觸到CorePlot中的兩個概念:宿主view 和圖graph。然后我們設置_graph的 padding,這樣在圖的四周與屏幕邊緣之間留有一絲空隙。
// Setup plot space: 設置一屏內可顯示的x,y量度范圍 // CPTXYPlotSpace * plotSpace = (CPTXYPlotSpace *)_graph.defaultPlotSpace; plotSpace.allowsUserInteraction = YES; plotSpace.xRange = [CPTPlotRange plotRangeWithLocation:CPTDecimalFromFloat(1.0) length:CPTDecimalFromFloat(2.0)]; plotSpace.yRange = [CPTPlotRange plotRangeWithLocation:CPTDecimalFromFloat(1.0) length:CPTDecimalFromFloat(3.0)];
上面的代碼設置PlotSpace,這是什么意思呢?x,y二維空間可以無限延伸,但在屏幕上我們可以看到的只是一小部分空間,這部分可視空間就由 Plot Space設置。CPTXYPlotSpace 的 xRange 和 yRange 就設置了一屏內可顯示的x,y方向的量度范圍。在這里,我們設置x,y軸上的起點都是1.0,然后長度分別為2個和3個單位。請結合上面的說明圖理解 PlotSpace 的含義。(注意:說明圖中的起點不是1.0,這是因為設置了 allowsUserInteraction 為 YES,我對PlotSpace進行了拖動所導致的)。
// Axes: 設置x,y軸屬性,如原點,量度間隔,標簽,刻度,顏色等 // CPTXYAxisSet *axisSet = (CPTXYAxisSet *)_graph.axisSet; lineStyle.miterLimit = 1.0f; lineStyle.lineWidth = 2.0; lineStyle.lineColor = [CPTColor whiteColor]; CPTXYAxis * x = axisSet.xAxis; x.orthogonalCoordinateDecimal = CPTDecimalFromString(@"2"); // 原點的 x 位置 x.majorIntervalLength = CPTDecimalFromString(@"0.5"); // x軸主刻度:顯示數字標簽的量度間隔 x.minorTicksPerInterval = 2; // x軸細分刻度:每一個主刻度范圍內顯示細分刻度的個數 x.minorTickLineStyle = lineStyle; // 需要排除的不顯示數字的主刻度 NSArray * exclusionRanges = [NSArray arrayWithObjects: [self CPTPlotRangeFromFloat:0.99 length:0.02], [self CPTPlotRangeFromFloat:2.99 length:0.02], nil]; x.labelExclusionRanges = exclusionRanges;
b), 有了 xy 軸圖對象,我們可以來對 xy 軸的顯示屬性進行設置了。通過獲取 XYGraph 的 axisSet 來獲取軸的集合,集合中就包含了 x,y 軸對象 CPTXYAxis。在這里,設置 x 軸的原點為 2,主刻度的量度間隔為 0.5,每一個主刻度內顯示細分刻度的個數為 2 個,並用白色寬度為2的線條來描繪 x 軸。如果有一些刻度的標簽我們不想讓它顯示那該如何呢?很簡單,設置軸的排除標簽范圍 labelExclusionRanges 即可。
同樣,我們設置 y 軸的顯示屬性:
CPTXYAxis * y = axisSet.yAxis; y.orthogonalCoordinateDecimal = CPTDecimalFromString(@"2"); // 原點的 y 位置 y.majorIntervalLength = CPTDecimalFromString(@"0.5"); // y軸主刻度:顯示數字標簽的量度間隔 y.minorTicksPerInterval = 4; // y軸細分刻度:每一個主刻度范圍內顯示細分刻度的個數 y.minorTickLineStyle = lineStyle; exclusionRanges = [NSArray arrayWithObjects: [self CPTPlotRangeFromFloat:1.99 length:0.02], [self CPTPlotRangeFromFloat:2.99 length:0.02], nil]; y.labelExclusionRanges = exclusionRanges; y.delegate = self;
請參考說明圖理解上面軸設置的含義。注意,在這里,我設置了 y 軸的 delegate 為自身,這個 delegate 需要實現CPTAxisDelegate 協議,在這里我想要用不同的顏色對 y 軸方向大於等於0和小於0的刻度標簽進行區分,因此需要實現該協議方法:axis:shouldUpdateAxisLabelsAtLocations:。
#pragma mark - #pragma mark Axis Delegate Methods -(BOOL)axis:(CPTAxis *)axis shouldUpdateAxisLabelsAtLocations:(NSSet *)locations { static CPTTextStyle * positiveStyle = nil; static CPTTextStyle * negativeStyle = nil; NSNumberFormatter * formatter = axis.labelFormatter; CGFloat labelOffset = axis.labelOffset; NSDecimalNumber * zero = [NSDecimalNumber zero]; NSMutableSet * newLabels = [NSMutableSet set]; for (NSDecimalNumber * tickLocation in locations) { CPTTextStyle *theLabelTextStyle; if ([tickLocation isGreaterThanOrEqualTo:zero]) { if (!positiveStyle) { CPTMutableTextStyle * newStyle = [axis.labelTextStyle mutableCopy]; newStyle.color = [CPTColor greenColor]; positiveStyle = newStyle; } theLabelTextStyle = positiveStyle; } else { if (!negativeStyle) { CPTMutableTextStyle * newStyle = [axis.labelTextStyle mutableCopy]; newStyle.color = [CPTColor redColor]; negativeStyle = newStyle; } theLabelTextStyle = negativeStyle; } NSString * labelString = [formatter stringForObjectValue:tickLocation]; CPTTextLayer * newLabelLayer= [[CPTTextLayer alloc] initWithText:labelString style:theLabelTextStyle]; CPTAxisLabel * newLabel = [[CPTAxisLabel alloc] initWithContentLayer:newLabelLayer]; newLabel.tickLocation = tickLocation.decimalValue; newLabel.offset = labelOffset; [newLabels addObject:newLabel]; } axis.axisLabels = newLabels; return NO; }
在上面的代碼中,對於 y 軸上大於等於0的刻度標簽用綠色描繪,而小於0的刻度標簽用紅色描繪。因為在這里我們自己設置了軸標簽的描繪,所以這個方法返回 NO 告訴系統不需要使用系統的標簽描繪設置了。其效果如下:
至此,xy軸部分的描繪設置完成。
c) 下面我們向圖中添加曲線的描繪:
// Create a red-blue plot area // lineStyle.miterLimit = 1.0f; lineStyle.lineWidth = 3.0f; lineStyle.lineColor = [CPTColor blueColor]; CPTScatterPlot * boundLinePlot = [[CPTScatterPlot alloc] init]; boundLinePlot.dataLineStyle = lineStyle; boundLinePlot.identifier = BLUE_PLOT_IDENTIFIER; boundLinePlot.dataSource = self; // Do a red-blue gradient: 漸變色區域 // CPTColor * blueColor = [CPTColor colorWithComponentRed:0.3 green:0.3 blue:1.0 alpha:0.8]; CPTColor * redColor = [CPTColor colorWithComponentRed:1.0 green:0.3 blue:0.3 alpha:0.8]; CPTGradient * areaGradient1 = [CPTGradient gradientWithBeginningColor:blueColor endingColor:redColor]; areaGradient1.angle = -90.0f; CPTFill * areaGradientFill = [CPTFill fillWithGradient:areaGradient1]; boundLinePlot.areaFill = areaGradientFill; boundLinePlot.areaBaseValue = [[NSDecimalNumber numberWithFloat:1.0] decimalValue]; // 漸變色的起點位置 // Add plot symbols: 表示數值的符號的形狀 // CPTMutableLineStyle * symbolLineStyle = [CPTMutableLineStyle lineStyle]; symbolLineStyle.lineColor = [CPTColor blackColor]; symbolLineStyle.lineWidth = 2.0; CPTPlotSymbol * plotSymbol = [CPTPlotSymbol ellipsePlotSymbol]; plotSymbol.fill = [CPTFill fillWithColor:[CPTColor blueColor]]; plotSymbol.lineStyle = symbolLineStyle; plotSymbol.size = CGSizeMake(10.0, 10.0); boundLinePlot.plotSymbol = plotSymbol; [_graph addPlot:boundLinePlot];
首先,添加一個由紅到藍漸變的曲線圖 CPTScatterPlot,設置該曲線圖的曲線線條顏色為藍色,寬度為3,標識為 @"Blue Plot",數據源 datasource 為自身。注意:一個圖中可以有多個曲線圖,每個曲線圖通過其 identifier 進行唯一標識。 數據源將在后面介紹。如果我們不僅僅是描繪曲線,還想描繪曲線覆蓋的區域,那么就要設置曲線圖的區域填充顏色 areaFill,並設置 areaBaseValue。areaBaseValue就是設置該填充顏色從哪個值開始描述,比如本例是從1.0開始描繪(見上圖中紅色部分開始的位置為 y=1)。在這里我們設置的填充顏色為從紅色變到藍色的漸變色 CPTGradient,CPTGradient默認的變化開始色從x軸左邊變化到右邊的結束色,如下圖所示:
在本例中,將漸變色旋轉-90度(即順時針方向旋轉90度),使得紅色在下面,藍色在上面(見說明圖)。 對於曲線上的數值點用什么樣的符號來表示呢?這就是CPTPlotSymbol 發揮作用的時候了,在這里是用藍色的實心圓點來表示具體的數值。
d), 有了藍紅曲線圖的介紹,下面再來添加一個破折線風格的綠色曲線圖:
// Create a green plot area: 畫破折線 // lineStyle = [CPTMutableLineStyle lineStyle]; lineStyle.lineWidth = 3.f; lineStyle.lineColor = [CPTColor greenColor]; lineStyle.dashPattern = [NSArray arrayWithObjects: [NSNumber numberWithFloat:5.0f], [NSNumber numberWithFloat:5.0f], nil]; CPTScatterPlot * dataSourceLinePlot = [[CPTScatterPlot alloc] init]; dataSourceLinePlot.dataLineStyle = lineStyle; dataSourceLinePlot.identifier = GREEN_PLOT_IDENTIFIER; dataSourceLinePlot.dataSource = self; // Put an area gradient under the plot above // CPTColor * areaColor = [CPTColor colorWithComponentRed:0.3 green:1.0 blue:0.3 alpha:0.8]; CPTGradient * areaGradient = [CPTGradient gradientWithBeginningColor:areaColor endingColor:[CPTColor clearColor]]; areaGradient.angle = -90.0f; areaGradientFill = [CPTFill fillWithGradient:areaGradient]; dataSourceLinePlot.areaFill = areaGradientFill; dataSourceLinePlot.areaBaseValue= CPTDecimalFromString(@"1.75"); // Animate in the new plot: 淡入動畫 dataSourceLinePlot.opacity = 0.0f; CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; fadeInAnimation.duration = 3.0f; fadeInAnimation.removedOnCompletion = NO; fadeInAnimation.fillMode = kCAFillModeForwards; fadeInAnimation.toValue = [NSNumber numberWithFloat:1.0]; [dataSourceLinePlot addAnimation:fadeInAnimation forKey:@"animateOpacity"]; [_graph addPlot:dataSourceLinePlot];
上面的代碼與前面的紅藍漸變曲線圖結構大體相同,只不過在這里使用的市破折線風格的線條,並且沒有使用特殊符號對數值點進行描繪。在這里,我們添加了一個有意思的淡入動畫。
至此,描繪相關設置就完成了。先來回顧一下整個步驟:
a) 在 CPTGraphHostingView 上放置一個 xy 軸圖 CPTXYGraph;
b) 然后對 xy 軸圖進行設置,設置其主題,可視空間 CPTPlotSpace,以及兩個軸 CPTXYAxis;
c) 然后在 xy 軸圖上添加紅藍漸變的曲線圖CPTScatterPlot;
d) 然后在 xy 軸圖上添加綠色破折線曲線圖CPTScatterPlot;
e) 最后,我們來初始化一些演示數據,從而結束 setupCoreplotViews 方法的介紹。
// Add some initial data // _dataForPlot = [NSMutableArray arrayWithCapacity:100]; NSUInteger i; for ( i = 0; i < 100; i++ ) { id x = [NSNumber numberWithFloat:0 + i * 0.05]; id y = [NSNumber numberWithFloat:1.2 * rand() / (float)RAND_MAX + 1.2]; [_dataForPlot addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys:x, @"x", y, @"y", nil]]; }
5,實現數據源協議
#pragma mark - #pragma mark Plot Data Source Methods -(NSUInteger)numberOfRecordsForPlot:(CPTPlot *)plot { return [_dataForPlot count]; } -(NSNumber *)numberForPlot:(CPTPlot *)plot field:(NSUInteger)fieldEnum recordIndex:(NSUInteger)index { NSString * key = (fieldEnum == CPTScatterPlotFieldX ? @"x" : @"y"); NSNumber * num = [[_dataForPlot objectAtIndex:index] valueForKey:key]; // Green plot gets shifted above the blue if ([(NSString *)plot.identifier isEqualToString:GREEN_PLOT_IDENTIFIER]) { if (fieldEnum == CPTScatterPlotFieldY) { num = [NSNumber numberWithDouble:[num doubleValue] + 1.0]; } } return num; }
和 NSTableView 相似,我們需要提供數據的個數,以及對應x/y軸的數據。至此,編譯允許,你就能看到如期的效果:綠色破折線曲線圖淡入,然后整個xy軸圖就呈現在你面前,並且該圖是允許你拖拽的,不妨多拖拽下,以更好地理解 CorePlot 中各種概念屬性的含義。
6,動態修改 CPTPlotSpace 的范圍
為了讓例子更有趣一點,在 SetupCoreplotViews 的末尾添加如下代碼:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(changePlotRange) userInfo:nil repeats:YES];
並實現 changePlotRange 方法:
-(void)changePlotRange { // Change plot space CPTXYPlotSpace * plotSpace = (CPTXYPlotSpace *)_graph.defaultPlotSpace; plotSpace.xRange = [self CPTPlotRangeFromFloat:0.0 length:(3.0 + 2.0 * rand() / RAND_MAX)]; plotSpace.yRange = [self CPTPlotRangeFromFloat:0.0 length:(3.0 + 2.0 * rand() / RAND_MAX)]; }
四,Core Plot 框架結構分析
CorePlot 的類結構關系如下:
其中最核心的就是 CPTGraph,本示例中的 CPTXYGraph是它的子類;一個圖 CPTGraph包含一個軸集 CPTAxiset,每個軸集可包含多個軸;一個圖 CPTGraph 可包含多個圖空間 CPTPlotSpace;一個圖 CPTGraph 可包含多個圖形CPTSplot(曲線,餅圖,柱狀圖等);圖形CPTSplot 和軸都展現在某個圖空間 CPTPlotSpace 中。其余的部分,尚未介紹到,暫且不提。
也許下圖能更形象地描述出 Core Plot 各種對象之間的關系。
五,結語
Core Plot 的類結構非常清晰,使用起來也很簡單,可定制化程度非常高。保守估計國內做矢量圖表展示的至少有 90% 以上在用它,曾經做過的一個基金應用就用到它。BTW,吐槽下iOS開發界的浮躁,很多招聘要求都具體到用會用這個庫那個庫,如加密/解密,解析json/xml,或數據庫 sqlite,或圖形庫Core Plot 或 Three20 什么的。對於有過一定工作經驗的程序員來說,學習這些庫的使用也不就是一兩天的功夫么?當然,對於程序員本身來說,我們不能浮躁地只停留在使用層面上,應該多一點好奇心,鑽研下這些庫的實現,適用場景,同類庫比較等等是大有裨益的。