iOS繪圖詳解 – iPhone手機開發 iPhone軟體開發教學課程

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函數關閉圖形上下文。

第二種方法是利用cocoa為你生成的圖形上下文。當你子類化瞭一個UIView並實現瞭自己的drawRect:方法後,一旦drawRect:方法被調用,Cocoa就會為你創建一個圖形上下文,此時你對圖形上下文的所有繪圖操作都會顯示在UIView上。

判斷一個上下文是否為當前圖形上下文需要註意的幾點:

1.UIGraphicsBeginImageContextWithOptions函數不僅僅是創建瞭一個適用於圖形操作的上下文,並且該上下文也屬於當前上下文。

2.當drawRect方法被調用時,UIView的繪圖上下文屬於當前圖形上下文。

3.回調方法所持有的context:參數並不會讓任何上下文成為當前圖形上下文。此參數僅僅是對一個圖形上下文的引用罷瞭。

作為初學者,很容易被UIKit和Core Graphics兩個支持繪圖的框架迷惑。

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)]; [[UIColorblueColor]setFill]; [pfill]; }

第二種繪圖形式: 使用Core Graphics實現繪制藍色圓。

-( void )drawRect:(CGRect)rect{ CGContextRefcon=UIGraphicsGetCurrentContext(); CGContextAddEllipseInRect(con,CGRectMake(0,0,100,100)); CGContextSetFillColorWithColor(con,[UIColorblueColor].CGColor); CGContextFillPath(con); }

第三種繪圖形式: 我將在UIView子類的drawLayer:inContext:方法中實現繪圖任務。drawLayer:inContext:方法是一個繪制圖層內容的代理方法。為瞭能夠調用drawLayer:inContext:方法,我們需要設定圖層的代理對象。但要註意,不應該將UIView對象設置為顯示層的委托對象,這是因為UIView對象已經是隱式層的代理對象,再將它設置為另一個層的委托對象就會出問題。輕量級的做法是:編寫負責繪圖形的代理類。在MyView.h文件中聲明如下代碼:

@ interface MyLayerDelegate:NSObject @end

然後MyView.m文件中實現接口代碼:

@implementationMyLayerDelegate -( void )drawLayer:(CALayer*)layerinContext:(CGContextRef)ctx{ UIGraphicsPushContext(ctx); UIBezierPath*p=[UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)]; [[UIColorblueColor]setFill]; [pfill]; UIGraphicsPopContext(); } @end

直接將代理類的實現代碼放在MyView.m文件的#import代碼的下面,這樣感覺好像在使用私有類完成繪圖任務(雖然這不是私有類)。需要註意的是,我們所引用的上下文並不是當前上下文,所以為瞭能夠使用UIKit,我們需要將引用的上下文轉變成當前上下文。

因為圖層的代理是assign內存管理策略,那麼這裡就不能以局部變量的形式創建MyLayerDelegate實例對象賦值給圖層代理。這裡選擇在MyView.m中增加一個實例變量,因為實例變量默認是strong:

@ interface MyView(){ MyLayerDelegate*_layerDeleagete; } @end

使用該圖層代理:

MyView*myView=[[MyViewalloc]initWithFrame:CGRectMake(0,0,320,480)]; CALayer*myLayer=[CALayerlayer]; _layerDelegate=[[MyLayerDelegatealloc]init]; myLayer.delegate=_layerDelegate; [myView.layeraddSublayer:myLayer]; [myViewsetNeedsDisplay]; //調用此方法,drawLayer:inContext:方法才會被調用。

第四種繪圖形式: 使用Core Graphics在drawLayer:inContext:方法中實現同樣操作,代碼如下:

-( void )drawLayer:(CALayer*)layinContext:(CGContextRef)con{ CGContextAddEllipseInRect(con,CGRectMake(0,0,100,100)); CGContextSetFillColorWithColor(con,[UIColorblueColor].CGColor); CGContextFillPath(con); }

最後,演示UIGraphicsBeginImageContextWithOptions的用法,並從上下文中生成一個UIImage對象。生成UIImage對象的代碼並不需要等待某些方法被調用後或在UIView的子類中才能去做。

第五種繪圖形式: 使用UIKit實現:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100),NO,0); UIBezierPath*p=[UIBezierPathbezierPathWithOvalInRect:CGRectMake(0,0,100,100)]; [[UIColorblueColor]setFill]; [pfill]; UIImage*im=UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();

