本文是《Programming iOS5》中Drawing一章的翻譯,考慮到主題完整性,翻譯版本中加入了一些書中未涉及到的內(nèi)容。希望本文能夠?qū)δ阌兴鶐椭?。(本文由海水的味道翻譯整理,轉(zhuǎn)載請注明譯者和出處,請勿用于商業(yè)用途!原文)
Core Graphics Framework是一套基于C的API框架,使用了Quartz作為繪圖引擎。它提供了低級別、輕量級、高保真度的2D渲染。該框架可以用于基于路徑的繪圖、變換、顏色管理、脫屏渲染,模板、漸變、遮蔽、圖像數(shù)據(jù)管理、圖像的創(chuàng)建、遮罩以及PDF文檔的創(chuàng)建、顯示和分析。為了從感官上對這些概念做一個入門的認(rèn)識,你可以運(yùn)行一下官方的example code。
iOS支持兩套圖形API族:Core Graphics/QuartZ 2D 和OpenGL ES。OpenGL ES是跨平臺的圖形API,屬于OpenGL的一個簡化版本。QuartZ 2D是蘋果公司開發(fā)的一套API,它是Core Graphics Framework的一部分。需要注意的是:OpenGL ES是應(yīng)用程序編程接口,該接口描述了方法、結(jié)構(gòu)、函數(shù)應(yīng)具有的行為以及應(yīng)該如何被使用的語義。也就是說它只定義了一套規(guī)范,具體的實現(xiàn)由設(shè)備制造商根據(jù)規(guī)范去做。而往往很多人對接口和實現(xiàn)存在誤解。舉一個不恰當(dāng)?shù)谋扔鳎荷习l(fā)條的時鐘和裝電池的時鐘都有相同的可視行為,但兩者的內(nèi)部實現(xiàn)截然不同。因為制造商可以自由的實現(xiàn)Open GL ES,所以不同系統(tǒng)實現(xiàn)的OpenGL ES也存在著巨大的性能差異。
Core Graphics API所有的操作都在一個上下文中進(jìn)行。所以在繪圖之前需要獲取該上下文并傳入執(zhí)行渲染的函數(shù)中。如果你正在渲染一副在內(nèi)存中的圖片,此時就需要傳入圖片所屬的上下文。獲得一個圖形上下文是我們完成繪圖任務(wù)的第一步,你可以將圖形上下文理解為一塊畫布。如果你沒有得到這塊畫布,那么你就無法完成任何繪圖操作。當(dāng)然,有許多方式獲得一個圖形上下文,這里我介紹兩種最為常用的獲取方法。
第一種方法就是創(chuàng)建一個圖片類型的上下文。調(diào)用UIGraphicsBeginImageContextWithOptions函數(shù)就可獲得用來處理圖片的圖形上下文。利用該上下文,你就可以在其上進(jìn)行繪圖,并生成圖片。調(diào)用UIGraphicsGetImageFromCurrentImageContext函數(shù)可從當(dāng)前上下文中獲取一個UIImage對象。記住在你所有的繪圖操作后別忘了調(diào)用UIGraphicsEndImageContext函數(shù)關(guān)閉圖形上下文。
第二種方法是利用cocoa為你生成的圖形上下文。當(dāng)你子類化了一個UIView并實現(xiàn)了自己的drawRect:方法后,一旦drawRect:方法被調(diào)用,Cocoa就會為你創(chuàng)建一個圖形上下文,此時你對圖形上下文的所有繪圖操作都會顯示在UIView上。
判斷一個上下文是否為當(dāng)前圖形上下文需要注意的幾點:
1.UIGraphicsBeginImageContextWithOptions函數(shù)不僅僅是創(chuàng)建了一個適用于圖形操作的上下文,并且該上下文也屬于當(dāng)前上下文。
2.當(dāng)drawRect方法被調(diào)用時,UIView的繪圖上下文屬于當(dāng)前圖形上下文。
3.回調(diào)方法所持有的context:參數(shù)并不會讓任何上下文成為當(dāng)前圖形上下文。此參數(shù)僅僅是對一個圖形上下文的引用罷了。
作為初學(xué)者,很容易被UIKit和Core Graphics兩個支持繪圖的框架迷惑。
UIKit
像UIImage、NSString(繪制文本)、UIBezierPath(繪制形狀)、UIColor都知道如何繪制自己。這些類提供了功能有限但使用方便的方法來讓我們完成繪圖任務(wù)。一般情況下,UIKit就是我們所需要的。
使用UiKit,你只能在當(dāng)前上下文中繪圖,所以如果你當(dāng)前處于UIGraphicsBeginImageContextWithOptions函數(shù)或drawRect:方法中,你就可以直接使用UIKit提供的方法進(jìn)行繪圖。如果你持有一個context:參數(shù),那么使用UIKit提供的方法之前,必須將該上下文參數(shù)轉(zhuǎn)化為當(dāng)前上下文。幸運(yùn)的是,調(diào)用UIGraphicsPushContext 函數(shù)可以方便的將context:參數(shù)轉(zhuǎn)化為當(dāng)前上下文,記住最后別忘了調(diào)用UIGraphicsPopContext函數(shù)恢復(fù)上下文環(huán)境。
Core Graphics
這是一個繪圖專用的API族,它經(jīng)常被稱為QuartZ或QuartZ 2D。Core Graphics是iOS上所有繪圖功能的基石,包括UIKit。
使用Core Graphics之前需要指定一個用于繪圖的圖形上下文(CGContextRef),這個圖形上下文會在每個繪圖函數(shù)中都會被用到。如果你持有一個圖形上下文context:參數(shù),那么你等同于有了一個圖形上下文,這個上下文也許就是你需要用來繪圖的那個。如果你當(dāng)前處于UIGraphicsBeginImageContextWithOptions函數(shù)或drawRect:方法中,并沒有引用一個上下文。為了使用Core Graphics,你可以調(diào)用UIGraphicsGetCurrentContext函數(shù)獲得當(dāng)前的圖形上下文。
至此,我們有了兩大繪圖框架的支持以及三種獲得圖形上下文的方法(drawRect:、drawRect: inContext:、UIGraphicsBeginImageContextWithOptions)。那么我們就有6種繪圖的形式。如果你有些困惑了,不用怕,我接下來將說明這6種情況。無需擔(dān)心還沒有具體的繪圖命令,你只需關(guān)注上下文如何被創(chuàng)建以及我們是在使用UIKit還是Core Graphics。
第一種繪圖形式:在UIView的子類方法drawRect:中繪制一個藍(lán)色圓,使用UIKit在Cocoa為我們提供的當(dāng)前上下文中完成繪圖任務(wù)。
第二種繪圖形式:使用Core Graphics實現(xiàn)繪制藍(lán)色圓。
第三種繪圖形式:我將在UIView子類的drawLayer:inContext:方法中實現(xiàn)繪圖任務(wù)。drawLayer:inContext:方法是一個繪制圖層內(nèi)容的代理方法。為了能夠調(diào)用drawLayer:inContext:方法,我們需要設(shè)定圖層的代理對象。但要注意,不應(yīng)該將UIView對象設(shè)置為顯示層的委托對象,這是因為UIView對象已經(jīng)是隱式層的代理對象,再將它設(shè)置為另一個層的委托對象就會出問題。輕量級的做法是:編寫負(fù)責(zé)繪圖形的代理類。在MyView.h文件中聲明如下代碼:
然后MyView.m文件中實現(xiàn)接口代碼:
直接將代理類的實現(xiàn)代碼放在MyView.m文件的#import代碼的下面,這樣感覺好像在使用私有類完成繪圖任務(wù)(雖然這不是私有類)。需要注意的是,我們所引用的上下文并不是當(dāng)前上下文,所以為了能夠使用UIKit,我們需要將引用的上下文轉(zhuǎn)變成當(dāng)前上下文。
因為圖層的代理是assign內(nèi)存管理策略,那么這里就不能以局部變量的形式創(chuàng)建MyLayerDelegate實例對象賦值給圖層代理。這里選擇在MyView.m中增加一個實例變量,因為實例變量默認(rèn)是strong:
使用該圖層代理:
第四種繪圖形式: 使用Core Graphics在drawLayer:inContext:方法中實現(xiàn)同樣操作,代碼如下:
最后,演示UIGraphicsBeginImageContextWithOptions的用法,并從上下文中生成一個UIImage對象。生成UIImage對象的代碼并不需要等待某些方法被調(diào)用后或在UIView的子類中才能去做。
第五種繪圖形式: 使用UIKit實現(xiàn):
解釋一下UIGraphicsBeginImageContextWithOptions函數(shù)參數(shù)的含義:第一個參數(shù)表示所要創(chuàng)建的圖片的尺寸;第二個參數(shù)用來指定所生成圖片的背景是否為不透明,如上我們使用YES而不是NO,則我們得到的圖片背景將會是黑色,顯然這不是我想要的;第三個參數(shù)指定生成圖片的縮放因子,這個縮放因子與UIImage的scale屬性所指的含義是一致的。傳入0則表示讓圖片的縮放因子根據(jù)屏幕的分辨率而變化,所以我們得到的圖片不管是在單分辨率還是視網(wǎng)膜屏上看起來都會很好。
第六種繪圖形式: 使用Core Graphics實現(xiàn):
UIKit和Core Graphics可以在相同的圖形上下文中混合使用。在iOS 4.0之前,使用UIKit和UIGraphicsGetCurrentContext被認(rèn)為是線程不安全的。而在iOS4.0以后蘋果讓繪圖操作在第二個線程中執(zhí)行解決了此問題。
UIImage常用的繪圖操作
一個UIImage對象提供了向當(dāng)前上下文繪制自身的方法。我們現(xiàn)在已經(jīng)知道如何獲取一個圖片類型的上下文并將它轉(zhuǎn)變成當(dāng)前上下文。
平移操作:下面的代碼展示了如何將UIImage繪制在當(dāng)前的上下文中。
圖1 UIImage平移處理
縮放操作:下面代碼展示了如何對UIImage進(jìn)行縮放操作:
圖2 UIImage縮放處理
UIImage沒有提供截取圖片指定區(qū)域的功能。但通過創(chuàng)建一個較小的圖形上下文并移動圖片到一個適當(dāng)?shù)膱D形上下文坐標(biāo)系內(nèi),指定區(qū)域內(nèi)的圖片就會被獲取。
裁剪操作:下面代碼展示了如何獲取圖片的右半邊:
以上的代碼首先創(chuàng)建一個一半圖片寬度的圖形上下文,然后將圖片左上角原點移動到與圖形上下文負(fù)X坐標(biāo)對齊,從而讓圖片只有右半部分與圖形上下文相交。
圖3 UIImage裁剪原理
CGImage常用的繪圖操作
UIImage的Core Graphics版本是CGImage(具體類型是CGImageRef)。兩者可以直接相互轉(zhuǎn)化: 使用UIImage的CGImage屬性可以訪問Quartz圖片數(shù)據(jù);將CGImage作為UIImage方法imageWithCGImage:或initWithCGImage:的參數(shù)創(chuàng)建UIImage對象。
一個CGImage對象可以讓你獲取原始圖片中指定區(qū)域的圖片(也可以獲取指定區(qū)域外的圖片,UIImage卻辦不到)。
下面的代碼展示了將圖片拆分成兩半,并分別繪制在上下文的左右兩邊:
你也許發(fā)現(xiàn)繪出的圖是上下顛倒的!圖片的顛倒并不是因為被旋轉(zhuǎn)了。當(dāng)你創(chuàng)建了一個CGImage并使用CGContextDrawImage方法繪圖就會引起這種問題。這主要是因為原始的本地坐標(biāo)系統(tǒng)(坐標(biāo)原點在左上角)與目標(biāo)上下文(坐標(biāo)原點在左下角)不匹配。有很多方法可以修復(fù)這個問題,其中一種方法就是使用CGContextDrawImage方法先將CGImage繪制到UIImage上,然后獲取UIImage對應(yīng)的CGImage,此時就得到了一個倒轉(zhuǎn)的CGImage。當(dāng)再調(diào)用CGContextDrawImage方法,我們就將倒轉(zhuǎn)的圖片還原回來了。實現(xiàn)代碼如下:
現(xiàn)在將之前的代碼修改如下:
然而,這里又出現(xiàn)了另外一個問題:在雙分辨率的設(shè)備上,如果我們的圖片文件是高分辨率(@2x)版本,上面的繪圖就是錯誤的。原因在于對于UIImage來說,在加載原始圖片時使用imageNamed:方法,它會自動根據(jù)所在設(shè)備的分辨率類型選擇圖片,并且UIImage通過設(shè)置用來適配的scale屬性補(bǔ)償圖片的兩倍尺寸。但是一個CGImage對象并沒有scale屬性,它不知道圖片文件的尺寸是否為兩倍!所以當(dāng)調(diào)用UIImage的CGImage方法,你不能假定所獲得的CGImage尺寸與原始UIImage是一樣的。在單分辨率和雙分辨率下,一個UIImage對象的size屬性值都是一樣的,但是雙分辨率UIImage對應(yīng)的CGImage是單分辨率UIImage對應(yīng)的CGImage的兩倍大。所以我們需要修改上面的代碼,讓其在單雙分辨率下都可以工作。代碼如下:
上面的代碼初看上去很繁雜,不過不用擔(dān)心,這里還有另一種修復(fù)倒置問題的方案。相對于使用flip函數(shù),你可以在繪圖之前將CGImage包裝進(jìn)UIImage中,這樣做有兩大優(yōu)點:
1.當(dāng)UIImage繪圖時它會自動修復(fù)倒置問題
2.當(dāng)你從CGImage轉(zhuǎn)化為Uimage時,可調(diào)用imageWithCGImage:scale:orientation:方法生成CGImage作為對縮放性的補(bǔ)償。
所以這是一個解決倒置和縮放問題的自包含方法。
代碼如下:
還有另一種解決倒置問題的方案是在繪制CGImage之前,對上下文應(yīng)用變換操作,有效地倒置上下文的內(nèi)部坐標(biāo)系統(tǒng)。這里先不做討論。
為什么會發(fā)生倒置問題
究其原因是因為Core Graphics源于Mac OS X系統(tǒng),在Mac OS X中,坐標(biāo)原點在左下方并且正y坐標(biāo)是朝上的,而在iOS中,原點坐標(biāo)是在左上方并且正y坐標(biāo)是朝下的。在大多數(shù)情況下,這不會出現(xiàn)任何問題,因為圖形上下文的坐標(biāo)系統(tǒng)是會自動調(diào)節(jié)補(bǔ)償?shù)摹5莿?chuàng)建和繪制一個CGImage對象時就會暴露出倒置問題。
CIFilter與CIImage
CIFilter與CIImage是iOS 5新引入的,雖然它們已在MAX OS X系統(tǒng)中存在多年。前綴“CI”表示Core Image,這是一種使用數(shù)學(xué)濾鏡變換圖片的技術(shù)。但是你不要去幻想iOS提供了像Photoshop軟件那樣強(qiáng)大的濾鏡功能。使用Core Image之前你需要將CoreImage.framework框架導(dǎo)入到你的target之中。
所謂濾鏡指的是CIFilter類,濾鏡可被分為以下幾類:
模板與漸變類
這兩類濾鏡創(chuàng)建的CIImage可以和其他的CIImage進(jìn)行合并,比如一種單色,一個棋盤,條紋,亦或是漸變。
合成類
此類濾鏡可以將一張圖片與另外的圖片合并,合成濾鏡模式常見于圖形處理軟件Photoshop中。
色彩類
此濾鏡調(diào)整、修改圖片的色彩。因此你可以改變一張圖片的飽和度、色度、亮度、對比度、伽馬、白點、曝光度、陰影、高亮等屬性。
幾何變換類
此類濾鏡可對圖片執(zhí)行基本的幾何變換,比如縮放、旋轉(zhuǎn)、裁剪。
CIFilter使用起來非常的簡單。CIFilter看上去就像一個由鍵值組成的字典。它生成一個CIImage對象作為其輸出。一般地,一個濾鏡有一個或多個輸入,而對于部分濾鏡,生成的圖片是基于其他類型的參數(shù)值。CIFilter對象是一個集合,可使用鍵值對進(jìn)行檢索。通過提供濾鏡的字符串名稱創(chuàng)建一個濾鏡,如果想知道有哪些濾鏡,可以查詢蘋果的Core Image Filter Reference文檔,或是調(diào)用CIFilter的類方法filterNamesInCategories:,參數(shù)值為nil。每一個濾鏡擁有一小部分用來確定其行為的鍵值。如果你想修改某一個鍵(比如亮度鍵)對應(yīng)的值,你可以調(diào)用setValue:forKey:方法或當(dāng)你指定一個濾鏡名時提供所有鍵值對。
需要處理的圖片必須是CIImage類型,調(diào)用initWithCGImage:方法可獲得CIImage。因為CGImage又是作為濾鏡的輸出,因此濾鏡之間可被連接在一起(將濾鏡的輸出作為initWithCGImage:方法的輸入?yún)?shù))
當(dāng)你構(gòu)建一個濾鏡鏈時,并沒有做復(fù)雜的運(yùn)算。只有當(dāng)整個濾鏡鏈需要輸出一個CGImage時,密集型計算才會發(fā)生。調(diào)用contextWithOptions:和createCGImage: fromRect:方法創(chuàng)建CIContext。與以往不同的地方是CIImage沒有frame與bounds屬性;只有extent屬性。你將非常頻繁的使用這個屬性作為createCGImage: fromRect:方法的第二個參數(shù)。
接下來我將演示Core Image的使用。首先創(chuàng)建一個徑向漸變的濾鏡,該濾鏡是從白到黑的漸變方式,白色區(qū)域的半徑默認(rèn)是100。接著將其與一張使用CIDarkenBlendMode濾鏡的圖片合成。CIDarkenBlendMode的作用是背景圖片樣本將被源圖片的黑色部分替換掉。
代碼如下:
圖4 圖片合成快照
這個例子可能沒有什么吸引人的地方,因為所有一切都可以使用Core Graphics完成。除了Core Image是使用GPU處理,可能有點吸引人。Core Graphics也可以做到徑向漸變并使用混合模式合成圖片。但Core Image要簡單得多,特別是當(dāng)你有多個圖片輸入想重用一個濾鏡鏈時。并且Core Image的顏色調(diào)整功能比Core Graphics更加強(qiáng)大。對了,Core Image還能實現(xiàn)自動人臉識別哦!
繪制一個UIView
繪制一個UIVIew最靈活的方式就是由它自己完成繪制。實際上你不是繪制一個UIView,你只是子類化了UIView并賦予子類繪制自己的能力。當(dāng)一個UIVIew需要執(zhí)行繪圖操作的時, drawRect:方法就會被調(diào)用。覆蓋此方法讓你獲得繪圖操作的機(jī)會。當(dāng)drawRect:方法被調(diào)用,當(dāng)前圖形上下文也被設(shè)置為屬于視圖的圖形上下文。你可以使用Core Graphics或UIKit提供的方法將圖形畫到該上下文中。
你不應(yīng)該手動調(diào)用drawRect:方法!如果你想調(diào)用drawRect:方法更新視圖,只需發(fā)送setNeedsDisplay方法。這將使得drawRect:方法會在下一個適當(dāng)?shù)臅r間調(diào)用。當(dāng)然,不要覆蓋drawRect:方法除非你知道這樣做絕對合法。比方說,在UIImageView子類中覆蓋drawRect:方法是不合法的,你將得不到你繪制的圖形。
在UIView子類的drawRect:方法中無需調(diào)用super,因為本身UIView的drawRect:方法是空的。為了提高一些繪圖性能,你可以調(diào)用setNeedsDisplayInRect方法重新繪制視圖的子區(qū)域,而視圖的其他部分依然保持不變。
一般情況下,你不應(yīng)該過早的進(jìn)行優(yōu)化。繪圖代碼可能看上去非常的繁瑣,但它們是非??斓?。并且iOS繪圖系統(tǒng)自身也是非常高效,它不會頻繁調(diào)用drawRect:方法,除非迫不得已(或調(diào)用了setNeedsDisplay方法)。一旦一個視圖已由自己繪制完成,那么繪制的結(jié)果會被緩存下來留待重用,而不是每次重頭再來。(蘋果公司將緩存繪圖稱為視圖的位圖存儲回填(bitmap backing store))。你可能會發(fā)現(xiàn)drawRect:方法中的代碼在整個應(yīng)用程序生命周期內(nèi)只被調(diào)用了一次!事實上,將代碼移到drawRect:方法中是提高性能的普遍做法。這是因為繪圖引擎直接對屏幕進(jìn)行渲染相對于先是脫屏渲染然后再將像素拷貝到屏幕要來的高效。
當(dāng)視圖的backgroundColor為nil并且opaque屬性為YES,視圖的背景顏色就會變成黑色。
Core Graphics上下文屬性設(shè)置
當(dāng)你在圖形上下文中繪圖時,當(dāng)前圖形上下文的相關(guān)屬性設(shè)置將決定繪圖的行為與外觀。因此,繪圖的一般過程是先設(shè)定好圖形上下文參數(shù),然后繪圖。比方說,要畫一根紅線,接著畫一根藍(lán)線。那么首先需要將上下文的線條顏色屬性設(shè)定為為紅色,然后畫紅線;接著設(shè)置上下文的線條顏色屬性為藍(lán)色,再畫出藍(lán)線。表面上看,紅線和藍(lán)線是分開的,但事實上,在你畫每一條線時,線條顏色卻是整個上下文的屬性。無論你用的是UIKit方法還是Core Graphics函數(shù)。
因為圖形上下文在每一時刻都有一個確定的狀態(tài),該狀態(tài)概括了圖形上下文所有屬性的設(shè)置。為了便于操作這些狀態(tài),圖形上下文提供了一個用來持有狀態(tài)的棧。調(diào)用CGContextSaveGState函數(shù),上下文會將完整的當(dāng)前狀態(tài)壓入棧頂;調(diào)用CGContextRestoreGState函數(shù),上下文查找處在棧頂?shù)臓顟B(tài),并設(shè)置當(dāng)前上下文狀態(tài)為棧頂狀態(tài)。
因此一般繪圖模式是:在繪圖之前調(diào)用CGContextSaveGState函數(shù)保存當(dāng)前狀態(tài),接著根據(jù)需要設(shè)置某些上下文狀態(tài),然后繪圖,最后調(diào)用CGContextRestoreGState函數(shù)將當(dāng)前狀態(tài)恢復(fù)到繪圖之前的狀態(tài)。要注意的是,CGContextSaveGState函數(shù)和CGContextRestoreGState函數(shù)必須成對出現(xiàn),否則繪圖很可能出現(xiàn)意想不到的錯誤,這里有一個簡單的做法避免這種情況。代碼如下:
但你不需要在每次修改上下文狀態(tài)之前都這樣做,因為你對某一上下文屬性的設(shè)置并不一定會和之前的屬性設(shè)置或其他的屬性設(shè)置產(chǎn)生沖突。你完全可以在不調(diào)用保存和恢復(fù)函數(shù)的情況下先設(shè)置線條顏色為紅色,然后再設(shè)置為藍(lán)色。但在一定情況下,你希望你對狀態(tài)的設(shè)置是可撤銷的,我將在接下來討論這樣的情況。
許多的屬性組成了一個圖形上下文狀態(tài),這些屬性設(shè)置決定了在你繪圖時圖形的外觀和行為。下面我列出了一些屬性和對應(yīng)修改屬性的函數(shù);雖然這些函數(shù)是關(guān)于Core Graphics的,但記住,實際上UIKit同樣是調(diào)用這些函數(shù)操縱上下文狀態(tài)。
線條的寬度和線條的虛線樣式
CGContextSetLineWidth、CGContextSetLineDash
線帽和線條聯(lián)接點樣式
CGContextSetLineCap、CGContextSetLineJoin、CGContextSetMiterLimit
線條顏色和線條模式
CGContextSetRGBStrokeColor、CGContextSetGrayStrokeColor、CGContextSetStrokeColorWithColor、CGContextSetStrokePattern
填充顏色和模式
CGContextSetRGBFillColor,CGContextSetGrayFillColor,CGContextSetFillColorWithColor, CGContextSetFillPattern
陰影
CGContextSetShadow、CGContextSetShadowWithColor
混合模式
CGContextSetBlendMode(決定你當(dāng)前繪制的圖形與已經(jīng)存在的圖形如何被合成)
整體透明度
CGContextSetAlpha(個別顏色也具有alpha成分)
文本屬性
CGContextSelectFont、CGContextSetFont、CGContextSetFontSize、CGContextSetTextDrawingMode、CGContextSetCharacterSpacing
是否開啟反鋸齒和字體平滑
CGContextSetShouldAntialias、CGContextSetShouldSmoothFonts
另外一些屬性設(shè)置:
裁剪區(qū)域:在裁剪區(qū)域外繪圖不會被實際的畫出來。
變換(或稱為“CTM“,意為當(dāng)前變換矩陣): 改變你隨后指定的繪圖命令中的點如何被映射到畫布的物理空間。
許多這些屬性設(shè)置接下來我都會舉例說明。
路徑與繪圖
通過編寫移動虛擬畫筆的代碼描畫一段路徑,這樣的路徑并不構(gòu)成一個圖形。繪制路徑意味著對路徑描邊或填充該路徑,也或者兩者都做。同樣,你應(yīng)該從某些繪圖程序中得到過相似的體會。
一段路徑是由點到點的描畫構(gòu)成。想象一下繪圖系統(tǒng)是你手里的一只畫筆,你首先必須要設(shè)置畫筆當(dāng)前所處的位置,然后給出一系列命令告訴畫筆如何描畫隨后的每段路徑。每一段新增的路徑開始于當(dāng)前點,當(dāng)完成一條路徑的描畫,路徑的終點就變成了當(dāng)前點。
下面列出了一些路徑描畫的命令:
定位當(dāng)前點
CGContextMoveToPoint
描畫一條線
CGContextAddLineToPoint、CGContextAddLines
描畫一個矩形
CGContextAddRect、CGContextAddRects
描畫一個橢圓或圓形
CGContextAddEllipseInRect
描畫一段圓弧
CGContextAddArcToPoint、CGContextAddArc
通過一到兩個控制點描畫一段貝賽爾曲線
CGContextAddQuadCurveToPoint、CGContextAddCurveToPoint
關(guān)閉當(dāng)前路徑
CGContextClosePath 這將從路徑的終點到起點追加一條線。如果你打算填充一段路徑,那么就不需要使用該命令,因為該命令會被自動調(diào)用。
描邊或填充當(dāng)前路徑
CGContextStrokePath、CGContextFillPath、CGContextEOFillPath、CGContextDrawPath。對當(dāng)前路徑描邊或填充會清除掉路徑。如果你只想使用一條命令完成描邊和填充任務(wù),可以使用CGContextDrawPath命令,因為如果你只是使用CGContextStrokePath對路徑描邊,路徑就會被清除掉,你就不能再對它進(jìn)行填充了。
創(chuàng)建路徑并描邊路徑或填充路徑只需一條命令就可完成的函數(shù):CGContextStrokeLineSegments、CGContextStrokeRect、CGContextStrokeRectWithWidth、CGContextFillRect、CGContextFillRects、CGContextStrokeEllipseInRect、CGContextFillEllipseInRect。
一段路徑是被合成的,意思是它是由多條獨(dú)立的路徑組成。舉個例子,一條單獨(dú)的路徑可能由兩個獨(dú)立的閉合形狀組成:一個矩形和一個圓形。當(dāng)你在構(gòu)造一條路徑的中間過程(意思是在描畫了一條路徑后沒有調(diào)用描邊或填充命令,或調(diào)用CGContextBeginPath函數(shù)來清除路徑)調(diào)用CGContextMoveToPoint函數(shù),就像是你拾起畫筆,并將畫筆移動到一個新的位置,如此來準(zhǔn)備開始一段獨(dú)立的相同路徑。如果你擔(dān)心當(dāng)你開始描畫一條路徑的時候,已經(jīng)存在的路徑和新的路徑會被認(rèn)為是已存在路徑的一個合成部分,你可以調(diào)用CGContextBeginPath函數(shù)指定你繪制的路徑是一條獨(dú)立的路徑;蘋果的許多例子都是這樣做的,但在實際開發(fā)中我發(fā)現(xiàn)這是非必要的。
CGContextClearRect函數(shù)的功能是擦除一個區(qū)域。這個函數(shù)會擦除一個矩形內(nèi)的所有已存在的繪圖;并對該區(qū)域執(zhí)行裁剪。結(jié)果像是打了一個貫穿所有已存在繪圖的孔。
CGContextClearRect函數(shù)的行為依賴于上下文是透明還是不透明。當(dāng)在圖形上下文中繪圖時,這會尤為明顯和直觀。如果圖片上下文是透明的(UIGraphicsBeginImageContextWithOptions第二個參數(shù)為NO),那么CGContextClearRect函數(shù)執(zhí)行擦除后的顏色為透明,反之則為黑色。
當(dāng)在一個視圖中直接繪圖(使用drawRect:或drawLayer:inContext:方法),如果視圖的背景顏色為nil或顏色哪怕有一點點透明度,那么CGContextClearRect的矩形區(qū)域?qū)@示為透明的,打出的孔將穿過視圖包括它的背景顏色。如果背景顏色完全不透明,那么CGContextClearRect函數(shù)的結(jié)果將會是黑色。這是因為視圖的背景顏色決定了是否視圖的圖形上下文是透明的還是不透明的。
圖5 CGContextClearRect函數(shù)的應(yīng)用
如圖5,在左邊的藍(lán)色正方形被挖去部分留為黑色,然而在右邊的藍(lán)色正方形也被挖去部分留為透明。但這兩個正方形都是UIView子類的實例,采用相同的繪圖代碼!不同之處在于視圖的背景顏色,左邊的正方形的背景顏色在nib文件中
但是這卻完全改變了CGContextClearRect函數(shù)的效果。UIView子類的drawRect:方法看起來像這樣:
為了說明典型路徑的描畫命令,我將生成一個向上的箭頭圖案,我謹(jǐn)慎避免使用便利函數(shù)操作,也許這不是創(chuàng)建箭頭最好的方式,但依然清楚的展示了各種典型命令的用法。
圖6 一個簡單的路徑繪圖
確切的說,為了以防萬一,我們應(yīng)該在繪圖代碼周圍使用CGContextSaveGState和CGContextRestoreGState函數(shù)??蓪τ谶@個例子來說,添加與否不會有任何的區(qū)別。因為上下文在調(diào)用drawRect:方法中不會被持久,所以不會被破壞。
如果一段路徑需要重用或共享,你可以將路徑封裝為CGPath(具體類型是CGPathRef)。你可以創(chuàng)建一個新的CGMutablePathRef對象并使用多個類似于圖形的路徑函數(shù)的CGPath函數(shù)構(gòu)造路徑,或者使用CGContextCopyPath函數(shù)復(fù)制圖形上下文的當(dāng)前路徑。有許多CGPath函數(shù)可用于創(chuàng)建基于簡單幾何形狀的路徑(CGPathCreateWithRect、CGPathCreateWithEllipseInRect)或基于已存在路徑(CGPathCreateCopyByStrokingPath、CGPathCreateCopyDashingPath、CGPathCreateCopyByTransformingPath)。
UIKit的UIBezierPath類包裝了CGPath。它提供了用于繪制某種形狀路徑的方法,以及用于描邊、填充、存取某些當(dāng)前上下文狀態(tài)的設(shè)置方法。類似地,UIColor提供了用于設(shè)置當(dāng)前上下文描邊與填充的顏色。因此我們可以重寫我們之前繪制箭頭的代碼:
在這種特殊情況下,完成同樣的工作并沒有節(jié)省多少代碼,但是UIBezierPath仍然還是有用的。如果你需要對象特性,UIBezierPath提供了一個便利方法:bezierPathWithRoundedRect:cornerRadius:,它可用于繪制帶有圓角的矩形,如果是使用Core Graphics就相當(dāng)冗長乏味了。還可以只讓圓角出現(xiàn)在左上角和右上角。
圖7 左右圓角矩形
裁剪
路徑的另一用處是遮蔽區(qū)域,以防對遮蔽區(qū)域進(jìn)一步繪圖。這種用法被稱為裁剪。裁剪區(qū)域外的圖形不會被繪制到。默認(rèn)情況下,一個圖形上下文的裁剪區(qū)域是整個圖形上下文。你可在上下文中的任何地方繪圖。
總的來說,裁剪區(qū)域是上下文的一個特性。與已存在的裁剪區(qū)域相交會出現(xiàn)新的裁剪區(qū)域。所以如果你應(yīng)用了你自己的裁剪區(qū)域,稍后將它從圖形上下文中移除的做法是使用CGContextSaveGState和CGContextRestoreGState函數(shù)將代碼包裝起來。
為了便于說明這一點,我使用裁剪而不是使用混合模式在箭頭桿子上打孔的方法重寫了生成箭頭的代碼。這樣做有點小復(fù)雜,因為我們想要裁剪區(qū)域不在三角形內(nèi)而在三角形外部。為了表明這一點,我們使用了一個三角形和一個矩形組成了一個組合路徑。
當(dāng)填充一個組合路徑并使用它表示一個裁剪區(qū)域時,系統(tǒng)遵循以下兩規(guī)則之一:
環(huán)繞規(guī)則(Winding rule)
如果邊界是順時針繪制,那么在其內(nèi)部逆時針繪制的邊界所包含的內(nèi)容為空。如果邊界是逆時針繪制,那么在其內(nèi)部順時針繪制的邊界所包含的內(nèi)容為空。
奇偶規(guī)則
最外層的邊界代表內(nèi)部都有效,都要填充;之后向內(nèi)第二個邊界代表它的內(nèi)部無效,不需填充;如此規(guī)則繼續(xù)向內(nèi)尋找邊界線。我們的情況非常簡單,所以使用奇偶規(guī)則就很容易了。這里我們使用CGContextEOCllip設(shè)置裁剪區(qū)域然后進(jìn)行繪圖。(如果不是很明白,可以參見這篇文章:五種方法繪制有孔的2d形狀)
漸變
漸變可以很簡單也可以很復(fù)雜。一個簡單的漸變(接下來要討論的)由一端點的顏色與另一端點的顏色決定,如果在中間點加入顏色(可選),那么漸變會在上下文的兩個點之間線性的繪制或在上下文的兩個圓之間放射狀的繪制。不能使用漸變作為路徑的填充色,但可使用裁剪限制對路徑形狀的漸變。
我重寫了繪制箭頭的代碼,箭桿使用了線性漸變。效果如圖7所示。
圖8 箭頭桿子漸變
調(diào)用CGContextReplacePathWithStrokedPath函數(shù)假裝對當(dāng)前路徑描邊,并使用當(dāng)前線段寬度和與線段相關(guān)的上下文狀態(tài)設(shè)置。但接著創(chuàng)建的是描邊路徑外部的一個新的路徑。因此,相對于使用粗的線條,我們使用了一個矩形區(qū)域作為裁剪區(qū)域。
雖然過程比較冗長但是非常的簡單;我們將漸變描述為一組在一端點(0.0)和另一端點(1.0)之間連續(xù)區(qū)上的位置,以及設(shè)置與每個位置相對應(yīng)的顏色。為了提亮邊緣的漸變,加深中間的漸變,我使用了三個位置,黑色點的位置是0.5。為了創(chuàng)建漸變,還需要提供一個顏色空間。最后,我創(chuàng)建出了該漸變,并對裁剪區(qū)域繪制線性漸變,最后釋放了顏色空間和漸變。
顏色與模板
在iOS中,CGColor表示顏色(具體類型為CGColorRef)。使用UIColor的colorWithCGColor:和CGColor方法可bridged cast到UIColor。
在iOS中,模板表示為CGPattern(具體類型為CGPatternRef)。你可以創(chuàng)建一個模板并使用它進(jìn)行描邊或填充。其過程是相當(dāng)復(fù)雜的。作為一個非常簡單的例子,我將使用紅藍(lán)相間的三角形替換箭頭的三角形部分。現(xiàn)在移除下面行:
CGContextSetFillColorWithColor(con, [UIColor redColor].CGColor));
在被移除的地方填入下面代碼:
代碼非常冗長,但它卻是一個完整的樣板。現(xiàn)在我們從后往前分析代碼: 我們調(diào)用CGContextSetFillPattern不是設(shè)置填充顏色,我們設(shè)置的是填充的模板。函數(shù)的第三個參數(shù)是一個指向CGFloat的指針,所以我們事先設(shè)置CGFloat自身。第二個參數(shù)是一個CGPatternRef對象,所以我們需要事先創(chuàng)建CGPatternRef,并在最后釋放它。
現(xiàn)在開始討論CGPatternCreate。一個模板是在一個矩形元中的繪圖。我們需要矩形元的尺寸(第二個參數(shù))以及矩形元原始點之間的間隙(第四和第五個參數(shù))。這這種情況下,矩形元是4*4的,每一個矩形元與它的周圍矩形元是緊密貼合的。我們需要提供一個應(yīng)用到矩形元的變換參數(shù)(第三個參數(shù));在這種情況下,我們不需要變換做什么工作,所以我們應(yīng)用了一個恒等變換。我們應(yīng)用了一個瓷磚規(guī)則(第六個參數(shù))。我們需要聲明的是顏色模板不是漏印(stencil)模板,所以參數(shù)值為true。并且我們需要提供一個指向回調(diào)函數(shù)的指針,回調(diào)函數(shù)的工作是向矩形元繪制模板。第八個參數(shù)是一個指向CGPatternCallbacks結(jié)構(gòu)體的指針。這個結(jié)構(gòu)體由數(shù)字0和兩個指向函數(shù)的指針構(gòu)成。第一個函數(shù)指針指向的函數(shù)當(dāng)模板被繪制到矩形元中被調(diào)用,第二個函數(shù)指針指向的函數(shù)當(dāng)模板被釋放后調(diào)用。第二個函數(shù)指針我們沒有指定,它的存在主要是為了內(nèi)存管理的需要。但在這個簡單的例子中,我們并不需要。
在你使用顏色模板調(diào)用CGContextSetFillPattern函數(shù)之前,你需要設(shè)置將應(yīng)用到模板顏色空間的上下文填充顏色空間。如果你忽略這項工作,那么當(dāng)你調(diào)用CGContextSetFillPattern函數(shù)時會發(fā)生錯誤。所以我們創(chuàng)建了顏色空間,設(shè)置它作為上下文的填充顏色空間,并在后面做了釋放。
到這里我們?nèi)匀粵]有完成繪圖。因為我還沒有編寫向矩形元中繪圖的函數(shù)!繪圖函數(shù)地址被表示為&drawStripes。繪圖代碼如下所示:
圖9 模板填充
如你所見,實際的模板繪圖代碼是非常簡單的。唯一的復(fù)雜點在于CGPatternCreate函數(shù)必須與模板繪圖函數(shù)的矩形元尺寸相同。我們知道矩形元的尺寸為4*4,所以我們用紅色填充它,并接著填充它的下半部分為綠色。當(dāng)這些矩形元被水平垂直平鋪時,我們得到了如圖8所示的條紋圖案。
注意,最后圖形上下文遺留下了一個不可取的狀態(tài),即填充顏色空間被設(shè)置為了一個模板顏色空間。如果稍后嘗試設(shè)置填充顏色為常規(guī)顏色,就會引起錯誤。通常的解決方案是,使用CGContextSaveGState和CGContextRestoreGState函數(shù)將代碼包起來。
你可能觀察到圖8的平鋪效果并不與箭頭的三角形內(nèi)部相符合:最底部的似乎只平鋪了一半藍(lán)色。這是因為一個模板的定位并不關(guān)心你填充(描邊)的形狀,總的來說它只關(guān)心圖形上下文。我們可以調(diào)用CGContextSetPatternPhase函數(shù)改變模板的定位。
圖形上下文變換
就像UIView可以實現(xiàn)變換,同樣圖形上下文也具備這項功能。然而對圖形上下文應(yīng)用一個變換操作不會對已在圖形上下文上的繪圖產(chǎn)生什么影響,它只會影響到在上下文變換之后被繪制的圖形,并改變被映射到圖形上下文區(qū)域的坐標(biāo)方式。一個圖形上下文變換被稱為CTM,意為“當(dāng)前變換矩陣“(current transformation matrix)。
完全利用圖形上下文的CTM來免于即使是簡單的計算操作是很常見的。你可以使用CGContextConcatCTM函數(shù)將當(dāng)前變換乘上任何CGAffineTransform,還有一些便利函數(shù)可對當(dāng)前變換應(yīng)用平移、縮放,旋轉(zhuǎn)變換。
當(dāng)你獲得上下文的時候,對圖形上下文的基本變換已經(jīng)設(shè)置好了;這就是系統(tǒng)能映射上下文繪圖坐標(biāo)到屏幕坐標(biāo)的原因。無論你對當(dāng)前變換應(yīng)用了什么變換,基本變換變換依然有效并且繪圖繼續(xù)工作。通過將你的變換代碼封裝到CGContextSaveGState和CGContextRestoreGState函數(shù)調(diào)用中,對基本變換應(yīng)用的變換操作可以被還原。
舉個例子,對于我們迄今為止使用代碼繪制的向上箭頭來說,已知的放置箭頭的方式僅僅只有一個位置:箭頭矩形框的左上角被硬編碼在坐標(biāo){80,0}。這樣代碼很難理解、靈活性差、且很難被重用。最明智的做法是通過將所有代碼中的x坐標(biāo)值減去80,讓箭頭矩形框左上角在坐標(biāo){0,0}。事先應(yīng)用一個簡單的平移變換,很容易將箭頭畫在任何位置。為了映射坐標(biāo)到箭頭的左上角,我們使用下面代碼:
CGContextTranslateCTM(con, 80, 0); //在坐標(biāo){0,0}處繪制箭頭
旋轉(zhuǎn)變換特別的有用,它可以讓你在一個被旋轉(zhuǎn)的方向上進(jìn)行繪制而無需使用任何復(fù)雜的三角函數(shù)。然而這略有點復(fù)雜,因為旋轉(zhuǎn)變換圍繞的點是原點坐標(biāo)。這幾乎不是你所想要的,所以你先是應(yīng)用了一個平移變換,為的是映射原點到你真正想繞其旋轉(zhuǎn)的點。但是接著,在旋轉(zhuǎn)之后,為了算出你在哪里繪圖,你可能需要做一次逆向平移變換。
為了說明這個做法,我將繞箭頭桿子尾部旋轉(zhuǎn)多個角度重復(fù)繪制箭頭,并把對箭頭的繪圖封裝為UIImage對象。接著我們簡單重復(fù)繪制UIImage對象。
具體代碼如下:
圖10 使用CTM旋轉(zhuǎn)變換
變換有多個方法解決我們早期使用CGContextDrawImage函數(shù)遇到的倒置問題。相對于逆向繪圖,我們選擇逆向我們繪圖的上下文。實質(zhì)上,我們對上下文坐標(biāo)系統(tǒng)應(yīng)用了一個“倒置”變換。你自上而下移動上下文,接著你通過應(yīng)用一個讓y坐標(biāo)乘以-1的縮放變換逆向y坐標(biāo)的方向。
上下文的頂部應(yīng)該被你往下移動多遠(yuǎn)依賴于你繪制的圖片。比如說我們可以繪制沒有倒置問題的兩個半邊的火星圖形(前面討論的一個例子)。
陰影
為了在繪圖上加入陰影,可在繪圖之前設(shè)置上下文的陰影值。陰影的位置表示為CGSize,如果CGSize的兩個值都是正數(shù),則表示陰影是朝下和朝右的。模糊度被表示為任何一個正數(shù)。蘋果沒有解釋縮放的工作方式,但實驗表明12是最佳的模糊度,99及以上的模糊度會讓陰影變得不成形。
我在圖9的基礎(chǔ)上給上下文加了一個陰影:
然而,使用這種方法有一個不太明顯的問題。我們是在每繪制一個箭頭的時候加上的陰影。因此,箭頭的陰影會投射在另一個箭頭上面。我們想要的是讓所有的箭頭集體地投射出一個陰影。解決方法是使用一個透明的圖層;該圖層類似一個先是疊加所有繪圖然后加上陰影的一個子上下文。代碼如下:
圖11 陰影效果
點與像素
一個點是由xy坐標(biāo)描述的一個無窮小量的位置。通過指定點實現(xiàn)在圖形上下文中的繪圖。我們并沒有關(guān)心設(shè)備的分辨率,因為Core Graphics已經(jīng)精細(xì)地將繪圖映射到物理輸出設(shè)備(基于CTM、反鋸齒和平滑技術(shù))。因此,文章之前的討論只關(guān)心圖形上下文的點,不關(guān)注點與屏幕像素的關(guān)系。
然而像素是真實存在的。一個像素是真實世界中一個具有完整物理尺寸的顯示單元。整數(shù)的點實際上介于像素之間。在單分辨率設(shè)備上,這可能會讓人感到迷惑。比方說,如果使用線寬為1的線條對一個整數(shù)坐標(biāo)的垂直路徑描邊,那么線條將會被分為兩半,分別落在路徑的兩側(cè)。所以在單分辨率設(shè)備上線寬會變成2px(因為設(shè)備無法表示半個像素)。
圖12 整數(shù)的點坐標(biāo)與偏移0.5點的坐標(biāo)對應(yīng)的描邊處理
當(dāng)你遇到顯示效果不佳的時,可能會被建議通過對坐標(biāo)增減0.5讓它在像素中居中。這個建議可能有效,如圖11。但它只是做了一些頭腦簡單的假設(shè)。一個復(fù)雜的做法是獲得UIView的contentScaleFactor屬性。這個值為1.0或2.0,所以你可以除以這個屬性值得到從像素到點的轉(zhuǎn)換。還可以想想用最精確的方式繪制一條水平或垂直的線條的方式不是描邊路徑,而是填充路徑。使用這種方法UIView的子類代碼將可以在任何設(shè)備上繪制一條完美的1px寬的垂線,代碼如下:
內(nèi)容模式
一個視圖向它自身繪圖,相對于只有背景顏色和子視圖,它還有內(nèi)容。這意味著每當(dāng)視圖被調(diào)整大小它的contentMode屬性就變得非常重要。正如我之前提到的,繪圖系統(tǒng)會盡可能避免重頭開始繪制視圖。相反,繪圖系統(tǒng)將使用之前繪圖操作的緩存結(jié)果(位圖回填)。所以,如果視圖被重新調(diào)整大小,系統(tǒng)可能簡單的伸縮或重定位緩存繪圖,前提是你的contentMode設(shè)置指令是是這樣設(shè)置的。
說明這一點略有點復(fù)雜。因為我需要安排調(diào)整視圖大小而不引起重繪操作(調(diào)用drawRect:方法)。當(dāng)程序啟動時,我將創(chuàng)建一個MyView實例,并將它放在window上。接著將執(zhí)行調(diào)整MyView尺寸的操作延遲到window出現(xiàn)和界面初次顯示之后:
我們將視圖的高度調(diào)成之前的2倍。沒有觸發(fā)drawRect:方法的調(diào)用。如果我們視圖的drawRect:方法代碼和生成圖9的代碼相同,則我們得到如圖12的結(jié)果,視圖被顯示在正確高度上。
圖13 內(nèi)容自動伸展
可是早晚drawRect:方法會被調(diào)用,繪圖將按照drawRect:方法中的代碼被刷新。代碼不會將箭頭繪制在相對于視圖邊界的高度。它是在一個固定的高度。因此箭頭會伸展,而且會在以后某個時間返回到原始的尺寸。
通常我們的視圖的contentMode屬性需要與視圖繪制自己的方式一致。假設(shè)我們的drawRect:方法中的代碼讓箭頭的尺寸和位置相對于視圖的邊界原點,即它的左上方。所以我們可以設(shè)置它的contentMode為UIViewContentModeTopLeft。又或者,我們可以將contentMode設(shè)置為UIVIewContentModeRedraw,這將引起緩存內(nèi)容的自動縮放和重定位被關(guān)閉,最終結(jié)果是視圖的setNeedsDisplay方法將被調(diào)用,觸發(fā)drawRect:方法重繪視圖內(nèi)容。
在另一方面,如果一個視圖只是暫時被調(diào)整大小。假設(shè)是作為動畫的一部分,那么伸縮行為正是你所想要的。假設(shè)我們的動畫是想要讓視圖變大然后還原回原始大小以達(dá)到作為吸引用戶的一種手段。這就需要視圖伸縮的時候視圖的內(nèi)容也跟著伸縮,正確的contentMode的值是UIViewContentModeScaleToFill,被伸縮的內(nèi)容僅僅是視圖內(nèi)容的一副緩存圖片,所以它運(yùn)行起來十分的高效。
完。
---------------------------------------------------------------------------------
譯者說明:譯文中的錯誤或不當(dāng)之處望不吝指出。
Drop me a line: xdreamarshal@gmail.com, http://weibo.com/xdream86
|
|