Core Graphics 是非常棒的iOSApI,我們可以用它來自定義一些很酷的UI,而不必依賴圖片。
但是對于大部分開發(fā)者而言,它是令人畏懼的。因為它的PAI很多,有很多的東西需要去理解。
這篇文章會通過畫一個tableview,來為我們一步一步的揭開Core Graphics的神秘面紗。
看起來像這樣

在這一篇教程中我們會初步的使用Core Graphics。實現(xiàn)像,繪制一個矩形,繪制一個漸變,還有如何處理1像素的線的問題。
在下一篇教程中我們將完成這個app的剩余部分,tableview的header,footer還有觸摸事件。
Getting Started
新建一個項目選擇Single View Application,輸入CoolTable作為項目名稱,勾選Use Storyboards、Use Automatic Reference Counting,創(chuàng)建項目。然后刪除ViewController.h 和 ViewController.m用UITableViewController來替代。


創(chuàng)建一個新類繼承UITableViewController命名為CoolTableViewController

選中默認的the starting viewcontroller,并刪除它。從object library里面拉一個導(dǎo)航欄出來,

把UITableViewController的class改為你自定義的class,

刪除導(dǎo)航欄bar的title,

最后為cell準備一個reuse identify,使用cell,

運行app,

這是一個空白的tableview,讓我們添加一些數(shù)據(jù)。選中CoolTableViewController.m 文件,添加如下code
@interface CoolTableViewController ()
@property (copy) NSMutableArray *thingsToLearn;
@property (copy) NSMutableArray *thingsLearned;
@end
這兩個數(shù)組里面的數(shù)據(jù)源是填充tableview的兩個section的,注意這兩個數(shù)組是在私有的interface 里面聲明的因為它不需要讓外界知道。
繼續(xù)加入下列code
- (void)viewDidLoad{
[super viewDidLoad];
self.title = @"Core Graphics 101";
self.thingsToLearn = [@[@"Drawing Rects", @"Drawing Gradients", @"Drawing Arcs"] mutableCopy];
self.thingsLearned = [@[@"Table Views", @"UIKit", @"Objective-C"] mutableCopy];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
// Return the number of sections.
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
if (section == 0) {
return self.thingsToLearn.count;
}
else
{
return self.thingsLearned.count;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString * CellIdentifier = @"Cell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; NSString entry;
if (indexPath.section == 0) {
entry = self.thingsToLearn[indexPath.row];
}
else
{
entry = self.thingsLearned[indexPath.row];
}
cell.textLabel.text = entry;
return cell;
}
-(NSString *) tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (section == 0) {
return @"Things We'll Learn";
}
else
{
return @"Things Already Covered";
}
}
現(xiàn)在繼續(xù)運行

當你滑動的時候會發(fā)現(xiàn)第一個section header會黏在頂部

因為你使用的是tableview的plain模式,你可以用grouped模式來避免這種情況的發(fā)生。

Table View Style Analyzed
我們會通過三個部分來繪制tableview:cell,header,footer。

在這篇文章里我們先繪制cell,讓我們仔細觀察一下

我們發(fā)現(xiàn)了以下幾點:
- cell是漸變的從white到light gray
- 每個cell都有白色的輪廓,除了最后一個只有一邊有
- 每個cell通過light gray顏色的線分割,除了最后一個
- cell的邊緣有鋸齒狀
Hellow Core Graphics!
我們的code要寫在UIView的drawRect方法里。創(chuàng)建一個view命名為CustomCellBackground,然后切換到CustomCellBackground.m添加code
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor *redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
CGContextSetFillColorWithColor(context, redColor.CGColor);
CGContextFillRect(context, self.bounds);
}
在第一行我們調(diào)用UIGraphicsGetCurrentContext()方法獲得一個Core Graphics Context
在下面的方法中會用到它。
我們可以把context看做是一個畫布‘canvas’,我們可以在上面繪制。在這種情況下‘canvas’是view,還有其他的畫布,例如offscreen buffer,它可以變成一個圖片,在將來的某個時候。
關(guān)于context的第一個有趣的東西是stateful,當處于stateful意味著我們可以改變一些東西,像填充顏色。這個填充的顏色將會被保留下來用作填充顏色,除非你在后面把它改為不同的值。
在第三行使用了CGContextSetFillColorWithColor這個方法,來把填充色設(shè)置為red。你可以在任何時候使用這個方法來填充圖形。
你可能會注意到,你不能直接調(diào)用UIColor,你必須用CGColorRef這個類來替代,幸運的是它們之間的轉(zhuǎn)化非常簡單。
最后你調(diào)用一個方法來填充矩形,你需要傳入矩形的bounds。
現(xiàn)在你已經(jīng)有了一個red view,你將會把它設(shè)為cell的background view。
在CoolTableViewController.m的上面導(dǎo)入
#import "CustomCellBackground.h",然后修改tableView:cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString * CellIdentifier = @"Cell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
NSString * entry;
// START NEW
if (![cell.backgroundView isKindOfClass:[CustomCellBackground class]]) {
cell.backgroundView = [[CustomCellBackground alloc] init];
}
if (![cell.selectedBackgroundView isKindOfClass:[CustomCellBackground class]]) {
cell.selectedBackgroundView = [[CustomCellBackground alloc] init];
}
// END NEW
if (indexPath.section == 0) {
entry = self.thingsToLearn[indexPath.row];
} else {
entry = self.thingsLearned[indexPath.row];
}
cell.textLabel.text = entry;
cell.textLabel.backgroundColor = [UIColor clearColor]; // NEW
return cell;
}
run