解釋一下UIGraphicsBeginImageContextWithOptions函數參數的含義:第一個參數表示所要創建的圖片的尺寸;第二個參數用來指定所生成圖片的背景是否為不透明,如上我們使用YES而不是NO,則我們得到的圖片背景將會是黑色,顯然這不是我想要的;第三個參數指定生成圖片的縮放因子,這個縮放因子與UIImage的scale屬性所指的含義是一致的。傳入0則表示讓圖片的縮放因子根據屏幕的分辨率而變化,所以我們得到的圖片不管是在單分辨率還是視網膜屏上看起來都會很好。

第六種繪圖形式: 使用Core Graphics實現:

UIGraphicsBeginImageContextWithOptions(CGSizeMake(100,100),NO,0); CGContextRefcon=UIGraphicsGetCurrentContext(); CGContextAddEllipseInRect(con,CGRectMake(0,0,100,100)); CGContextSetFillColorWithColor(con,[UIColorblueColor].CGColor); CGContextFillPath(con); UIImage*im=UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();

UIKit和Core Graphics可以在相同的圖形上下文中混合使用。在iOS 4.0之前,使用UIKit和UIGraphicsGetCurrentContext被認為是線程不安全的。而在iOS4.0以後蘋果讓繪圖操作在第二個線程中執行解決瞭此問題。

UIImage常用的繪圖操作

一個UIImage對象提供瞭向當前上下文繪制自身的方法。我們現在已經知道如何獲取一個圖片類型的上下文並將它轉變成當前上下文。

平移操作:下面的代碼展示瞭如何將UIImage繪制在當前的上下文中。

UIImage*mars=[UIImageimageNamed:@ "Mars.png" ]; CGSizesz=[marssize]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*2,sz.height),NO,0); [marsdrawAtPoint:CGPointMake(0,0)]; [marsdrawAtPoint:CGPointMake(sz.width,0)]; UIImage*im=UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); UIImageView*iv=[[UIImageViewalloc]initWithImage:im]; [self.window.rootViewController.viewaddSubview:iv]; iv.center=self.window.center;

圖1 UIImage平移處理

縮放操作: 下面代碼展示瞭如何對UIImage進行縮放操作:

UIImage*mars=[UIImageimageNamed:@ "Mars.png" ]; CGSizesz=[marssize]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*2,sz.height*2),NO,0); [marsdrawInRect:CGRectMake(0,0,sz.width*2,sz.height*2)]; [marsdrawInRect:CGRectMake(sz.width/2.0,sz.height/2.0,sz.width,sz.height)blendMode:kCGBlendModeMultiplyalpha:1.0]; UIImage*im=UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();

圖2 UIImage縮放處理

UIImage沒有提供截取圖片指定區域的功能。但通過創建一個較小的圖形上下文並移動圖片到一個適當的圖形上下文坐標系內,指定區域內的圖片就會被獲取。

裁剪操作: 下面代碼展示瞭如何獲取圖片的右半邊:

