本文翻譯自:《iOS 7編程》 Matt Neuburg 著,OREILLY出版。

很多UIView的子類(lèi),例如UIButton或者UIlabel,都知道如何繪制自己;不過(guò)遲早,你都會(huì)想繪制一些自己想要的效果。你可以通過(guò)一些已有的類(lèi)在代碼中繪制一幅圖片,然后在自己的界面上展示出來(lái),例如UIImageVIew和UIButton。單純一個(gè)UIView就是只與繪制有關(guān),它給你了很大的空間來(lái)繪畫(huà);你的代碼決定了這個(gè)視圖怎么繪制自己,最終怎么在你界面上展示。
UIImage和UIImageView
iOS系統(tǒng)支持很多標(biāo)準(zhǔn)的圖片格式:TIFF、JPEG、GIF、PNG等。當(dāng)一張圖片被包含在我們的app 包內(nèi),iOS系統(tǒng)特別地,會(huì)對(duì)PNG文件提供更加友好的支持,不只是因?yàn)橄到y(tǒng)會(huì)對(duì)它進(jìn)行壓縮處理,還有在不同分辨率下的對(duì)圖片的選取和展示都做了很多工作,所以我們應(yīng)該優(yōu)先選擇PNG格式圖片。我們可以通過(guò) imageNamed: 這個(gè)UIImage類(lèi)提供的方法獲取app包內(nèi)的圖片,這個(gè)方法會(huì)從兩個(gè)地方尋找指定的圖片:
app包頂級(jí)目錄
系統(tǒng)會(huì)通過(guò)提供的圖片名字,名字是大小寫(xiě)敏感的,以及包括圖片的類(lèi)型,在app包中尋找。如果沒(méi)有提供類(lèi)型,默認(rèn)是png格式。
Asset catalog 資源目錄
它會(huì)通過(guò)提供的名字,在這個(gè)資源目錄中尋找匹配的圖片集。如果名字帶有文件后綴,就不會(huì)在這里查找,以便舊代 碼中,如果把圖片移動(dòng)到這個(gè)目錄仍然能夠正常工作。這個(gè)目錄的查找優(yōu)先級(jí)比上面的查找高,也就意味著,如果在這個(gè) 資源目錄下找到了匹配的圖片,方法就會(huì)返回,而不會(huì)再去app包頂級(jí)目錄中查找。
可調(diào)整大小的Images
可以通過(guò)向一個(gè)UIImage發(fā)送 resizableImageWithCapInsets:resizingMode: 消息,來(lái)把圖片轉(zhuǎn)換成可調(diào)整大小的圖片。capInsets參數(shù)是一個(gè)UIEdgeInsets類(lèi)型的結(jié)構(gòu)體,由四個(gè)浮點(diǎn)型數(shù)字組成:top,left,bottom,right。它們代表著從圖片邊緣向內(nèi)的距離。在一個(gè)比圖片要大的上下文中,可調(diào)整大小的Image有兩種工作模式,通過(guò) resizingMode: value: 指定
UIImageResizingModeTile
在上面capInsets 指定的內(nèi)部矩形區(qū)域會(huì)平鋪在內(nèi)部,每一個(gè)邊緣由對(duì)應(yīng)邊的矩形區(qū)域平鋪而成,而外面的四個(gè)角落的矩形不變。
UIImageResizingModeStretch
內(nèi)部的矩形會(huì)被拉伸一次來(lái)填充內(nèi)部,每個(gè)邊緣由對(duì)應(yīng)變的矩形區(qū)域拉伸而成,而外面的四個(gè)角落的矩形不變。
例如:假設(shè) self.iv 是一個(gè)有固定長(zhǎng)寬的UIImageView,contentMode是UIViewContentModeScaleToFill。
(1)設(shè)置capInsets 為 UIEdgeInsetsZero
UIImage* mars = [UIImage imageNamed:@"Mars"]; UIImage* marsTiled = [mars resizableImageWithCapInsets: UIEdgeInsetsZero resizingMode: UIImageResizingModeTile]; self.iv.image = marsTiled;

(2)
UIImage* marsTiled = [mars resizableImageWithCapInsets: UIEdgeInsetsMake(mars.size.height/4.0, mars.size.width/4.0, mars.size.height/4.0, mars.size.width/4.0) resizingMode: UIImageResizingModeTile];

(3)常用的拉伸策略是把幾乎是原始圖片的一半作為capinset,僅僅在中間留出1到2像素來(lái)填充整個(gè)內(nèi)部。
UIImage* marsTiled = [mars resizableImageWithCapInsets: UIEdgeInsetsMake(mars.size.height/2.0 - 1, mars.size.width/2.0 - 1, mars.size.height/2.0 - 1, mars.size.width/2.0 - 1) resizingMode: UIImageResizingModeStretch];