Drawing Gradients
現(xiàn)在我們將會在項目中繪制許多漸變,把你的漸變code放在helper類里面方便以后在不同的項目中使用。
新建一個NSObject的子類,命名為Common刪除Common.h里面的所有內(nèi)容
添加如下code
#import <Foundation/Foundation.h>
void drawLinerGradient(CGContextRef context,CGRect rect, CGColorRef startColor, CGColorRef endColor);
你不是正真的創(chuàng)建了一個類,因為你不需要任何狀態(tài),只需要一個全局的方法。切換到Common.m添加code
#import "Common.h"
void drawLinearGradient(CGContextRef context, CGRect rect, CGColorRef startColor, CGColorRef endColor)
{
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat locations[] = {0.0,1.0};
NSArray *colors = @[(__bridge id)startColor,(__bridge id)endColor];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
// More coming...
}
這里有很多的方法。
第一件事,你需要有一個color space來繪制漸變color,通過color space你可以做很多事情。大部分時候你只需要用到RGB類型的color space,所以你只需要使用CGColorSpaceCreateDeviceRGB方法來獲得你需要的引用(RGB)。
設(shè)置一個數(shù)組,在漸變范圍你每種顏色的位置。0意味著開始漸變,1意味著漸變結(jié)束。你只需要兩個顏色,一個用來開始漸變,一個用來結(jié)束漸變,所以你只要傳入0和1。
注意,如果你想要的話你可以設(shè)置更多的漸變顏色,你要設(shè)置每種顏色開始漸變的位置。用這個方法可以實現(xiàn)很炫的效果哦。
想了解更多關(guān)于bridge和memory management,請看這篇教程Automatic Reference Counting.
這樣你就用CGGradientCreateWithColors創(chuàng)建了一個漸變,傳入了color space、color array、locations(顏色的位置)。
現(xiàn)在你有了一個漸變引用,但是不是一個真正的漸變圖像?,F(xiàn)在把下面code添加到More coming的注釋下面
CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGContextSaveGState(context);
CGContextAddRect(context, rect);
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGContextRestoreGState(context);
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
第一件事是計算開始和結(jié)束的點,剩下的code是幫你在rectage里面繪制漸變。主要的方法是CGContextDrawLinearGradient。這個方法很奇怪,因為它用漸變填補了畫布的整個區(qū)域,也就是說,它沒辦法填補某個區(qū)域。Clip是Core Graphics是一個很棒的特點,你可以用它來繪制任意的圖形,你要做的僅僅是把圖形添加到context里面。和一起不同的是,你只需要調(diào)用CGContextClip,這樣所有的繪制內(nèi)容就會限制在該區(qū)域。
所以這里你添加了一個矩形到context里面,裁剪它,然后調(diào)用CGContextDrawLinearGradient傳入你之前準備好的所有變量。
CGContextSaveCGState/CGContextRestoreCGState這個方法做了什么呢?記住Core Graphics有一種狀態(tài)機制。只要你設(shè)置了它的狀態(tài),它就會一直保持,直到你去改變它。這里就用到了這兩個方法,保存你當前context的設(shè)置到stack中。將來你想要恢復(fù)state的時候,就從stack中pop出來。
最后一件事,你需要釋放memory,通過調(diào)用CGGradientRelease方法來釋放CGGradientCreateWithColors方法創(chuàng)建的對象。
回到CustomCellBackground.m,導(dǎo)入#import "Common.h",替代drawRect方法里的code
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor * whiteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
UIColor * lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0];
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor.CGColor, lightGrayColor.CGColor);

Stroking Paths
我們要在cell四周繪制一個白色的矩形,并在cell之間繪制灰色的分割線。我們已經(jīng)填充了一個矩形,劃線也是很簡單的。修改CustomCellBackground.m的drawRect:方法
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor * whiteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
UIColor * lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0];
UIColor *redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor.CGColor, lightGrayColor.CGColor);
CGRect storeRect = CGRectInset(paperRect, 5.0, 5.0);
CGContextSetStrokeColorWithColor(context, redColor.CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, storeRect);
為了讓這些改變?nèi)菀卓闯鰜?,我們在cell的中間畫了一個紅色的矩形。CGRectInset這個方法是返回一個矩形,該矩形的rect是原參數(shù)矩形的基礎(chǔ)上,上下都減少了Y,左右都減了X。然后返回一個新的矩形給你。設(shè)置線寬為1point(在retain屏幕上是2pixels,非retain屏是1pixel),顏色為紅色。調(diào)用CGContextStrokeRect方法來繪制矩形。