UIImage*mars=[UIImageimageNamed:@ "Mars.png" ]; CGSizesz=[marssize]; UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width/2.0,sz.height),NO,0); [marsdrawAtPoint: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=[UIImageimageNamed:@ "Mars.png" ]; //抽取圖片的左右半邊 CGSizesz=[marssize]; CGImageRefmarsLeft=CGImageCreateWithImageInRect([marsCGImage],CGRectMake(0,0,sz.width/2.0,sz.height)); CGImageRefmarsRight=CGImageCreateWithImageInRect([marsCGImage],CGRectMake(sz.width/2.0,0,sz.width/2.0,sz.height)); //將每一個CGImage繪制到圖形上下文中 UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5,sz.height),NO,0); CGContextRefcon=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方法,我們就將倒轉的圖片還原回來瞭。實現代碼如下:

CGImageRefflip(CGImageRefim){ CGSizesz=CGSizeMake(CGImageGetWidth(im),CGImageGetHeight(im)); UIGraphicsBeginImageContextWithOptions(sz,NO,0); CGContextDrawImage(UIGraphicsGetCurrentContext(),CGRectMake(0,0,sz.width,sz.height),im); CGImageRefresult=[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=[UIImageimageNamed:@ "Mars.png" ]; CGSizesz=[marssize]; //轉換CGImage並使用對應的CGImage尺寸截取圖片的左右部分 CGImageRefmarsCG=[marsCGImage]; CGSizeszCG=CGSizeMake(CGImageGetWidth(marsCG),CGImageGetHeight(marsCG)); CGImageRefmarsLeft=CGImageCreateWithImageInRect(marsCG,CGRectMake(0,0,szCG.width/2.0,szCG.height)); CGImageRefmarsRight=CGImageCreateWithImageInRect(marsCG,CGRectMake(szCG.width/2.0,0,szCG.width/2.0,szCG.height)); UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5,sz.height),NO,0); //剩下的和之前的代碼一樣,修復倒置問題 CGContextRefcon=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中,這樣做有兩大優點:

1.當UIImage繪圖時它會自動修復倒置問題

2.當你從CGImage轉化為Uimage時,可調用imageWithCGImage:scale:orientation:方法生成CGImage作為對縮放性的補償。

所以這是一個解決倒置和縮放問題的自包含方法。

代碼如下:

UIImage*mars=[UIImageimageNamed:@ "Mars.png" ]; CGSizesz=[marssize]; CGImageRefmarsCG=[marsCGImage]; CGSizeszCG=CGSizeMake(CGImageGetWidth(marsCG),CGImageGetHeight(marsCG)); CGImageRefmarsLeft=CGImageCreateWithImageInRect(marsCG,CGRectMake(0,0,szCG.width/2.0,szCG.height)); CGImageRefmarsRight=CGImageCreateWithImageInRect(marsCG,CGRectMake(szCG.width/2.0,0,szCG.width/2.0,szCG.height)); UIGraphicsBeginImageContextWithOptions(CGSizeMake(sz.width*1.5,sz.height),NO,0); [[UIImageimageWithCGImage:marsLeftscale:[marsscale]orientation:UIImageOrientationUp]drawAtPoint:CGPointMake(0,0)]; [[UIImageimageWithCGImage:marsRightscale:[marsscale]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=[UIImageimageNamed:@ "Mars.jpeg" ]; CIImage*moi2=[[CIImagealloc]initWithCGImage:moi.CGImage]; CIFilter*grad=[CIFilterfilterWithName:@ "CIRadialGradient" ]; CIVector*center=[CIVectorvectorWithX:moi.size.width/2.0Y:moi.size.height/2.0]; //使用setValue:forKey:方法設置濾鏡屬性 [gradsetValue:centerforKey:@ "inputCenter" ]; //在指定濾鏡名時提供所有濾鏡鍵值對 CIFilter*dark=[CIFilterfilterWithName:@ "CIDarkenBlendMode" keysAndValues:@ "inputImage" ,grad.outputImage,@ "inputBackgroundImage" ,moi2,nil]; CIContext*c=[CIContextcontextWithOptions:nil]; CGImageRefmoi3=[ccreateCGImage:dark.outputImagefromRect:moi2.extent]; UIImage*moi4=[UIImageimageWithCGImage:moi3scale:moi.scaleorientation: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{ CGContextRefctx=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:方法看起來像這樣:

CGContextRefcon=UIGraphicsGetCurrentContext(); CGContextSetFillColorWithColor(con,[UIColorblueColor].CGColor); CGContextFillRect(con,rect); CGContextClearRect(con,CGRectMake(0,0,30,30));

為瞭說明典型路徑的描畫命令,我將生成一個向上的箭頭圖案,我謹慎避免使用便利函數操作,也許這不是創建箭頭最好的方式,但依然清楚的展示瞭各種典型命令的用法。

圖6 一個簡單的路徑繪圖

CGContextRefcon=UIGraphicsGetCurrentContext(); //繪制一個黑色的垂直黑色線,作為箭頭的桿子 CGContextMoveToPoint(con,100,100); CGContextAddLineToPoint(con,100,19); CGContextSetLineWidth(con,20); CGContextStrokePath(con); //繪制一個紅色三角形箭頭 CGContextSetFillColorWithColor(con,[[UIColorredColor]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=[UIBezierPathbezierPath]; [pmoveToPoint:CGPointMake(100,100)]; [paddLineToPoint:CGPointMake(100,19)]; [psetLineWidth:20]; [pstroke]; [[UIColorredColor]set]; [premoveAllPoints]; [pmoveToPoint:CGPointMake(80,25)]; [paddLineToPoint:CGPointMake(100,0)]; [paddLineToPoint:CGPointMake(120,25)]; [pfill]; [premoveAllPoints]; [pmoveToPoint:CGPointMake(90,101)]; [paddLineToPoint:CGPointMake(100,90)]; [paddLineToPoint:CGPointMake(110,101)]; [pfillWithBlendMode:kCGBlendModeClearalpha:1.0];

在這種特殊情況下,完成同樣的工作並沒有節省多少代碼,但是UIBezierPath仍然還是有用的。如果你需要對象特性,UIBezierPath提供瞭一個便利方法:bezierPathWithRoundedRect:cornerRadius:,它可用於繪制帶有圓角的矩形,如果是使用Core Graphics就相當冗長乏味瞭。還可以隻讓圓角出現在左上角和右上角。

-( void )drawRect:(CGRect)rect{ CGContextRefctx=UIGraphicsGetCurrentContext(); CGContextSetStrokeColorWithColor(ctx,[UIColorblackColor].CGColor); CGContextSetLineWidth(ctx,3); UIBezierPath*path; path=[UIBezierPathbezierPathWithRoundedRect:CGRectMake(100,100,100,100)byRoundingCorners:(UIRectCornerTopLeft|UIRectCornerTopRight)cornerRadii:CGSizeMake(10,10)]; [pathstroke]; }

圖7 左右圓角矩形

裁剪

路徑的另一用處是遮蔽區域,以防對遮蔽區域進一步繪圖。這種用法被稱為裁剪。裁剪區域外的圖形不會被繪制到。默認情況下,一個圖形上下文的裁剪區域是整個圖形上下文。你可在上下文中的任何地方繪圖。

總的來說,裁剪區域是上下文的一個特性。與已存在的裁剪區域相交會出現新的裁剪區域。所以如果你應用瞭你自己的裁剪區域,稍後將它從圖形上下文中移除的做法是使用CGContextSaveGState和CGContextRestoreGState函數將代碼包裝起來。

為瞭便於說明這一點,我使用裁剪而不是使用混合模式在箭頭桿子上打孔的方法重寫瞭生成箭頭的代碼。這樣做有點小復雜,因為我們想要裁剪區域不在三角形內而在三角形外部。為瞭表明這一點,我們使用瞭一個三角形和一個矩形組成瞭一個組合路徑。

當填充一個組合路徑並使用它表示一個裁剪區域時,系統遵循以下兩規則之一:

環繞規則(Winding rule)

如果邊界是順時針繪制,那麼在其內部逆時針繪制的邊界所包含的內容為空。如果邊界是逆時針繪制,那麼在其內部順時針繪制的邊界所包含的內容為空。

奇偶規則

最外層的邊界代表內部都有效,都要填充;之後向內第二個邊界代表它的內部無效,不需填充;如此規則繼續向內尋找邊界線。我們的情況非常簡單,所以使用奇偶規則就很容易瞭。這裡我們使用CGContextEOCllip設置裁剪區域然後進行繪圖。(如果不是很明白,可以參見這篇文章: 五種方法繪制有孔的2d形狀 )

CGContextRefcon=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,[[UIColorredColor]CGColor]); CGContextMoveToPoint(con,80,25); CGContextAddLineToPoint(con,100,0); CGContextAddLineToPoint(con,120,25); CGContextFillPath(con);

漸變

漸變可以很簡單也可以很復雜。一個簡單的漸變(接下來要討論的)由一端點的顏色與另一端點的顏色決定,如果在中間點加入顏色(可選),那麼漸變會在上下文的兩個點之間線性的繪制或在上下文的兩個圓之間放射狀的繪制。不能使用漸變作為路徑的填充色,但可使用裁剪限制對路徑形狀的漸變。

我重寫瞭繪制箭頭的代碼,箭桿使用瞭線性漸變。效果如圖7所示。

圖8 箭頭桿子漸變

CGContextRefcon=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); //繪制漸變 CGFloatlocs[3]={0.0,0.5,1.0}; CGFloatcolors[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 //末尾顏色,透明灰 }; CGColorSpaceRefsp=CGColorSpaceCreateDeviceGray(); CGGradientRefgrad=CGGradientCreateWithColorComponents(sp,colors,locs,3); CGContextDrawLinearGradient(con,grad,CGPointMake(89,0),CGPointMake(111,0),0); CGColorSpaceRelease(sp); CGGradientRelease(grad); CGContextRestoreGState(con); //完成裁剪 //繪制紅色箭頭 CGContextSetFillColorWithColor(con,[[UIColorredColor]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));

在被移除的地方填入下面代碼:

CGColorSpaceRefsp2=CGColorSpaceCreatePattern(NULL); CGContextSetFillColorSpace(con,sp2); CGColorSpaceRelease(sp2); CGPatternCallbackscallback={0,&drawStripes,NULL}; CGAffineTransformtr=CGAffineTransformIdentity; CGPatternRefpatt=CGPatternCreate(NULL,CGRectMake(0,0,4,4),tr,4,4,kCGPatternTilingConstantSpacingMinimalDistortion, true ,&callback); CGFloatalph=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,CGContextRefcon){ //assume4x4cell CGContextSetFillColorWithColor(con,[[UIColorredColor]CGColor]); CGContextFillRect(con,CGRectMake(0,0,4,4)); CGContextSetFillColorWithColor(con,[[UIColorblueColor]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); CGContextRefcon=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); CGFloatlocs[3]={0.0,0.5,1.0}; CGFloatcolors[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 }; CGColorSpaceRefsp=CGColorSpaceCreateDeviceGray(); CGGradientRefgrad=CGGradientCreateWithColorComponents(sp,colors,locs,3); CGContextDrawLinearGradient(con,grad,CGPointMake(89-80,0),CGPointMake(111-80,0),0); CGColorSpaceRelease(sp); CGGradientRelease(grad); CGContextRestoreGState(con); CGColorSpaceRefsp2=CGColorSpaceCreatePattern(NULL); CGContextSetFillColorSpace(con,sp2); CGColorSpaceRelease(sp2); CGPatternCallbackscallback={0,&drawStripes,NULL}; CGAffineTransformtr=CGAffineTransformIdentity; CGPatternRefpatt=CGPatternCreate(NULL,CGRectMake(0,0,4,4),tr,4,4,kCGPatternTilingConstantSpacingMinimalDistortion, true ,&callback); CGFloatalph=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(); [imdrawAtPoint: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); [imdrawAtPoint: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為[marssize] 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); [imdrawAtPoint:CGPointMake(0,0)];

然而,使用這種方法有一個不太明顯的問題。我們是在每繪制一個箭頭的時候加上的陰影。因此,箭頭的陰影會投射在另一個箭頭上面。我們想要的是讓所有的箭頭集體地投射出一個陰影。解決方法是使用一個透明的圖層;該圖層類似一個先是疊加所有繪圖然後加上陰影的一個子上下文。代碼如下:

con=UIGraphicsGetCurrentContext(); CGContextSetShadow(con,CGSizeMake(7,7),12); CGContextBeginTransparencyLayer(con,NULL); [imdrawAtPoint: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); [imdrawAtPoint: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*)applicationdidFinishLaunchingWithOptions:(NSDictionary*)launchOptions{ self.window=[[UIWindowalloc]initWithFrame:[[UIScreenmainScreen]bounds]]; self.window.rootViewController=[UIViewController new ]; MyView*mv=[[MyViewalloc]initWithFrame:CGRectMake(0,0,self.window.bounds.size.width-50,150)]; mv.center=self.window.center; [self.window.rootViewController.viewaddSubview:mv]; mv.opaque=NO; mv.tag=111; //soIcangetareferencetothisviewlater [selfperformSelector:@selector(resize:)withObject:nilafterDelay:0.1]; self.window.backgroundColor=[UIColorwhiteColor]; [self.windowmakeKeyAndVisible]; return YES; }

我們將視圖的高度調成之前的2倍。沒有觸發drawRect:方法的調用。如果我們視圖的drawRect:方法代碼和生成圖9的代碼相同,則我們得到如圖12的結果,視圖被顯示在正確高度上。

圖13 內容自動伸展

可是早晚drawRect:方法會被調用,繪圖將按照drawRect:方法中的代碼被刷新。代碼不會將箭頭繪制在相對於視圖邊界的高度。它是在一個固定的高度。因此箭頭會伸展,而且會在以後某個時間返回到原始的尺寸。

通常我們的視圖的contentMode屬性需要與視圖繪制自己的方式一致。假設我們的drawRect:方法中的代碼讓箭頭的尺寸和位置相對於視圖的邊界原點,即它的左上方。所以我們可以設置它的contentMode為UIViewContentModeTopLeft。又或者,我們可以將contentMode設置為UIVIewContentModeRedraw,這將引起緩存內容的自動縮放和重定位被關閉,最終結果是視圖的setNeedsDisplay方法將被調用,觸發drawRect:方法重繪視圖內容。

在另一方面,如果一個視圖隻是暫時被調整大小。假設是作為動畫的一部分,那麼伸縮行為正是你所想要的。假設我們的動畫是想要讓視圖變大然後還原回原始大小以達到作為吸引用戶的一種手段。這就需要視圖伸縮的時候視圖的內容也跟著伸縮,正確的contentMode的值是UIViewContentModeScaleToFill,被伸縮的內容僅僅是視圖內容的一副緩存圖片,所以它運行起來十分的高效。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *