本文是《Programming iOS5》中Drawing一章的翻譯,考慮到主題完整性,翻譯版本中加入了一些書中未涉及到的內容。但願本文可以對你有所幫助。html
本文由海水的味道翻譯整理,轉載請註明譯者和出處,請勿用於商業用途!ios
Core Graphics Framework是一套基於C的API框架,使用了Quartz做爲繪圖引擎。它提供了低級別、輕量級、高保真度的2D渲染。該框架能夠用於基於路徑的繪圖、變換、顏色管理、脫屏渲染,模板、漸變、遮蔽、圖像數據管理、圖像的建立、遮罩以及PDF文檔的建立、顯示和分析。爲了從感官上對這些概念作一個入門的認識,你能夠運行一下官方的example code。編程
iOS支持兩套圖形API族:Core Graphics/QuartZ 2D 和OpenGL ES。OpenGL ES是跨平臺的圖形API,屬於OpenGL的一個簡化版本。QuartZ 2D是蘋果公司開發的一套API,它是Core Graphics Framework的一部分。須要注意的是:OpenGL ES是應用程序編程接口,該接口描述了方法、結構、函數應具備的行爲以及應該如何被使用的語義。也就是說它只定義了一套規範,具體的實現由設備製造商根據規範去作。而每每不少人對接口和實現存在誤解。舉一個不恰當的比喻:上發條的時鐘和裝電池的時鐘都有相同的可視行爲,但二者的內部實現大相徑庭。由於製造商能夠自由的實現Open GL ES,因此不一樣系統實現的OpenGL ES也存在着巨大的性能差別。緩存
Core Graphics API全部的操做都在一個上下文中進行。因此在繪圖以前須要獲取該上下文並傳入執行渲染的函數中。若是你正在渲染一副在內存中的圖片,此時就須要傳入圖片所屬的上下文。得到一個圖形上下文是咱們完成繪圖任務的第一步,你能夠將圖形上下文理解爲一塊畫布。若是你沒有獲得這塊畫布,那麼你就沒法完成任何繪圖操做。固然,有許多方式得到一個圖形上下文,這裏我介紹兩種最爲經常使用的獲取方法。安全
第一種方法就是建立一個圖片類型的上下文。調用UIGraphicsBeginImageContextWithOptions函數就可得到用來處理圖片的圖形上下文。利用該上下文,你就能夠在其上進行繪圖,並生成圖片。調用UIGraphicsGetImageFromCurrentImageContext函數可從當前上下文中獲取一個UIImage對象。記住在你全部的繪圖操做後別忘了調用UIGraphicsEndImageContext函數關閉圖形上下文。app
第二種方法是利用cocoa爲你生成的圖形上下文。當你子類化了一個UIView並實現了本身的drawRect:方法後,一旦drawRect:方法被調用,Cocoa就會爲你建立一個圖形上下文,此時你對圖形上下文的全部繪圖操做都會顯示在UIView上。框架
判斷一個上下文是否爲當前圖形上下文須要注意的幾點:函數
做爲初學者,很容易被UIKit和Core Graphics兩個支持繪圖的框架迷惑。post
UIKit性能
像UIImage、NSString(繪製文本)、UIBezierPath(繪製形狀)、UIColor都知道如何繪製本身。這些類提供了功能有限但使用方便的方法來讓咱們完成繪圖任務。通常狀況下,UIKit就是咱們所須要的。
使用UiKit,你只能在當前上下文中繪圖,因此若是你當前處於UIGraphicsBeginImageContextWithOptions函數或drawRect:方法中,你就能夠直接使用UIKit提供的方法進行繪圖。若是你持有一個context:參數,那麼使用UIKit提供的方法以前,必須將該上下文參數轉化爲當前上下文。幸運的是,調用UIGraphicsPushContext 函數能夠方便的將context:參數轉化爲當前上下文,記住最後別忘了調用UIGraphicsPopContext函數恢復上下文環境。
Core Graphics
這是一個繪圖專用的API族,它常常被稱爲QuartZ或QuartZ 2D。Core Graphics是iOS上全部繪圖功能的基石,包括UIKit。
使用Core Graphics以前須要指定一個用於繪圖的圖形上下文(CGContextRef),這個圖形上下文會在每一個繪圖函數中都會被用到。若是你持有一個圖形上下文context:參數,那麼你等同於有了一個圖形上下文,這個上下文也許就是你須要用來繪圖的那個。若是你當前處於UIGraphicsBeginImageContextWithOptions函數或drawRect:方法中,並無引用一個上下文。爲了使用Core Graphics,你能夠調用UIGraphicsGetCurrentContext函數得到當前的圖形上下文。
至此,咱們有了兩大繪圖框架的支持以及三種得到圖形上下文的方法(drawRect:、drawRect: inContext:、UIGraphicsBeginImageContextWithOptions)。那麼咱們就有6種繪圖的形式。若是你有些困惑了,不用怕,我接下來將說明這6種狀況。無需擔憂尚未具體的繪圖命令,你只需關注上下文如何被建立以及咱們是在使用UIKit仍是Core Graphics。
第一種繪圖形式:在UIView的子類方法drawRect:中繪製一個藍色圓,使用UIKit在Cocoa爲咱們提供的當前上下文中完成繪圖任務。
- (void) drawRect: (CGRect) rect {
UIBezierPath* p = [UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
}
第二種繪圖形式:使用Core Graphics實現繪製藍色圓。
- (void) drawRect: (CGRect) rect {
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
}
第三種繪圖形式:我將在UIView子類的drawLayer:inContext:方法中實現繪圖任務。drawLayer:inContext:方法是一個繪製圖層內容的代理方法。爲了可以調用drawLayer:inContext:方法,咱們須要設定圖層的代理對象。但要注意,不該該將UIView對象設置爲顯示層的委託對象,這是由於UIView對象已是隱式層的代理對象,再將它設置爲另外一個層的委託對象就會出問題。輕量級的作法是:編寫負責繪圖形的代理類。在MyView.h文件中聲明以下代碼:
@interface MyLayerDelegate : NSObject
@end
而後MyView.m文件中實現接口代碼:
@implementation MyLayerDelegate
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx {
UIGraphicsPushContext(ctx);
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
UIGraphicsPopContext();
}
@end
直接將代理類的實現代碼放在MyView.m文件的#import代碼的下面,這樣感受好像在使用私有類完成繪圖任務(雖然這不是私有類)。須要注意的是,咱們所引用的上下文並非當前上下文,因此爲了可以使用UIKit,咱們須要將引用的上下文轉變成當前上下文。
由於圖層的代理是assign內存管理策略,那麼這裏就不能以局部變量的形式建立MyLayerDelegate實例對象賦值給圖層代理。這裏選擇在MyView.m中增長一個實例變量,由於實例變量默認是strong:
@interface MyView () {
MyLayerDelegate* _layerDeleagete;
}
@end
使用該圖層代理:
MyView *myView = [[MyView alloc] initWithFrame: CGRectMake(0, 0, 320, 480)];
CALayer *myLayer = [CALayer layer];
_layerDelegate = [[MyLayerDelegate alloc] init];
myLayer.delegate = _layerDelegate;
[myView.layer addSublayer:myLayer];
[myView setNeedsDisplay]; // 調用此方法,drawLayer: inContext:方法纔會被調用。
第四種繪圖形式: 使用Core Graphics在drawLayer:inContext:方法中實現一樣操做,代碼以下:
- (void)drawLayer:(CALayer*)lay inContext:(CGContextRef)con {
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
}
最後,演示UIGraphicsBeginImageContextWithOptions的用法,並從上下文中生成一個UIImage對象。生成UIImage對象的代碼並不須要等待某些方法被調用後或在UIView的子類中才能去作。
第五種繪圖形式: 使用UIKit實現:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)];
[[UIColor blueColor] setFill];
[p fill];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
解釋一下UIGraphicsBeginImageContextWithOptions函數參數的含義:第一個參數表示所要建立的圖片的尺寸;第二個參數用來指定所生成圖片的背景是否爲不透明,如上咱們使用YES而不是NO,則咱們獲得的圖片背景將會是黑色,顯然這不是我想要的;第三個參數指定生成圖片的縮放因子,這個縮放因子與UIImage的scale屬性所指的含義是一致的。傳入0則表示讓圖片的縮放因子根據屏幕的分辨率而變化,因此咱們獲得的圖片無論是在單分辨率仍是視網膜屏上看起來都會很好。
第六種繪圖形式: 使用Core Graphics實現:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100), NO, 0);
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100));
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillPath(con);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIKit和Core Graphics能夠在相同的圖形上下文中混合使用。在iOS 4.0以前,使用UIKit和UIGraphicsGetCurrentContext被認爲是線程不安全的。而在iOS4.0之後蘋果讓繪圖操做在第二個線程中執行解決了此問題。
UIImage經常使用的繪圖操做
一個UIImage對象提供了向當前上下文繪製自身的方法。咱們如今已經知道如何獲取一個圖片類型的上下文並將它轉變成當前上下文。
平移操做:下面的代碼展現瞭如何將UIImage繪製在當前的上下文中。
UIImage* mars = [UIImage imageNamed:@"Mars.png"];
CGSize sz = [mars size];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*2, sz.height), NO, 0);
[mars drawAtPoint:CGPointMake(0,0)];
[mars drawAtPoint:CGPointMake(sz.width,0)];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView* iv = [[UIImageView alloc] initWithImage:im];
[self.window.rootViewController.view addSubview: iv];
iv.center = self.window.center;
圖1 UIImage平移處理
縮放操做:下面代碼展現瞭如何對UIImage進行縮放操做:
UIImage* mars = [UIImage imageNamed:@"Mars.png"];
CGSize sz = [mars size];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*2, sz.height*2), NO, 0);
[mars drawInRect:CGRectMake(0,0,sz.width*2,sz.height*2)];
[mars drawInRect:CGRectMake(sz.width/2.0, sz.height/2.0, sz.width, sz.height) blendMode:kCGBlendModeMultiply alpha:1.0];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
圖2 UIImage縮放處理
UIImage沒有提供截取圖片指定區域的功能。但經過建立一個較小的圖形上下文並移動圖片到一個適當的圖形上下文座標系內,指定區域內的圖片就會被獲取。
裁剪操做:下面代碼展現瞭如何獲取圖片的右半邊:
UIImage* mars = [UIImage imageNamed:@"Mars.png"];
CGSize sz = [mars size];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width/2.0, sz.height), NO, 0);
[mars drawAtPoint:CGPointMake(-sz.width/2.0, 0)];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
以上的代碼首先建立一個一半圖片寬度的圖形上下文,而後將圖片左上角原點移動到與圖形上下文負X座標對齊,從而讓圖片只有右半部分與圖形上下文相交。
圖3 UIImage裁剪原理
CGImage經常使用的繪圖操做
UIImage的Core Graphics版本是CGImage(具體類型是CGImageRef)。二者能夠直接相互轉化: 使用UIImage的CGImage屬性能夠訪問Quartz圖片數據;將CGImage做爲UIImage方法imageWithCGImage:或initWithCGImage:的參數建立UIImage對象。
一個CGImage對象可讓你獲取原始圖片中指定區域的圖片(也能夠獲取指定區域外的圖片,UIImage卻辦不到)。
下面的代碼展現了將圖片拆分紅兩半,並分別繪製在上下文的左右兩邊:
UIImage* mars = [UIImage imageNamed:@"Mars.png"];
// 抽取圖片的左右半邊
CGSize sz = [mars size];
CGImageRef marsLeft = CGImageCreateWithImageInRect([mars CGImage],CGRectMake(0,0,sz.width/2.0,sz.height));
CGImageRef marsRight = CGImageCreateWithImageInRect([mars CGImage],CGRectMake(sz.width/2.0,0,sz.width/2.0,sz.height));
// 將每個CGImage繪製到圖形上下文中
UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5, sz.height), NO, 0);
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextDrawImage(con, CGRectMake(0,0,sz.width/2.0,sz.height), marsLeft);
CGContextDrawImage(con, CGRectMake(sz.width,0,sz.width/2.0,sz.height), marsRight);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 記得釋放內存,ARC在這裏無效
CGImageRelease(marsLeft);
CGImageRelease(marsRight);
你也許發現繪出的圖是上下顛倒的!圖片的顛倒並非由於被旋轉了。當你建立了一個CGImage並使用CGContextDrawImage方法繪圖就會引發這種問題。這主要是由於原始的本地座標系統(座標原點在左上角)與目標上下文(座標原點在左下角)不匹配。有不少方法能夠修復這個問題,其中一種方法就是使用CGContextDrawImage方法先將CGImage繪製到UIImage上,而後獲取UIImage對應的CGImage,此時就獲得了一個倒轉的CGImage。當再調用CGContextDrawImage方法,咱們就將倒轉的圖片還原回來了。實現代碼以下:
CGImageRef flip (CGImageRef im) {
CGSize sz = CGSizeMake(CGImageGetWidth(im), CGImageGetHeight(im));
UIGraphicsBeginImageContextWithOptions(sz, NO, 0);
CGContextDrawImage(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, sz.width, sz.height), im);
CGImageRef result = [UIGraphicsGetImageFromCurrentImageContext() CGImage];
UIGraphicsEndImageContext();
return result;
}
如今將以前的代碼修改以下:
CGContextDrawImage(con, CGRectMake(0,0,sz.width/2.0,sz.height), flip(marsLeft));
CGContextDrawImage(con, CGRectMake(sz.width,0,sz.width/2.0,sz.height), flip(marsRight));
然而,這裏又出現了另一個問題:在雙分辨率的設備上,若是咱們的圖片文件是高分辨率(@2x)版本,上面的繪圖就是錯誤的。緣由在於對於UIImage來講,在加載原始圖片時使用imageNamed:方法,它會自動根據所在設備的分辨率類型選擇圖片,而且UIImage經過設置用來適配的scale屬性補償圖片的兩倍尺寸。可是一個CGImage對象並無scale屬性,它不知道圖片文件的尺寸是否爲兩倍!因此當調用UIImage的CGImage方法,你不能假定所得到的CGImage尺寸與原始UIImage是同樣的。在單分辨率和雙分辨率下,一個UIImage對象的size屬性值都是同樣的,可是雙分辨率UIImage對應的CGImage是單分辨率UIImage對應的CGImage的兩倍大。因此咱們須要修改上面的代碼,讓其在單雙分辨率下均可以工做。代碼以下:
UIImage* mars = [UIImage imageNamed:@"Mars.png"];
CGSize sz = [mars size];
// 轉換CGImage並使用對應的CGImage尺寸截取圖片的左右部分
CGImageRef marsCG = [mars CGImage];
CGSize szCG = CGSizeMake(CGImageGetWidth(marsCG), CGImageGetHeight(marsCG));
CGImageRef marsLeft = CGImageCreateWithImageInRect(marsCG,CGRectMake(0,0,szCG.width/2.0,szCG.height));
CGImageRef marsRight = CGImageCreateWithImageInRect(marsCG, CGRectMake(szCG.width/2.0,0,szCG.width/2.0,szCG.height));
UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5, sz.height), NO, 0);
//剩下的和以前的代碼同樣,修復倒置問題
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextDrawImage(con, CGRectMake(0,0,sz.width/2.0,sz.height),flip(marsLeft));
CGContextDrawImage(con, CGRectMake(sz.width,0,sz.width/2.0,sz.height),flip(marsRight));
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRelease(marsLeft);
CGImageRelease(marsRight);
上面的代碼初看上去很繁雜,不過不用擔憂,這裏還有另外一種修復倒置問題的方案。相對於使用flip函數,你能夠在繪圖以前將CGImage包裝進UIImage中,這樣作有兩大優勢:
因此這是一個解決倒置和縮放問題的自包含方法。
代碼以下:
UIImage* mars = [UIImage imageNamed:@"Mars.png"];
CGSize sz = [mars size];
CGImageRef marsCG = [mars CGImage];
CGSize szCG = CGSizeMake(CGImageGetWidth(marsCG), CGImageGetHeight(marsCG));
CGImageRef marsLeft = CGImageCreateWithImageInRect(marsCG, CGRectMake(0,0,szCG.width/2.0,szCG.height));
CGImageRef marsRight = CGImageCreateWithImageInRect(marsCG, CGRectMake(szCG.width/2.0,0,szCG.width/2.0,szCG.height));
UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5, sz.height), NO, 0);
[[UIImage imageWithCGImage:marsLeft scale:[mars scale] orientation:UIImageOrientationUp] drawAtPoint:CGPointMake(0,0)];
[[UIImage imageWithCGImage:marsRight scale:[mars scale] orientation:UIImageOrientationUp] drawAtPoint:CGPointMake(sz.width,0)];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGImageRelease(marsLeft); CGImageRelease(marsRight);
還有另外一種解決倒置問題的方案是在繪製CGImage以前,對上下文應用變換操做,有效地倒置上下文的內部座標系統。這裏先不作討論。
爲何會發生倒置問題
究其緣由是由於Core Graphics源於Mac OS X系統,在Mac OS X中,座標原點在左下方而且正y座標是朝上的,而在iOS中,原點座標是在左上方而且正y座標是朝下的。在大多數狀況下,這不會出現任何問題,由於圖形上下文的座標系統是會自動調節補償的。可是建立和繪製一個CGImage對象時就會暴露出倒置問題。
CIFilter與CIImage
CIFilter與CIImage是iOS 5新引入的,雖然它們已在MAX OS X系統中存在多年。前綴「CI」表示Core Image,這是一種使用數學濾鏡變換圖片的技術。可是你不要去幻想iOS提供了像Photoshop軟件那樣強大的濾鏡功能。使用Core Image以前你須要將CoreImage.framework框架導入到你的target之中。
所謂濾鏡指的是CIFilter類,濾鏡可被分爲如下幾類:
模板與漸變類
這兩類濾鏡建立的CIImage能夠和其餘的CIImage進行合併,好比一種單色,一個棋盤,條紋,亦或是漸變。
合成類
此類濾鏡能夠將一張圖片與另外的圖片合併,合成濾鏡模式常見於圖形處理軟件Photoshop中。
色彩類
此濾鏡調整、修改圖片的色彩。所以你能夠改變一張圖片的飽和度、色度、亮度、對比度、伽馬、白點、曝光度、陰影、高亮等屬性。
幾何變換類
此類濾鏡可對圖片執行基本的幾何變換,好比縮放、旋轉、裁剪。
CIFilter使用起來很是的簡單。CIFilter看上去就像一個由鍵值組成的字典。它生成一個CIImage對象做爲其輸出。通常地,一個濾鏡有一個或多個輸入,而對於部分濾鏡,生成的圖片是基於其餘類型的參數值。CIFilter對象是一個集合,可以使用鍵值對進行檢索。經過提供濾鏡的字符串名稱建立一個濾鏡,若是想知道有哪些濾鏡,能夠查詢蘋果的Core Image Filter Reference文檔,或是調用CIFilter的類方法filterNamesInCategories:,參數值爲nil。每個濾鏡擁有一小部分用來肯定其行爲的鍵值。若是你想修改某一個鍵(好比亮度鍵)對應的值,你能夠調用setValue:forKey:方法或當你指定一個濾鏡名時提供全部鍵值對。
須要處理的圖片必須是CIImage類型,調用initWithCGImage:方法可得到CIImage。由於CGImage又是做爲濾鏡的輸出,所以濾鏡之間可被鏈接在一塊兒(將濾鏡的輸出做爲initWithCGImage:方法的輸入參數)
當你構建一個濾鏡鏈時,並無作複雜的運算。只有當整個濾鏡鏈須要輸出一個CGImage時,密集型計算纔會發生。調用contextWithOptions:和createCGImage: fromRect:方法建立CIContext。與以往不一樣的地方是CIImage沒有frame與bounds屬性;只有extent屬性。你將很是頻繁的使用這個屬性做爲createCGImage: fromRect:方法的第二個參數。
接下來我將演示Core Image的使用。首先建立一個徑向漸變的濾鏡,該濾鏡是從白到黑的漸變方式,白色區域的半徑默認是100。接着將其與一張使用CIDarkenBlendMode濾鏡的圖片合成。CIDarkenBlendMode的做用是背景圖片樣本將被源圖片的黑色部分替換掉。
代碼以下:
UIImage* moi = [UIImage imageNamed:@"Mars.jpeg"];
CIImage* moi2 = [[CIImage alloc] initWithCGImage:moi.CGImage];
CIFilter* grad = [CIFilter filterWithName:@"CIRadialGradient"];
CIVector* center = [CIVector vectorWithX:moi.size.width / 2.0 Y:moi.size.height / 2.0];
// 使用setValue:forKey:方法設置濾鏡屬性
[grad setValue:center forKey:@"inputCenter"];
// 在指定濾鏡名時提供全部濾鏡鍵值對
CIFilter* dark = [CIFilter filterWithName:@"CIDarkenBlendMode" keysAndValues:@"inputImage", grad.outputImage, @"inputBackgroundImage", moi2, nil];
CIContext* c = [CIContext contextWithOptions:nil];
CGImageRef moi3 = [c createCGImage:dark.outputImage fromRect:moi2.extent];
UIImage* moi4 = [UIImage imageWithCGImage:moi3 scale:moi.scale orientation:moi.imageOrientation];
CGImageRelease(moi3);
圖4 圖片合成快照
這個例子可能沒有什麼吸引人的地方,由於全部一切均可以使用Core Graphics完成。除了Core Image是使用GPU處理,可能有點吸引人。Core Graphics也能夠作到徑向漸變並使用混合模式合成圖片。但Core Image要簡單得多,特別是當你有多個圖片輸入想重用一個濾鏡鏈時。而且Core Image的顏色調整功能比Core Graphics更增強大。對了,Core Image還能實現自動人臉識別哦!
繪製一個UIView
繪製一個UIVIew最靈活的方式就是由它本身完成繪製。實際上你不是繪製一個UIView,你只是子類化了UIView並賦予子類繪製本身的能力。當一個UIVIew須要執行繪圖操做的時, drawRect:方法就會被調用。覆蓋此方法讓你得到繪圖操做的機會。當drawRect:方法被調用,當前圖形上下文也被設置爲屬於視圖的圖形上下文。你可使用Core Graphics或UIKit提供的方法將圖形畫到該上下文中。
你不該該手動調用drawRect:方法!若是你想調用drawRect:方法更新視圖,只需發送setNeedsDisplay方法。這將使得drawRect:方法會在下一個適當的時間調用。固然,不要覆蓋drawRect:方法除非你知道這樣作絕對合法。比方說,在UIImageView子類中覆蓋drawRect:方法是不合法的,你將得不到你繪製的圖形。
在UIView子類的drawRect:方法中無需調用super,由於自己UIView的drawRect:方法是空的。爲了提升一些繪圖性能,你能夠調用setNeedsDisplayInRect方法從新繪製視圖的子區域,而視圖的其餘部分依然保持不變。
通常狀況下,你不該該過早的進行優化。繪圖代碼可能看上去很是的繁瑣,但它們是很是快的。而且iOS繪圖系統自身也是很是高效,它不會頻繁調用drawRect:方法,除非無可奈何(或調用了setNeedsDisplay方法)。一旦一個視圖已由本身繪製完成,那麼繪製的結果會被緩存下來留待重用,而不是每次重頭再來。(蘋果公司將緩存繪圖稱爲視圖的位圖存儲回填(bitmap backing store))。你可能會發現drawRect:方法中的代碼在整個應用程序生命週期內只被調用了一次!事實上,將代碼移到drawRect:方法中是提升性能的廣泛作法。這是由於繪圖引擎直接對屏幕進行渲染相對於先是脫屏渲染而後再將像素拷貝到屏幕要來的高效。
當視圖的backgroundColor爲nil而且opaque屬性爲YES,視圖的背景顏色就會變成黑色。
Core Graphics上下文屬性設置
當你在圖形上下文中繪圖時,當前圖形上下文的相關屬性設置將決定繪圖的行爲與外觀。所以,繪圖的通常過程是先設定好圖形上下文參數,而後繪圖。比方說,要畫一根紅線,接着畫一根藍線。那麼首先須要將上下文的線條顏色屬性設定爲爲紅色,而後畫紅線;接着設置上下文的線條顏色屬性爲藍色,再畫出藍線。表面上看,紅線和藍線是分開的,但事實上,在你畫每一條線時,線條顏色倒是整個上下文的屬性。不管你用的是UIKit方法仍是Core Graphics函數。
由於圖形上下文在每一時刻都有一個肯定的狀態,該狀態歸納了圖形上下文全部屬性的設置。爲了便於操做這些狀態,圖形上下文提供了一個用來持有狀態的棧。調用CGContextSaveGState函數,上下文會將完整的當前狀態壓入棧頂;調用CGContextRestoreGState函數,上下文查找處在棧頂的狀態,並設置當前上下文狀態爲棧頂狀態。
所以通常繪圖模式是:在繪圖以前調用CGContextSaveGState函數保存當前狀態,接着根據須要設置某些上下文狀態,而後繪圖,最後調用CGContextRestoreGState函數將當前狀態恢復到繪圖以前的狀態。要注意的是,CGContextSaveGState函數和CGContextRestoreGState函數必須成對出現,不然繪圖極可能出現意想不到的錯誤,這裏有一個簡單的作法避免這種狀況。代碼以下:
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
{
// 繪圖代碼
}
CGContextRestoreGState(ctx);
}
但你不須要在每次修改上下文狀態以前都這樣作,由於你對某一上下文屬性的設置並不必定會和以前的屬性設置或其餘的屬性設置產生衝突。你徹底能夠在不調用保存和恢復函數的狀況下先設置線條顏色爲紅色,而後再設置爲藍色。但在必定狀況下,你但願你對狀態的設置是可撤銷的,我將在接下來討論這樣的狀況。
許多的屬性組成了一個圖形上下文狀態,這些屬性設置決定了在你繪圖時圖形的外觀和行爲。下面我列出了一些屬性和對應修改屬性的函數;雖然這些函數是關於Core Graphics的,但記住,實際上UIKit一樣是調用這些函數操縱上下文狀態。
線條的寬度和線條的虛線樣式
CGContextSetLineWidth、CGContextSetLineDash
線帽和線條聯接點樣式
CGContextSetLineCap、CGContextSetLineJoin、CGContextSetMiterLimit
線條顏色和線條模式
CGContextSetRGBStrokeColor、CGContextSetGrayStrokeColor、CGContextSetStrokeColorWithColor、CGContextSetStrokePattern
填充顏色和模式
CGContextSetRGBFillColor,CGContextSetGrayFillColor,CGContextSetFillColorWithColor, CGContextSetFillPattern
陰影
CGContextSetShadow、CGContextSetShadowWithColor
混合模式
CGContextSetBlendMode(決定你當前繪製的圖形與已經存在的圖形如何被合成)
總體透明度
CGContextSetAlpha(個別顏色也具備alpha成分)
文本屬性
CGContextSelectFont、CGContextSetFont、CGContextSetFontSize、CGContextSetTextDrawingMode、CGContextSetCharacterSpacing
是否開啓反鋸齒和字體平滑
CGContextSetShouldAntialias、CGContextSetShouldSmoothFonts
另一些屬性設置:
裁剪區域:在裁剪區域外繪圖不會被實際的畫出來。
變換(或稱爲「CTM「,意爲當前變換矩陣): 改變你隨後指定的繪圖命令中的點如何被映射到畫布的物理空間。
許多這些屬性設置接下來我都會舉例說明。
路徑與繪圖
經過編寫移動虛擬畫筆的代碼描畫一段路徑,這樣的路徑並不構成一個圖形。繪製路徑意味着對路徑描邊或填充該路徑,也或者二者都作。一樣,你應該從某些繪圖程序中獲得過類似的體會。
一段路徑是由點到點的描畫構成。想象一下繪圖系統是你手裏的一隻畫筆,你首先必需要設置畫筆當前所處的位置,而後給出一系列命令告訴畫筆如何描畫隨後的每段路徑。每一段新增的路徑開始於當前點,當完成一條路徑的描畫,路徑的終點就變成了當前點。
下面列出了一些路徑描畫的命令:
定位當前點
CGContextMoveToPoint
描畫一條線
CGContextAddLineToPoint、CGContextAddLines
描畫一個矩形
CGContextAddRect、CGContextAddRects
描畫一個橢圓或圓形
CGContextAddEllipseInRect
描畫一段圓弧
CGContextAddArcToPoint、CGContextAddArc
經過一到兩個控制點描畫一段貝賽爾曲線
CGContextAddQuadCurveToPoint、CGContextAddCurveToPoint
關閉當前路徑
CGContextClosePath 這將從路徑的終點到起點追加一條線。若是你打算填充一段路徑,那麼就不須要使用該命令,由於該命令會被自動調用。
描邊或填充當前路徑
CGContextStrokePath、CGContextFillPath、CGContextEOFillPath、CGContextDrawPath。對當前路徑描邊或填充會清除掉路徑。若是你只想使用一條命令完成描邊和填充任務,可使用CGContextDrawPath命令,由於若是你只是使用CGContextStrokePath對路徑描邊,路徑就會被清除掉,你就不能再對它進行填充了。
建立路徑並描邊路徑或填充路徑只需一條命令就可完成的函數:CGContextStrokeLineSegments、CGContextStrokeRect、CGContextStrokeRectWithWidth、CGContextFillRect、CGContextFillRects、CGContextStrokeEllipseInRect、CGContextFillEllipseInRect。
一段路徑是被合成的,意思是它是由多條獨立的路徑組成。舉個例子,一條單獨的路徑可能由兩個獨立的閉合形狀組成:一個矩形和一個圓形。當你在構造一條路徑的中間過程(意思是在描畫了一條路徑後沒有調用描邊或填充命令,或調用CGContextBeginPath函數來清除路徑)調用CGContextMoveToPoint函數,就像是你拾起畫筆,並將畫筆移動到一個新的位置,如此來準備開始一段獨立的相同路徑。若是你擔憂當你開始描畫一條路徑的時候,已經存在的路徑和新的路徑會被認爲是已存在路徑的一個合成部分,你能夠調用CGContextBeginPath函數指定你繪製的路徑是一條獨立的路徑;蘋果的許多例子都是這樣作的,但在實際開發中我發現這是非必要的。
CGContextClearRect函數的功能是擦除一個區域。這個函數會擦除一個矩形內的全部已存在的繪圖;並對該區域執行裁剪。結果像是打了一個貫穿全部已存在繪圖的孔。
CGContextClearRect函數的行爲依賴於上下文是透明仍是不透明。當在圖形上下文中繪圖時,這會尤其明顯和直觀。若是圖片上下文是透明的(UIGraphicsBeginImageContextWithOptions第二個參數爲NO),那麼CGContextClearRect函數執行擦除後的顏色爲透明,反之則爲黑色。
當在一個視圖中直接繪圖(使用drawRect:或drawLayer:inContext:方法),若是視圖的背景顏色爲nil或顏色哪怕有一點點透明度,那麼CGContextClearRect的矩形區域將會顯示爲透明的,打出的孔將穿過視圖包括它的背景顏色。若是背景顏色徹底不透明,那麼CGContextClearRect函數的結果將會是黑色。這是由於視圖的背景顏色決定了是否視圖的圖形上下文是透明的仍是不透明的。
圖5 CGContextClearRect函數的應用
如圖5,在左邊的藍色正方形被挖去部分留爲黑色,然而在右邊的藍色正方形也被挖去部分留爲透明。但這兩個正方形都是UIView子類的實例,採用相同的繪圖代碼!不一樣之處在於視圖的背景顏色,左邊的正方形的背景顏色在nib文件中
可是這卻徹底改變了CGContextClearRect函數的效果。UIView子類的drawRect:方法看起來像這樣:
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor);
CGContextFillRect(con, rect);
CGContextClearRect(con, CGRectMake(0,0,30,30));
爲了說明典型路徑的描畫命令,我將生成一個向上的箭頭圖案,我謹慎避免使用便利函數操做,也許這不是建立箭頭最好的方式,但依然清楚的展現了各類典型命令的用法。
圖6 一個簡單的路徑繪圖
CGContextRef con = UIGraphicsGetCurrentContext();
// 繪製一個黑色的垂直黑色線,做爲箭頭的杆子
CGContextMoveToPoint(con, 100, 100);
CGContextAddLineToPoint(con, 100, 19);
CGContextSetLineWidth(con, 20);
CGContextStrokePath(con);
// 繪製一個紅色三角形箭頭
CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);
CGContextMoveToPoint(con, 80, 25);
CGContextAddLineToPoint(con, 100, 0);
CGContextAddLineToPoint(con, 120, 25);
CGContextFillPath(con);
// 從箭頭杆子上裁掉一個三角形,使用清除混合模式
CGContextMoveToPoint(con, 90, 101);
CGContextAddLineToPoint(con, 100, 90);
CGContextAddLineToPoint(con, 110, 101);
CGContextSetBlendMode(con, kCGBlendModeClear);
CGContextFillPath(con);
確切的說,爲了以防萬一,咱們應該在繪圖代碼周圍使用CGContextSaveGState和CGContextRestoreGState函數。可對於這個例子來講,添加與否不會有任何的區別。由於上下文在調用drawRect:方法中不會被持久,因此不會被破壞。
若是一段路徑須要重用或共享,你能夠將路徑封裝爲CGPath(具體類型是CGPathRef)。你能夠建立一個新的CGMutablePathRef對象並使用多個相似於圖形的路徑函數的CGPath函數構造路徑,或者使用CGContextCopyPath函數複製圖形上下文的當前路徑。有許多CGPath函數可用於建立基於簡單幾何形狀的路徑(CGPathCreateWithRect、CGPathCreateWithEllipseInRect)或基於已存在路徑(CGPathCreateCopyByStrokingPath、CGPathCreateCopyDashingPath、CGPathCreateCopyByTransformingPath)。
UIKit的UIBezierPath類包裝了CGPath。它提供了用於繪製某種形狀路徑的方法,以及用於描邊、填充、存取某些當前上下文狀態的設置方法。相似地,UIColor提供了用於設置當前上下文描邊與填充的顏色。所以咱們能夠重寫咱們以前繪製箭頭的代碼:
UIBezierPath* p = [UIBezierPath bezierPath];
[p moveToPoint:CGPointMake(100,100)];
[p addLineToPoint:CGPointMake(100, 19)];
[p setLineWidth:20];
[p stroke];
[[UIColor redColor] set];
[p removeAllPoints];
[p moveToPoint:CGPointMake(80,25)];
[p addLineToPoint:CGPointMake(100, 0)];
[p addLineToPoint:CGPointMake(120, 25)];
[p fill];
[p removeAllPoints];
[p moveToPoint:CGPointMake(90,101)];
[p addLineToPoint:CGPointMake(100, 90)];
[p addLineToPoint:CGPointMake(110, 101)];
[p fillWithBlendMode:kCGBlendModeClear alpha:1.0];
在這種特殊狀況下,完成一樣的工做並無節省多少代碼,可是UIBezierPath仍然仍是有用的。若是你須要對象特性,UIBezierPath提供了一個便利方法:bezierPathWithRoundedRect:cornerRadius:,它可用於繪製帶有圓角的矩形,若是是使用Core Graphics就至關冗長乏味了。還能夠只讓圓角出如今左上角和右上角。
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextSetLineWidth(ctx, 3);
UIBezierPath *path;
path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(100, 100, 100, 100) byRoundingCorners:(UIRectCornerTopLeft |UIRectCornerTopRight) cornerRadii:CGSizeMake(10, 10)];
[path stroke];
}
圖7 左右圓角矩形
裁剪
路徑的另外一用處是遮蔽區域,以防對遮蔽區域進一步繪圖。這種用法被稱爲裁剪。裁剪區域外的圖形不會被繪製到。默認狀況下,一個圖形上下文的裁剪區域是整個圖形上下文。你可在上下文中的任何地方繪圖。
總的來講,裁剪區域是上下文的一個特性。與已存在的裁剪區域相交會出現新的裁剪區域。因此若是你應用了你本身的裁剪區域,稍後將它從圖形上下文中移除的作法是使用CGContextSaveGState和CGContextRestoreGState函數將代碼包裝起來。
爲了便於說明這一點,我使用裁剪而不是使用混合模式在箭頭杆子上打孔的方法重寫了生成箭頭的代碼。這樣作有點小複雜,由於咱們想要裁剪區域不在三角形內而在三角形外部。爲了代表這一點,咱們使用了一個三角形和一個矩形組成了一個組合路徑。
當填充一個組合路徑並使用它表示一個裁剪區域時,系統遵循如下兩規則之一:
環繞規則(Winding rule)
若是邊界是順時針繪製,那麼在其內部逆時針繪製的邊界所包含的內容爲空。若是邊界是逆時針繪製,那麼在其內部順時針繪製的邊界所包含的內容爲空。
奇偶規則
最外層的邊界表明內部都有效,都要填充;以後向內第二個邊界表明它的內部無效,不需填充;如此規則繼續向內尋找邊界線。咱們的狀況很是簡單,因此使用奇偶規則就很容易了。這裏咱們使用CGContextEOCllip設置裁剪區域而後進行繪圖。(若是不是很明白,能夠參見這篇文章:五種方法繪製有孔的2d形狀)
CGContextRef con = UIGraphicsGetCurrentContext();
// 在上下文裁剪區域中挖一個三角形狀的孔
CGContextMoveToPoint(con, 90, 100);
CGContextAddLineToPoint(con, 100, 90);
CGContextAddLineToPoint(con, 110, 100);
CGContextClosePath(con);
CGContextAddRect(con, CGContextGetClipBoundingBox(con));
// 使用奇偶規則,裁剪區域爲矩形減去三角形區域
CGContextEOClip(con);
// 繪製垂線
CGContextMoveToPoint(con, 100, 100);
CGContextAddLineToPoint(con, 100, 19);
CGContextSetLineWidth(con, 20);
CGContextStrokePath(con);
// 畫紅色箭頭
CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);
CGContextMoveToPoint(con, 80, 25);
CGContextAddLineToPoint(con, 100, 0);
CGContextAddLineToPoint(con, 120, 25);
CGContextFillPath(con);
漸變
漸變能夠很簡單也能夠很複雜。一個簡單的漸變(接下來要討論的)由一端點的顏色與另外一端點的顏色決定,若是在中間點加入顏色(可選),那麼漸變會在上下文的兩個點之間線性的繪製或在上下文的兩個圓之間放射狀的繪製。不能使用漸變做爲路徑的填充色,但可以使用裁剪限制對路徑形狀的漸變。
我重寫了繪製箭頭的代碼,箭桿使用了線性漸變。效果如圖7所示。
圖8 箭頭杆子漸變
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextSaveGState(con);
// 在上下文裁剪區域挖一個三角形孔
CGContextMoveToPoint(con, 90, 100);
CGContextAddLineToPoint(con, 100, 90);
CGContextAddLineToPoint(con, 110, 100);
CGContextClosePath(con);
CGContextAddRect(con, CGContextGetClipBoundingBox(con));
CGContextEOClip(con);
//繪製一個垂線,讓它的輪廓形狀成爲裁剪區域
CGContextMoveToPoint(con, 100, 100);
CGContextAddLineToPoint(con, 100, 19);
CGContextSetLineWidth(con, 20);
// 使用路徑的描邊版本替換圖形上下文的路徑
CGContextReplacePathWithStrokedPath(con);
// 對路徑的描邊版本實施裁剪
CGContextClip(con);
// 繪製漸變
CGFloat locs[3] = { 0.0, 0.5, 1.0 };
CGFloat colors[12] = {
0.3,0.3,0.3,0.8, // 開始顏色,透明灰
0.0,0.0,0.0,1.0, // 中間顏色,黑色
0.3,0.3,0.3,0.8 // 末尾顏色,透明灰
};
CGColorSpaceRef sp = CGColorSpaceCreateDeviceGray();
CGGradientRef grad = CGGradientCreateWithColorComponents (sp, colors, locs, 3);
CGContextDrawLinearGradient(con, grad, CGPointMake(89,0), CGPointMake(111,0), 0);
CGColorSpaceRelease(sp);
CGGradientRelease(grad);
CGContextRestoreGState(con); // 完成裁剪
// 繪製紅色箭頭
CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);
CGContextMoveToPoint(con, 80, 25);
CGContextAddLineToPoint(con, 100, 0);
CGContextAddLineToPoint(con, 120, 25);
CGContextFillPath(con);
調用CGContextReplacePathWithStrokedPath函數僞裝對當前路徑描邊,並使用當前線段寬度和與線段相關的上下文狀態設置。但接着建立的是描邊路徑外部的一個新的路徑。所以,相對於使用粗的線條,咱們使用了一個矩形區域做爲裁剪區域。
雖然過程比較冗長可是很是的簡單;咱們將漸變描述爲一組在一端點(0.0)和另外一端點(1.0)之間連續區上的位置,以及設置與每一個位置相對應的顏色。爲了提亮邊緣的漸變,加深中間的漸變,我使用了三個位置,黑色點的位置是0.5。爲了建立漸變,還須要提供一個顏色空間。最後,我建立出了該漸變,並對裁剪區域繪製線性漸變,最後釋放了顏色空間和漸變。
顏色與模板
在iOS中,CGColor表示顏色(具體類型爲CGColorRef)。使用UIColor的colorWithCGColor:和CGColor方法可bridged cast到UIColor。
在iOS中,模板表示爲CGPattern(具體類型爲CGPatternRef)。你能夠建立一個模板並使用它進行描邊或填充。其過程是至關複雜的。做爲一個很是簡單的例子,我將使用紅藍相間的三角形替換箭頭的三角形部分。如今移除下面行:
CGContextSetFillColorWithColor(con, [UIColor redColor].CGColor));
在被移除的地方填入下面代碼:
CGColorSpaceRef sp2 = CGColorSpaceCreatePattern(NULL);
CGContextSetFillColorSpace (con, sp2);
CGColorSpaceRelease (sp2);
CGPatternCallbacks callback = {0, &drawStripes, NULL };
CGAffineTransform tr = CGAffineTransformIdentity;
CGPatternRef patt = CGPatternCreate(NULL,CGRectMake(0,0,4,4), tr, 4, 4, kCGPatternTilingConstantSpacingMinimalDistortion, true, &callback);
CGFloat alph = 1.0;
CGContextSetFillPattern(con, patt, &alph);
CGPatternRelease(patt);
代碼很是冗長,但它倒是一個完整的樣板。如今咱們從後往前分析代碼: 咱們調用CGContextSetFillPattern不是設置填充顏色,咱們設置的是填充的模板。函數的第三個參數是一個指向CGFloat的指針,因此咱們事先設置CGFloat自身。第二個參數是一個CGPatternRef對象,因此咱們須要事先建立CGPatternRef,並在最後釋放它。
如今開始討論CGPatternCreate。一個模板是在一個矩形元中的繪圖。咱們須要矩形元的尺寸(第二個參數)以及矩形元原始點之間的間隙(第四和第五個參數)。這這種狀況下,矩形元是4*4的,每個矩形元與它的周圍矩形元是緊密貼合的。咱們須要提供一個應用到矩形元的變換參數(第三個參數);在這種狀況下,咱們不須要變換作什麼工做,因此咱們應用了一個恆等變換。咱們應用了一個瓷磚規則(第六個參數)。咱們須要聲明的是顏色模板不是漏印(stencil)模板,因此參數值爲true。而且咱們須要提供一個指向回調函數的指針,回調函數的工做是向矩形元繪製模板。第八個參數是一個指向CGPatternCallbacks結構體的指針。這個結構體由數字0和兩個指向函數的指針構成。第一個函數指針指向的函數當模板被繪製到矩形元中被調用,第二個函數指針指向的函數當模板被釋放後調用。第二個函數指針咱們沒有指定,它的存在主要是爲了內存管理的須要。但在這個簡單的例子中,咱們並不須要。
在你使用顏色模板調用CGContextSetFillPattern函數以前,你須要設置將應用到模板顏色空間的上下文填充顏色空間。若是你忽略這項工做,那麼當你調用CGContextSetFillPattern函數時會發生錯誤。因此咱們建立了顏色空間,設置它做爲上下文的填充顏色空間,並在後面作了釋放。
到這裏咱們仍然沒有完成繪圖。由於我尚未編寫向矩形元中繪圖的函數!繪圖函數地址被表示爲&drawStripes。繪圖代碼以下所示:
void drawStripes (void *info, CGContextRef con) {
// assume 4 x 4 cell
CGContextSetFillColorWithColor(con, [[UIColor redColor] CGColor]);
CGContextFillRect(con, CGRectMake(0,0,4,4));
CGContextSetFillColorWithColor(con, [[UIColor blueColor] CGColor]);
CGContextFillRect(con, CGRectMake(0,0,4,2));
}
圖9 模板填充
如你所見,實際的模板繪圖代碼是很是簡單的。惟一的複雜點在於CGPatternCreate函數必須與模板繪圖函數的矩形元尺寸相同。咱們知道矩形元的尺寸爲4*4,因此咱們用紅色填充它,並接着填充它的下半部分爲綠色。當這些矩形元被水平垂直平鋪時,咱們獲得瞭如圖8所示的條紋圖案。
注意,最後圖形上下文遺留下了一個不可取的狀態,即填充顏色空間被設置爲了一個模板顏色空間。若是稍後嘗試設置填充顏色爲常規顏色,就會引發錯誤。一般的解決方案是,使用CGContextSaveGState和CGContextRestoreGState函數將代碼包起來。
你可能觀察到圖8的平鋪效果並不與箭頭的三角形內部相符合:最底部的彷佛只平鋪了一半藍色。這是由於一個模板的定位並不關心你填充(描邊)的形狀,總的來講它只關心圖形上下文。咱們能夠調用CGContextSetPatternPhase函數改變模板的定位。
圖形上下文變換
就像UIView能夠實現變換,一樣圖形上下文也具有這項功能。然而對圖形上下文應用一個變換操做不會對已在圖形上下文上的繪圖產生什麼影響,它只會影響到在上下文變換以後被繪製的圖形,並改變被映射到圖形上下文區域的座標方式。一個圖形上下文變換被稱爲CTM,意爲「當前變換矩陣「(current transformation matrix)。
徹底利用圖形上下文的CTM來免於即便是簡單的計算操做是很常見的。你可使用CGContextConcatCTM函數將當前變換乘上任何CGAffineTransform,還有一些便利函數可對當前變換應用平移、縮放,旋轉變換。
當你得到上下文的時候,對圖形上下文的基本變換已經設置好了;這就是系統能映射上下文繪圖座標到屏幕座標的緣由。不管你對當前變換應用了什麼變換,基本變換變換依然有效而且繪圖繼續工做。經過將你的變換代碼封裝到CGContextSaveGState和CGContextRestoreGState函數調用中,對基本變換應用的變換操做能夠被還原。
舉個例子,對於咱們迄今爲止使用代碼繪製的向上箭頭來講,已知的放置箭頭的方式僅僅只有一個位置:箭頭矩形框的左上角被硬編碼在座標{80,0}。這樣代碼很難理解、靈活性差、且很難被重用。最明智的作法是經過將全部代碼中的x座標值減去80,讓箭頭矩形框左上角在座標{0,0}。事先應用一個簡單的平移變換,很容易將箭頭畫在任何位置。爲了映射座標到箭頭的左上角,咱們使用下面代碼:
CGContextTranslateCTM(con, 80, 0); //在座標{0,0}處繪製箭頭
旋轉變換特別的有用,它可讓你在一個被旋轉的方向上進行繪製而無需使用任何複雜的三角函數。然而這略有點複雜,由於旋轉變換圍繞的點是原點座標。這幾乎不是你所想要的,因此你先是應用了一個平移變換,爲的是映射原點到你真正想繞其旋轉的點。可是接着,在旋轉以後,爲了算出你在哪裏繪圖,你可能須要作一次逆向平移變換。
爲了說明這個作法,我將繞箭頭杆子尾部旋轉多個角度重複繪製箭頭,並把對箭頭的繪圖封裝爲UIImage對象。接着咱們簡單重複繪製UIImage對象。
具體代碼以下:
- (void)drawRect:(CGRect)rect {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(40,100), NO, 0.0);
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextSaveGState(con);
CGContextMoveToPoint(con, 90 - 80, 100);
CGContextAddLineToPoint(con, 100 - 80, 90);
CGContextAddLineToPoint(con, 110 - 80, 100);
CGContextMoveToPoint(con, 110 - 80, 100);
CGContextAddLineToPoint(con, 100 - 80, 90);
CGContextAddLineToPoint(con, 90 - 80, 100);
CGContextClosePath(con);
CGContextAddRect(con, CGContextGetClipBoundingBox(con));
CGContextEOClip(con);
CGContextMoveToPoint(con, 100 - 80, 100);
CGContextAddLineToPoint(con, 100 - 80, 19);
CGContextSetLineWidth(con, 20);
CGContextReplacePathWithStrokedPath(con);
CGContextClip(con);
CGFloat locs[3] = { 0.0, 0.5, 1.0 };
CGFloat colors[12] = {
0.3,0.3,0.3,0.8,
0.0,0.0,0.0,1.0,
0.3,0.3,0.3,0.8
};
CGColorSpaceRef sp = CGColorSpaceCreateDeviceGray();
CGGradientRef grad = CGGradientCreateWithColorComponents (sp, colors, locs, 3);
CGContextDrawLinearGradient (con, grad, CGPointMake(89 - 80,0), CGPointMake(111 - 80,0), 0);
CGColorSpaceRelease(sp);
CGGradientRelease(grad);
CGContextRestoreGState(con);
CGColorSpaceRef sp2 = CGColorSpaceCreatePattern(NULL);
CGContextSetFillColorSpace (con, sp2);
CGColorSpaceRelease (sp2);
CGPatternCallbacks callback = {0, &drawStripes, NULL };
CGAffineTransform tr = CGAffineTransformIdentity;
CGPatternRef patt = CGPatternCreate(NULL,CGRectMake(0,0,4,4),tr,4,4,kCGPatternTilingConstantSpacingMinimalDistortion,true, &callback);
CGFloat alph = 1.0;
CGContextSetFillPattern(con, patt, &alph);
CGPatternRelease(patt);
CGContextMoveToPoint(con, 80 - 80, 25);
CGContextAddLineToPoint(con, 100 - 80, 0);
CGContextAddLineToPoint(con, 120 - 80, 25);
CGContextFillPath(con);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
con = UIGraphicsGetCurrentContext();
[im drawAtPoint:CGPointMake(0,0)];
for (int i=0; i<3; i++) {
CGContextTranslateCTM(con, 20, 100);
CGContextRotateCTM(con, 30 * M_PI/180.0);
CGContextTranslateCTM(con, -20, -100);
[im drawAtPoint:CGPointMake(0,0)];
}
}
圖10 使用CTM旋轉變換
變換有多個方法解決咱們早期使用CGContextDrawImage函數遇到的倒置問題。相對於逆向繪圖,咱們選擇逆向咱們繪圖的上下文。實質上,咱們對上下文座標系統應用了一個「倒置」變換。你自上而下移動上下文,接着你經過應用一個讓y座標乘以-1的縮放變換逆向y座標的方向。
CGContextTranslateCTM(con, 0, theHeight);
CGContextScaleCTM(con, 1.0, -1.0);
上下文的頂部應該被你往下移動多遠依賴於你繪製的圖片。好比說咱們能夠繪製沒有倒置問題的兩個半邊的火星圖形(前面討論的一個例子)。
CGContextTranslateCTM(con, 0, sz.height); // sz爲[mars size]
CGContextScaleCTM(con, 1.0, -1.0);
CGContextDrawImage(con, CGRectMake(0, 0, sz.width/2.0, sz.height), marsLeft);
CGContextDrawImage(con, CGRectMake(b.size.width-sz.width/2.0, 0, sz.width/2.0, sz.height),marsRight);
陰影
爲了在繪圖上加入陰影,可在繪圖以前設置上下文的陰影值。陰影的位置表示爲CGSize,若是CGSize的兩個值都是正數,則表示陰影是朝下和朝右的。模糊度被表示爲任何一個正數。蘋果沒有解釋縮放的工做方式,但實驗代表12是最佳的模糊度,99及以上的模糊度會讓陰影變得不成形。
我在圖9的基礎上給上下文加了一個陰影:
~~~~~~~~~~~~
con = UIGraphicsGetCurrentContext();
CGContextSetShadow(con, CGSizeMake(7, 7), 12);
[im drawAtPoint:CGPointMake(0,0)];
~~~~~~~~~~~~~~~
然而,使用這種方法有一個不太明顯的問題。咱們是在每繪製一個箭頭的時候加上的陰影。所以,箭頭的陰影會投射在另外一個箭頭上面。咱們想要的是讓全部的箭頭集體地投射出一個陰影。解決方法是使用一個透明的圖層;該圖層相似一個先是疊加全部繪圖而後加上陰影的一個子上下文。代碼以下:
con = UIGraphicsGetCurrentContext();
CGContextSetShadow(con, CGSizeMake(7, 7), 12);
CGContextBeginTransparencyLayer(con, NULL);
[im drawAtPoint:CGPointMake(0,0)];
for (int i=0; i<3; i++) {
CGContextTranslateCTM(con, 20, 100);
CGContextRotateCTM(con, 30 * M_PI/180.0);
CGContextTranslateCTM(con, -20, -100);
[im drawAtPoint:CGPointMake(0,0)];
}
// 在調用了CGContextEndTransparencyLayer函數以後,
// 圖層內容會在應用全局alpha和上下文陰影狀態以後被合成到上下文中
CGContextEndTransparencyLayer(con);
圖11 陰影效果
點與像素
一個點是由xy座標描述的一個無窮小量的位置。經過指定點實如今圖形上下文中的繪圖。咱們並無關心設備的分辨率,由於Core Graphics已經精細地將繪圖映射到物理輸出設備(基於CTM、反鋸齒和平滑技術)。所以,文章以前的討論只關心圖形上下文的點,不關注點與屏幕像素的關係。
然而像素是真實存在的。一個像素是真實世界中一個具備完整物理尺寸的顯示單元。整數的點實際上介於像素之間。在單分辨率設備上,這可能會讓人感到迷惑。比方說,若是使用線寬爲1的線條對一個整數座標的垂直路徑描邊,那麼線條將會被分爲兩半,分別落在路徑的兩側。因此在單分辨率設備上線寬會變成2px(由於設備沒法表示半個像素)。
圖12 整數的點座標與偏移0.5點的座標對應的描邊處理
當你遇到顯示效果不佳的時,可能會被建議經過對座標增減0.5讓它在像素中居中。這個建議可能有效,如圖11。但它只是作了一些頭腦簡單的假設。一個複雜的作法是得到UIView的contentScaleFactor屬性。這個值爲1.0或2.0,因此你能夠除以這個屬性值獲得從像素到點的轉換。還能夠想一想用最精確的方式繪製一條水平或垂直的線條的方式不是描邊路徑,而是填充路徑。使用這種方法UIView的子類代碼將能夠在任何設備上繪製一條完美的1px寬的垂線,代碼以下:
CGContextFillRect(con, CGRectMake(100,0,1.0/self.contentScaleFactor,100));
內容模式
一個視圖向它自身繪圖,相對於只有背景顏色和子視圖,它還有內容。這意味着每當視圖被調整大小它的contentMode屬性就變得很是重要。正如我以前提到的,繪圖系統會盡量避免重頭開始繪製視圖。相反,繪圖系統將使用以前繪圖操做的緩存結果(位圖回填)。因此,若是視圖被從新調整大小,系統可能簡單的伸縮或重定位緩存繪圖,前提是你的contentMode設置指令是是這樣設置的。
說明這一點略有點複雜。由於我須要安排調整視圖大小而不引發重繪操做(調用drawRect:方法)。當程序啓動時,我將建立一個MyView實例,並將它放在window上。接着將執行調整MyView尺寸的操做延遲到window出現和界面初次顯示以後:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.window.rootViewController = [UIViewController new];
MyView* mv =[[MyView alloc] initWithFrame:CGRectMake(0, 0, self.window.bounds.size.width - 50, 150)];
mv.center = self.window.center;
[self.window.rootViewController.view addSubview: mv];
mv.opaque = NO;
mv.tag = 111; // so I can get a reference to this view later
[self performSelector:@selector(resize:) withObject:nil afterDelay:0.1];
self.window.backgroundColor = [UIColor whiteColor];
[self.window makeKeyAndVisible];
return YES;
}
咱們將視圖的高度調成以前的2倍。沒有觸發drawRect:方法的調用。若是咱們視圖的drawRect:方法代碼和生成圖9的代碼相同,則咱們獲得如圖12的結果,視圖被顯示在正確高度上。
圖13 內容自動伸展
但是遲早drawRect:方法會被調用,繪圖將按照drawRect:方法中的代碼被刷新。代碼不會將箭頭繪製在相對於視圖邊界的高度。它是在一個固定的高度。所以箭頭會伸展,並且會在之後某個時間返回到原始的尺寸。
一般咱們的視圖的contentMode屬性須要與視圖繪製本身的方式一致。假設咱們的drawRect:方法中的代碼讓箭頭的尺寸和位置相對於視圖的邊界原點,即它的左上方。因此咱們能夠設置它的contentMode爲UIViewContentModeTopLeft。又或者,咱們能夠將contentMode設置爲UIVIewContentModeRedraw,這將引發緩存內容的自動縮放和重定位被關閉,最終結果是視圖的setNeedsDisplay方法將被調用,觸發drawRect:方法重繪視圖內容。
在另外一方面,若是一個視圖只是暫時被調整大小。假設是做爲動畫的一部分,那麼伸縮行爲正是你所想要的。假設咱們的動畫是想要讓視圖變大而後還原回原始大小以達到做爲吸引用戶的一種手段。這就須要視圖伸縮的時候視圖的內容也跟着伸縮,正確的contentMode的值是UIViewContentModeScaleToFill,被伸縮的內容僅僅是視圖內容的一副緩存圖片,因此它運行起來十分的高效。
完。
本文由海水的味道翻譯,轉載請註明譯者和出處,請勿用於商業用途!
譯者說明:譯文中的錯誤或不當之處望不吝指出。
Drop me a line: xdreamarshal@gmail.com, http://weibo.com/xdream86