在最新的Xcode5 中,我們可以不用代碼來(lái)配置一個(gè)可調(diào)整大小的圖片,僅僅通過(guò)Xcode5提供的一個(gè) asset catalogs 功能,而不用多次編寫(xiě)同樣的代碼,這個(gè)功能僅在ios7.0以上版本可用。
圖片的渲染模式


- (void) drawRect: (CGRect) rect { UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)]; [[UIColor blueColor] setFill]; [p fill];}現(xiàn)在我用Core Graphics 實(shí)現(xiàn)同樣的效果;這樣需要我首先拿到一個(gè)當(dāng)前上下文的引用:
- (void) drawRect: (CGRect) rect { CGContextRef con = UIGraphicsGetCurrentContext(); CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100)); CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor); CGContextFillPath(con);}接下來(lái),我會(huì)在UIView子類(lèi)中實(shí)現(xiàn) drawLayer:inContext:。這種情況下,我們手中的上下文引用并不是當(dāng)前上下文,所以我需要用UIKit把它轉(zhuǎn)換成當(dāng)前上下文:
- (void)drawLayer:(CALayer*)lay inContext:(CGContextRef)con { UIGraphicsPushContext(con); UIBezierPath* p = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0,0,100,100)]; [[UIColor blueColor] setFill]; [p fill]; UIGraphicsPopContext();}為了在drawLayer:inContext:中使用Core Graphics,我僅僅需要簡(jiǎn)單地保留一個(gè)我持有的上下文即可:
- (void)drawLayer:(CALayer*)lay inContext:(CGContextRef)con { CGContextAddEllipseInRect(con, CGRectMake(0,0,100,100)); CGContextSetFillColorWithColor(con, [UIColor blueColor].CGColor); CGContextFillPath(con);}最后,為了完整性,讓我們創(chuàng)建一個(gè)藍(lán)色圓形的UIImage對(duì)象。我們可以在任何時(shí)間(我們不需要等待某些特定方法被調(diào)用)以及在任何類(lèi)(我們不需要在UIView的子類(lèi))中創(chuàng)建。創(chuàng)建的UIImage你可以在任何地方正常使用,例如,你可以把它放到一個(gè)可見(jiàn)的UIImageView中當(dāng)做圖片展示,或者你可以把它保存在一個(gè)文件中,或者你可以在其他的繪制中使用(下一節(jié)介紹)。
首先,我使用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(); // im is the blue circle image, do something with it here ...
下面是使用Core Graphics實(shí)現(xiàn)的:
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();// im is the blue circle image, do something with it here ...
你可能會(huì)對(duì)UIGraphicsBeginImageContextWithOptions這個(gè)方法的參數(shù)感到疑惑,其實(shí)第一個(gè)參數(shù)顯然是將要?jiǎng)?chuàng)建的圖片的大小。第二個(gè)參數(shù)表明這個(gè)圖片是否是不透明的,如果我在上面的方法中傳遞YES而是不是NO,我的圖片將會(huì)有一個(gè)黑色背景,而我不想要這種效果。第三個(gè)參數(shù)指定圖片的縮放比例,傳遞0是告訴系統(tǒng)根據(jù)當(dāng)前屏幕的尺寸為我自動(dòng)設(shè)置壓縮比例,這樣我的圖片就會(huì)在單分辨率和雙分辨率屏幕下都能完美顯示。
你不必完全使用UIKit或者Core Graphics,相反地,你可以混合UIKit 調(diào)用和Core Graphics調(diào)用來(lái)操作同樣的圖形上下文。它們僅僅只是表示兩種不同的方式對(duì)同樣的圖形上下文通信而已。
CGImage繪畫(huà)
UIImage在Core Graphics中的版本是CGImage(實(shí)際上是CGImageRef)。它們可以很容易地互相轉(zhuǎn)換:UIImage有一個(gè)CGImage的屬性,可以訪問(wèn)它的Quartz 的圖片數(shù)據(jù),你也可以把CGImage 轉(zhuǎn)換成UIImage,使用imageWithCGImage:或者initWithCGImage:(在實(shí)戰(zhàn)中,你會(huì)更偏向使用更加可配置性的姐妹方法:imageWithCGImage:scale:orientation: 以及 initWithCGImage:scale:orientation:)。
一個(gè)CGImage可以讓你從一個(gè)原始圖片的一個(gè)矩形區(qū)域中創(chuàng)建一個(gè)新的圖片,而UIImage是做不到的。(一個(gè)CGImage還有其他強(qiáng)大的功能而UIImage沒(méi)有的,例如你可以將圖片的遮罩應(yīng)用到CGImage中)。我將會(huì)通過(guò)分隔一張火星圖片為兩半,并分開(kāi)單獨(dú)繪制每一邊。

