繪制一個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 左右圓角矩形