它看起來不錯,但是仔細看會覺得有點模糊和怪異,如果放大了就能看清楚哪里不對勁。

你希望畫1point的線,但是你可以看到像素重合了,那怎么辦呢?
1 Point Lines and Pixel Boundaries
這件事證明了,用Core Graphics描一個路徑,描邊是以路徑為中間線。
我們希望填充矩形的路徑邊緣,當我們沿著邊緣畫1pixel,一半的線(0.5pixel)在矩形里面,一半的線在矩形的外面。
因為沒有辦法畫0.5pixel的線,所以Core Graphics用鋸齒來替代。
但是我們不想要鋸齒,我們需要的是1pixel的線,有下面幾種辦法來解決:
- 裁剪掉不想要的像素
- 使鋸齒無效,修改矩形的邊緣,確保達到你想要的效果
- 修改繪制路徑,把0.5pixel的影響考慮進去
打開Common.h文件,添加下列方法 CGRect rectFor1PxStroke(CGRect rect);
Common.m里面
CGRect rectFor1PxStroke(CGRect rect)
{
return CGRectMake(rect.origin.x + 0.5, rect.origin.y + 0.5, rect.size.width - 1, rect.size.height - 1);
}
路徑(是描邊的中線)向上移了1pixel,向右移了1pixel
回到CustomCellBackground.m用
CGRect strokeRect = rectFor1PxStroke(CGRectInset(paperRect, 5.0, 5.0));
替代以前的code,run

現(xiàn)在我們加上正確的顏色和位置
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor * whiteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
UIColor * lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0];
// UIColor *redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor.CGColor, lightGrayColor.CGColor);
// CGRect storeRect = CGRectInset(paperRect, 5.0, 5.0);
// CGRect storeRect = rectFor1PxStroke(CGRectInset(paperRect, 5.0, 5.0));
CGRect stroRect = paperRect;
stroRect.size.height -= 1;
stroRect = rectFor1PxStroke(stroRect);
CGContextSetStrokeColorWithColor(context, whiteColor.CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, stroRect);
這里我們減少一個高度來做分割,并把描邊換成白色,這樣在cell之間就有一個細微的白色,run

Drawing Lines
因為你已經(jīng)在項目里面花了不少的線,我們要把它抽出來。添加到Common.h類里面
void draw1PxStroke(CGContextRef context, CGPoint startPoint, CGPoint endPoint, CGColorRef color);
Common.m里面
void draw1PxStroke(CGContextRef context, CGPoint startPoint, CGPoint endPoint,CGColorRef color)
{
CGContextSaveGState(context);
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextSetStrokeColorWithColor(context, color);
CGContextSetLineWidth(context, 1.0);
CGContextMoveToPoint(context, startPoint.x + 0.5, startPoint.y + 0.5);
CGContextAddLineToPoint(context, endPoint.x + 0.5, endPoint.y + 0.5);
CGContextStrokePath(context);
CGContextRestoreGState(context);
}
在方法的開始,我們使用了save/restore,這樣我們在畫線的時候就不會對畫布周圍造成影響。
我們的線以cap的模式結(jié)束。這樣可以在一定程度上達到抗鋸齒的效果。
把點移動到A,畫A到B的線。
改變CustomCellBackground.m里的code
CGContextRef context = UIGraphicsGetCurrentContext();
UIColor * whiteColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1.0];
UIColor * lightGrayColor = [UIColor colorWithRed:230.0/255.0 green:230.0/255.0 blue:230.0/255.0 alpha:1.0];
UIColor * separatorColor = [UIColor colorWithRed:208.0/255.0 green:208.0/255.0 blue:208.0/255.0 alpha:1.0];
// UIColor *redColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0];
CGRect paperRect = self.bounds;
drawLinearGradient(context, paperRect, whiteColor.CGColor, lightGrayColor.CGColor);
// CGRect storeRect = CGRectInset(paperRect, 5.0, 5.0);
// CGRect storeRect = rectFor1PxStroke(CGRectInset(paperRect, 5.0, 5.0));
CGRect stroRect = paperRect;
stroRect.size.height -= 1;
stroRect = rectFor1PxStroke(stroRect);
CGContextSetStrokeColorWithColor(context, whiteColor.CGColor);
CGContextSetLineWidth(context, 1.0);
CGContextStrokeRect(context, stroRect);
CGPoint startPoint = CGPointMake(paperRect.origin.x, paperRect.origin.y + paperRect.size.height - 1);
CGPoint endPoint = CGPointMake(paperRect.origin.x + paperRect.size.width - 1, paperRect.origin.y + paperRect.size.height - 1);
draw1PxStroke(context, startPoint, endPoint, separatorColor.CGColor);
Run