注意,我們現(xiàn)在是在CFTypeRef范圍下操作,必須自動(dòng)管理好內(nèi)容:
UIImage* mars = [UIImage imageNamed:@"Mars"]; // extract each half as a CGImage 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)); // draw each CGImage into an image context 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(); CGImageRelease(marsLeft); CGImageRelease(marsRight)
但是這里的例子有個(gè)問(wèn)題:繪制的東西上下顛倒了! 它不是被旋轉(zhuǎn)了,而是從上到下映射,或者用專(zhuān)業(yè)的術(shù)語(yǔ),翻轉(zhuǎn)。這種想象會(huì)發(fā)生在你創(chuàng)建了一個(gè)CGImage,然后通過(guò)CGContextDrawImage繪制時(shí),是由于源和目標(biāo)上下文的本地坐標(biāo)系統(tǒng)不匹配。
有多種的方式補(bǔ)償這種不同坐標(biāo)系統(tǒng)之間的不匹配。其中一種就是把CGImage繪制成一個(gè)中間的UIImage,然后從UIImage中獲取CGImage,下面展示一個(gè)通用的函數(shù)來(lái)實(shí)現(xiàn)這種轉(zhuǎn)換:
// Utility for flipping an image drawingCGImageRef 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;}我們可以使用這個(gè)工具函數(shù)來(lái)修復(fù)我們上面例子中調(diào)用CGContextDrawImage產(chǎn)生的問(wèn)題,讓它們正確畫(huà)出火星的一半。
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));
但是,我們?nèi)匀挥幸粋€(gè)問(wèn)題:在雙分辨率設(shè)備上,如果我們的圖片有一個(gè)雙分辨率的版本(@2x.png),這個(gè)繪制就會(huì)出錯(cuò)。原因就是我們使用 imageNamed:來(lái)獲取原始的火星圖片,這樣就會(huì)返回一個(gè)為了適配雙分辨率而設(shè)置自己的縮放比例來(lái)產(chǎn)生雙倍分辨率的圖片。但是CGImage沒(méi)有scale屬性,同時(shí)對(duì)這張圖片為原始分辨率兩倍一無(wú)所知!因此,我們?cè)陔p分辨率設(shè)備上,我們通過(guò)調(diào)用 [mars CGImage]獲得到的火星CGImage圖片,是火星圖片大小的兩倍,那么我們所有的計(jì)算都是錯(cuò)的。
所以,為了在CGImage提取想要的片,我們必須把所有適當(dāng)?shù)闹党艘钥s放比例scale,或者以CGImage的尺寸來(lái)描述大小。下面是我們?cè)趩畏制梁碗p分屏都正確繪制的一個(gè)代碼版本,并且補(bǔ)償了翻轉(zhuǎn)效果:
UIImage* mars = [UIImage imageNamed:@"Mars"]; CGSize sz = mars.size; // Derive CGImage and use its dimensions to extract its halves 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); // The rest is as before, calling flip() to compensate for flipping 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);
另一種方案就是:在UIImage里面包裝一個(gè)CGImage,繪制這個(gè)UIImage。UIImage可以通過(guò)調(diào)用 imageWithCGImage:scale:orientation:來(lái)實(shí)現(xiàn)這種方式,補(bǔ)償縮放帶來(lái)的影響。此外,通過(guò)繪制一個(gè)UIImage,而不是一個(gè)的CGImage,我們避免了翻轉(zhuǎn)問(wèn)題。下面是一種同時(shí)處理翻轉(zhuǎn)和縮放的方法(沒(méi)有調(diào)用我們上面的公用類(lèi)):
UIImage* mars = [UIImage imageNamed:@"Mars"]; CGSize sz = mars.size; // Derive CGImage and use its dimensions to extract its halves 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);
是的,另一種方案解決翻轉(zhuǎn),就是在繪制CGImage之前,對(duì)圖形上下文進(jìn)行線性轉(zhuǎn)換,有效地翻轉(zhuǎn)圖形上下文中內(nèi)部的坐標(biāo)系統(tǒng)。這種方式很簡(jiǎn)潔,但是當(dāng)有其他的線性轉(zhuǎn)換時(shí)會(huì)變得難以理解。我會(huì)在下面的章節(jié)中談?wù)摳鄨D形上下文轉(zhuǎn)換的內(nèi)容。
為什么會(huì)發(fā)生翻轉(zhuǎn)??
Core Graphics 會(huì)意外發(fā)生翻轉(zhuǎn)的歷史,來(lái)源于OS X世界,OS X 里的坐標(biāo)系統(tǒng)的原點(diǎn)默認(rèn)是在左下角,正Y方向是向上的,而在iOS中,坐標(biāo)原點(diǎn)默認(rèn)在左上角,正Y方向是向下的。在大多數(shù)的繪畫(huà)中沒(méi)有問(wèn)題,因?yàn)閳D形上下文的坐標(biāo)系統(tǒng)會(huì)自動(dòng)適應(yīng)的。另外,在iOS的Core Graphics框架中的上下文繪畫(huà)時(shí),上下文的坐標(biāo)系統(tǒng)原點(diǎn)是左上角,我們都知道,但是,創(chuàng)建和繪制CGImage在兩個(gè)坐標(biāo)系統(tǒng)之間,互不匹配。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注