本章中迄今為止的繪制實施例中大多會產生一個UIImage
對象,主要是通過調用UIGraphicsBeginImageContextWithOptions
得到的圖形上下文,生成由UIImageView
或知道如何以顯示圖像的任何界面對象顯示的圖片。但是,正如我已經解釋過,一個UIView
提供了一個圖形上下文;不管你在圖形上下文中繪制什么界面都會把它顯示出來。這主要是通過子類化UIView
并實現子類的drawRect:
方法。
例如假設我們有一個名為MyView
的UIView
子類。我們想實例化這個類,并添加這個實列到視圖層次上。實現這個效果的一種方式是在nib
編輯器中拖入一個UIView
在屬性編輯中設置class
為MyView
;另一種是使用代碼創建MyView
的實例,并把它放到界面上的。
結果是MyView
的drawRect:
會被調用。這是你的子類,不管你畫的是什么都將出現在MyView
的實例中在代碼運行的時候。因為UIView
自己實現的drawRect:
方法中什么都不會做,所有沒有必要調用super
。當drawRect:
被調用時,當前的圖形上下文已經被設置為視圖自己的圖形上下文。你可以使用Core Graphics
函數或UIKit
的便利方法在上外文中繪制。
任何時候都不應該直接調用
drawRect:
!你希望通過調用drawRect:
來更新你的視圖,對視圖發送setNeedsDisplay
消息就可以。這會導致的drawRect
:在下一個適當的時刻被調用。另外,不要重寫drawRect:
除非你確實有必要。例如:在UIImageView
的子類中重寫drawRect:
是非法的,你不能把自己的繪制和UIImageView
進行結合。
實時繪制對初學者來說有點驚訝,她們可能擔心繪圖一個耗時的操作。這是一種合理的擔憂,相同的繪制會發生在界面的許多地方,通過構造繪制命令為一個UIImage
,然后在視圖的drawRect
中繪制UIImage
,這通過都比較高效比每次都執行相同的繪制。但一般情況下,你不應該過早的優化代碼。繪圖操作的代碼可能會非常冗長但是非常快。此外,iOS
的繪圖系統是非常高效的;除非確實有必要(或者通過setNeedsDisplay
消息)它才會調用drawRect:
,并且一旦一個view
繪制完成,它將被緩存起來,使得高速緩存的繪制可以重復使用,而不是每次都從新繪制。(蘋果將這個繪制緩存做為視圖的bitmap backing store
。)通過在drawRect:
中輸出一些log信息你可以很容易知道這些;你可能會驚奇地發現你自定義的UIView
的drawRect:
代碼在應用程序的整個生命周期中只被調用了一次!其實,在drawRect:
中直接繪制通常是提高效率的一種方式。這是因為它比繪制到屏幕外面然后復制這些像素到屏幕上更加高效。(這就是不要離屏渲染的原因).
繪制的內容是很寬泛的但是可以劃分幾個部分,你可以通過傳入drawRect:
的 rect
參數以獲得一些額外的效率。它指定了需要刷新的視圖的區域。通常,這是該視圖的整個邊界;但如果你調用setNeedsDisplayInRect:
你可以設置刷新的CGRect
。您可以指定繪制到這些區域但即使你不這樣做,你的繪制也會被剪切到這些區域,所以,雖然你不會花更少的時間繪制,系統自己會更加高效的繪制。
當在代碼中創建一個自定義UIView子類的實例時,你可能會驚訝地發現該視圖有一個黑色的背景:
let mv = MyView(frame: CGRectMake(20, 20, 150, 150))
self.view.addSubview(mv)
如果你期望是一個透明的背景,這會令你非常失望。初學者也會覺得相當混亂。黑色背景出現了,那么有兩件事可以確定:
- 視圖的
backgroundColor
是nil
. - 視圖的
opaque
屬性是true
.
不幸的是,在代碼中創建一個UIView
的時候,這兩個東西默認都是true
!所以,如果你不想要黑色的背景,則必須改變其中一個(或兩者)。如果視圖不會是不透明的,它的opaque
應該設置為false
,所以這可能是干凈的解決方案:
let mv = MyView(frame: CGRectMake(20, 20, 150, 150))
self.view.addSubview(mv)
mv.opaque = false
或者,這是你自己的UIView
子類,你可以實現它的init(frame:)
(指定初始化方法)并設置它自己的opapue
屬性為false
:
override func init(frame: CGRect) {
super.init(frame: frame)
self.opaque = false
}
如果用nib
創建一個UIView
,黑色背景的問題就不會出現。這是因為這樣一個UIView
的backgroundColor
不會是nil
。nib
會給它分配一些實際的背景顏色,即使該顏色是UIColor.clearColor()
。
當然,如果一個視圖用不透明繪制填充的所有區域或具有不透明背景顏色,你可以設置opaque
為true
,并獲得一些繪圖效率的提示。
Graphics Context Settings
當你在一個圖形上下文繪制,繪圖遵循上下文的當前設置。因此,程序總是先設置上下文的各項參數,然后菜繪制。例如,如果要畫出一條紅線,緊接著畫一條藍線,你會先設置上下文的線條顏色為紅色,然后繪制的第一條線;然后將上下文的線條顏色設置為藍色,然后繪制第二條。肉眼看來紅線和藍線是獨立的線條,但實際上,在繪制每條線的時候,線條的顏色是作用于整個圖形上下文的。不管你是否使用UIKit
的方法或Core Graphics
函數,都是這樣的。
一個圖形上下文在任何一個時刻都有一個狀態,就是所有設置的總和;繪制就是在某時刻上下文狀態下繪制的結果。圖形上下文為你提供一個棧來操作上下文的所有的狀態。每當你調用CGContextSaveGState
時間,上下文推動整個當前的狀態壓入堆棧;每次調用CGContextRestoreGState
時,上下文從堆棧的頂部檢索狀態,并設置為自己的狀態。
因此,一種常見的模式是:
- 調用
CGContextSaveGState
。 - 設置上下文,從而改變它的狀態。
- 繪制。
- 調用
CGContextRestoreGState
來恢復到上一個上下文狀態設置。
你沒有必要每次操作上下文的設置都這樣做,因為設置不一定與其他或過去設置沖突。你可以很容易設置上下文的線條顏色為紅色然后在設置為藍色。然而你可能希望你的上下文設置在某些特定的情況下是不可撤消的。
構成一個圖形上下文的狀態,并且決定某一時刻繪制的行為和樣式的那些上下文設置和繪圖程序是類似的。下面是其中一些的,和確定它們樣式的命令。我列出了一些Core Graphics
的函數,其次是一些調用它們的UIKit
方法:
-
Line thickness and dash style
CGContextSetLineWidth
,CGContextSetLineDash
(andUIBezierPath
lineWidth
,setLineDash:count:phase:
) -
Line end-cap style and join style
CGContextSetLineCap
,CGContextSetLineJoin
,CGContextSetMiterLimit
(andUIBezierPath
lineCapStyle
,lineJoinStyle
,miterLimit
) -
Line color or pattern
CGContextSetRGBStrokeColor
,CGContextSetGrayStrokeColor
,CGContextSetStrokeColorWithColor
,CGContextSetStrokePattern
(andUIColor setStroke
) -
Fill color or pattern
CGContextSetRGBFillColor
,CGContextSetGrayFillColor
,CGContextSetFillColorWithColor
,CGContextSetFillPattern
(andUIColor setFill
) -
Shadow
CGContextSetShadow
,CGContextSetShadowWithColor
-
Overall transparency and compositing
CGContextSetAlpha
,CGContextSetBlendMode
-
Anti-aliasing(抗鋸齒)
CGContextSetShouldAntialias
其它設置包括:
-
Clipping area
裁剪區域之外的繪制不會在物理設備上繪制。 -
Transform (or “CTM,” for “current transform matrix”)
如何將你在后續的繪圖命令中指定的點映射到物理設備的畫布上的變化。
Paths and Shapes
通過一系列用于移動一個假想的筆的繪圖指令,用來構建一個從點到點的路徑。你必須先告訴筆自己的定位,設置當前點;然后你告訴它如何描繪出路徑上每一個后續的點。路徑上的每一塊都開始于當前點;并且結束時它是新的當前點。
需要注意的是路徑與它本身并不構成繪圖!首先,你提供一個路徑;然后你繪制它。繪制意味著路徑描邊或填充路徑,或兩者同時進行。這應該是繪圖程序中一個熟悉的概念。
下面是一些你可以使用的路徑繪制命令:
-
Position the current point
CGContextMoveToPoint
-
Trace a line
CGContextAddLineToPoint
,CGContextAddLines
-
Trace a rectangle
CGContextAddRect
,CGContextAddRects
-
Trace an ellipse or circle
CGContextAddEllipseInRect
-
Trace an arc
CGContextAddArcToPoint
,CGContextAddArc
-
Trace a Bezier curve with one or two control points
CGContextAddQuadCurveToPoint
,CGContextAddCurveToPoint
-
Close the current path
CGContextClosePath
。這個追加一條從路徑的最后一個點至第一點的線。如果你要填充路徑,你沒有必要這么做,因為它已經為你做了。 -
Stroke or fill the current path
CGContextStrokePath
,CGContextFillPath
,CGContextEOFillPath
,CGContextDrawPath
。路徑描邊或填充路徑會清楚當前路徑。使用CGContextDrawPath
來同時描邊和填充,因為如果你先用CGContextStrokePath
來描邊,你永遠都不能再填充它,因為路徑已經在描邊后被清除了。也有很多便利的方法使用一個命令來創建路徑,描邊或填充路徑:- CGContextStrokeLineSegments
- CGContextStrokeRect
- CGContextStrokeRectWithWidth
- CGContextFillRect
- CGContextFillRects
- CGContextStrokeEllipseInRect
- CGContextFillEllipseInRect
路徑可以合成,這意味著它可以包含多個獨立的部分。例如,一個單一的路徑可能包括兩個單獨的封閉的形狀:矩形和圓形。當您在構建路徑的中間調用CGContextMoveToPoint
(即構造路徑之后,并沒有描邊或填充以清除它),你拿起虛擬畫筆,并將其移動到一個新位置而不跟蹤這一段,從而開始構造一段獨立的路徑。當你開始構造一個路徑,這有可能是一個現有路徑而且新路徑可能被看作是現有路徑的一部分,你可以調用CGContextBeginPath
來指定,這是一個不同的路徑;蘋果公司的許多例子都這樣做,但在實踐中我通常不覺得這有必要。
為了說明路徑繪制命令的使用方法,我會繪制上圖所示的箭頭。這可能不是創建箭頭的最佳方式,而且我刻意回避使用的便利的函數,但是這很清晰的顯示了品種基本的典型的命令使用方式:
//obtain the current graphics context
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 100, 100)
CGContextAddLineToPoint(context, 100, 19)
CGContextSetLineWidth(context, 20)
CGContextStrokePath(context)
CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
CGContextMoveToPoint(context, 80, 25)
CGContextAddLineToPoint(context, 100, 0)
CGContextAddLineToPoint(context, 120, 25)
CGContextFillPath(context)
CGContextMoveToPoint(context, 90, 101)
CGContextAddLineToPoint(context, 100, 90)
CGContextAddLineToPoint(context, 110, 101)
CGContextSetBlendMode(context, .Clear)
CGContextFillPath(context)
如果需要重復使用或共享路徑,可以將它封裝為一個CGPath
。你可以使用CGContextCopyPath
來復制圖形上下文的當前路徑。即使沒有圖形上下文,你可以創建一個新的CGMutablePath
(調用CGPathCreateMutable
),并使用和CGContext
的路徑構造函數相似的各種CGPath
函數來構建路徑。而且還有很多用于創建基于簡單幾何圖形或現有路徑的CGPath函數:
- CGPathCreateWithRect
- CGPathCreateWithEllipseInRect
- CGPathCreateWithRoundedRect
- CGPathCreateCopyByStrokingPath
- CGPathCreateCopyByDashingPath
- CGPathCreateCopyByTransformingPath
UIKit
的UIBezierPath
類封裝了CGPath
(CGPath
屬性);它提供了和CGContext
和CGPath
相似的函數用于構建路徑,如:
- init(rect:)
- init(ovalInRect:)
- init(roundedRect:cornerRadius:)
- moveToPoint:
- addLineToPoint:
- addArcWithCenter:radius:startAngle:endAngle:clockwise:
- addQuadCurveToPoint:controlPoint:
- addCurveToPoint:controlPoint1:controlPoint2:
- closePath
當調用UIBezierPath
實例方法填充或描邊(fillWithBlendMode:alpha:
或者strokeWithBlendMode:alpha:
),當前圖形上下文設置會被保存,包裝的CGPath
會成為當前的圖形上下文的路徑然后被描邊或填充,然后當前圖形上下文的設置會被恢復。
因此,使用UIBezierPath結合UIColor,我們可以完全用UIKit的方法改寫我們的箭頭繪制例子:
let path = UIBezierPath()
path.moveToPoint(CGPointMake(100, 100))
path.AddLineToPoint(CGPointMake(100, 19))
path.lineWidth = 20
path.strokePath()
UIColor.redColor().set()
path.removeAllPoints()
path.moveToPoint(CGPointMake(80, 25))
path.addLineToPoint(CGPointMake(100, 0))
path.addLineToPoint(CGPointMake(120, 25))
path.fill()
path.removeAllPoints()
path.moveToPoint(CGPointMake(90, 101))
path.addLineToPoint(CGPointMake(100, 90))
path.addLineToPoint(CGPointMake(110, 101))
path.fillWithBlendMode(.Clear, alpha: 1.0)
上面代碼沒有調用Core Graphics
函數,因此使用Core Graphics
或者UIKit
取決于你自己。UIBezierPath
也是很有用的,當你想捕捉一個CGPath
并把它作為一個object
傳遞的時候;
Clipping
你可能會使用路徑來遮罩(mask
)區域而不是通過填充或者路徑描邊,來使它們不會在以后被繪制。這就是clipping
。默認情況下,圖形上下文的剪裁區域是整個圖形上下文:你可以在上下文中的任何地方繪制。
裁剪區域的一個特征是把上下文作為一個整體,并且任何新的裁剪區域會于現有的裁剪區域相交;所以如果你想把你自己的裁剪區域從當前的上下文中刪除,通過提前調用CGContextSaveGState
和CGContextRestoreGState
來實現。
我將使用裁剪(clipping
)而不是混合模式(blend mode
)來處理上面例子中的箭頭尾部“切出”的三角形缺口。這有個小技巧,因為我們要裁剪的不是三角形的內部區域,但是三角形外面的區域。為了說明這一點,我們將使用由多個封閉區域組成的復合路徑 -- 三角形和做為整體的繪圖區域(可以通過CGContextGetClipBoundingBox
獲得)。
填充復合路徑或者用它來表達裁剪區域的時候,系統遵循以下兩個規則之一:
-
Winding rule (非0纏繞規則)
填充或剪裁區域由在路徑劃分的每個區域的方向(順時針或逆時針)表示。 -
Even-odd rule (EO--奇偶數規則)
由每個區域路徑的簡單計數表示填充或剪輯區域。
上面兩個規則都是圖形學方面來確定是否繪制子路徑的方法。iOS默認使用非0纏繞規則。
我們的情況非常簡單,所以更容易使用奇偶規則。因此我們通過CGContextEOClip
設置裁剪區域然后繪制箭頭:
let context = UIGraphicsGetCurrentContext()
CGContextMoveToPoint(context, 90, 100)
CGContextAddLineToPoint(context, 100, 90)
CGContextAddLineToPoint(context, 110, 100)
CGContextClosePath(context)
CGContextAddRect(context, CGContextGetClipBondingBox(context))
CGContextEOClip(context)
//draw vertical line
CGContextMoveToPoint(context, 100, 100)
CGContextAddLineToPoint(context, 100, 19)
CGContextSetLineWidth(context, 20)
CGContextStrokePath(context)
// draw red triangle
CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
CGContextMoveToPoint(context, 80, 25)
CGContextAddLineToPoint(context, 100, 0)
CGContextAddLineToPoint(context, 120, 25)
CGContextFillPath(context)
UIBezierPath
的裁剪命令是usesEvenOddFillRule
和addClip
。
| 上下文有多大 |
| :---------: |
|乍一看,似乎沒有方法知道圖形上下文的大小。通常情況下,這并不重要,因為圖形上下文是你自己創建或者被一些你知道它大小的物體創建的,比如一個UIView
的圖形上下文。但事實上,因為圖形上下文的默認剪切區域是整個上下文,你可以使用CGContextGetClipBoundingBox
知道上下文的“邊界”。|
Gradients
漸變可以很簡單也可以很復雜。一個簡單的漸變可以通過一個顏色點與另一個顏色點,再加(或可選)中間顏色點確定;那么漸變可以通過兩個點線性或兩個圓之間徑向繪制。
你不能用一個漸變做為路徑的填充顏色,但可以通過裁剪來限制路徑的形狀,這本質上是一樣的。
為了說明,我將重繪我們的箭頭,用線性漸變做為箭頭的“軸”:
let context = UIGraphicsGetCurrentContext()!
CGContextSaveState(context)
CGContextMoveToPoint(context, 90, 100)
CGContextAddLineToPoint(context, 100, 90)
CGContextAddLineToPoint(context, 110, 100)
CGContextClosePath(context)
CGContextAddRect(context, CGContextGetClipBoundingBox(context))
CGContextEOClip(context)
//draw vertical line
CGContextMoveToPoint(context, 100, 100)
CGContextAddLineToPoint(context, 100, 19)
CGContextSetLineWidth(context, 20)
CGContextReplacePathWithStrokePath(context)
CGContextClip(context)
//draw gradient
let locs: [CGFloat] = [0.0, 0.5, 1.0]
let colors: [CGFloat] = [
0.8, 0.4, //start color, transparent light gray
0.1, 0.5, //intermediate color,
0.8, 0.4, //end color,
]
let sp = CGColorSpaceCreateDeviceGray()
let grad = CGGradientCreateWithColorComponents(sp, colors, locs, 3)
CGContextDrawLinearGradient(context, grad, CGPointMake(89, 0), CGPointMake(110, 0), [])
//done clipping
CGContextRestoreState(context)
//draw red triangle
CGContextSetFillColorWithColor(context, UIColor.redColor().CGColor)
CGContextMoveToPoint(context, 80, 25)
CGContextAddLineToPoint(context, 100, 0)
CGContextAddLineToPoint(context, 120, 25)
CGContextFillPath(context)
效果如下圖:
對CGContextReplacePathWithStrokedPath
的調用會使用當前線寬和其他線條相關的上下文狀態設置來假裝繪制路徑,但然后創建表示該描邊路徑之外的新路徑。從而替換作為裁剪區域使用的粗線段。
我們創建漸變然后繪制它。該過程冗長但是簡單;一切都是模式化的。我們把漸變描述成一個端點(0.0)到另一個端點(1.0)之間的連續的顏色點的位置數組,具有對應于每個位置的顏色的顏色的COM ponents沿;在這個例子中,我希望漸變在邊緣處較亮在中間較暗,所以我用三個位置,而去較暗的位置在0.5處。我們還必須提供一個色彩空間;這將告訴漸變如何繪制我們的顏色分量。最后,創建漸變并將其繪制到指定位置。
Colors and Patterns
一種顏色是一個CGColor
。CGColor
不難使用,并且可以通過的UIColor
的init(CGColor:)
和CGColor
的方法在這兩個之間相互轉換。
一個模式(pattern
)也是一種顏色。您可以創建一個模式顏色然后描邊或者填充它。最簡單的方法是繪制模式顏色成一個小塊狀的UIImage
然后調用UIColor
的init(patternImage:)
創建顏色。為了說明這一點,我將創建橫條紋的模式顏色并用它來畫箭頭,而不是一個堅實的紅色:
// CGContextSetFillColorWithColor(con, UIColor.redColor().CGColor)
// not any more, we're going to paint with a pattern instead of red!
// create the pattern image tile
UIGraphicsBeginImageContextwithOptions(CGSizeMake(4, 4), false, 0)
let imcon = UIGraphicsGetCurrentContext()!
CGContextSetFillColorWithColor(imcon, UIColor.redColor().CGColor)
CGContextFillRect(imcon, CGRectMake(0, 0, 4, 4))
CGContextSetFillColorWithColor(imcon, UIColor.blueColor().CGColor)
CGContextFillRect(imcon, CGRectMake(0, 0, 4, 2))
let stripes = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
//paint the point of the arrow with it
let stripesPattern = UIColor(patternImage: stripes)
stripesPattern.setFill()
let p = UIBezierPath()
p.moveToPoint(CGPointMake(80, 25))
p.addLineToPoint(CGPointMake(100, 0))
p.addLineToPoint(CGPointMake(120, 25))
p.fill()
效果如下圖:
Core Graphics
的CGPattern
相當的強大,而且也更加的復雜:
let sp2 = CGColorSpaceCreatePattern(nil)
CGContextSetFillColorSpace(con, sp2)
let drawStripes : CGPatternDrawPatternCallback = {
_, con in
CGContextSetFillColorWithColor(con!, UIColor.redColor().CGColor)
CGContextFillRect(con!, CGRectMake(0,0,4,4))
CGContextSetFillColorWithColor(con!, UIColor.blueColor().CGColor)
CGContextFillRect(con!, CGRectMake(0,0,4,2))
}
var callbacks = CGPatternCallbacks(version: 0, drawPattern: drawStripes, releaseInfo: nil)
let patt = CGPatternCreate(nil, CGRectMake(0,0,4,4),
CGAffineTransformIdentity, 4, 4,
.ConstantSpacingMinimalDistortion, true, &callbacks)
var alph : CGFloat = 1.0
CGContextSetFillPattern(con, patt, &alph)
CGContextMoveToPoint(con, 80, 25)
CGContextAddLineToPoint(con, 100, 0)
CGContextAddLineToPoint(con, 120, 25)
CGContextFillPath(con)
要理解這段代碼可以倒著讀。一切都圍繞調用CGPatternCreate
。模式是在矩形“細胞”中的繪制。我們要說明細胞的尺寸(第二個參數)和細胞的原點之間的間距(第四和第五參數)。在這種情況下,該細胞大小是4×4,和每一個細胞恰好水平和垂直接觸它的鄰居。我們必須提供一個變換作用與細胞上(第三個參數);在這個例子中,我們沒有做任何變換,所以我們提供identity transform
。我們提供一個平鋪規則(第六個參數)。我們必須說明這是否是一個顏色模式或模板模式;這是一個顏色模式,所以第七個參數是true
。我們還有一個指針指向實際模式圖案到細胞(第八參數)的回調函數。
其實我們要在這里提供一個CGPatternCallbacks
結構的指針做為第八個參數。這個結構包括數字0和兩個函數指針,一個繪制模式到其細胞,另一個在模式被釋放的時候調用。我們沒有提供第二個函數,但是我們并不需要在這個簡單的例子中管理內存。
正如你所看到的,實際的模式繪制代碼(drawStripes
)是很簡單的。唯一棘手的問題是,調用CGPatternCreate
時單元格必須大小一樣,不然模式并不會以你期望的方式繪制成功。在這個例子中單元格大小是4×4。因此,我們用紅色填充它,然后用藍色填充它的下半部分。當這些單元格被水平和垂直平鋪時,我們得到了你在上圖中看到條紋。
通過CGPatternCreate
生成CGPattern
之后,調用CGContextSetFillPattern
而不是設置填充顏色,我們設置填充模式來填充路徑(這個例子中的三角箭頭)。CGContextSetFillPattern
第三個參數是指向一個CGFloat
的指針,所以我們必須事先設置好CGFloat
本身。第二個參數是CGPattern
。
剩下來唯一要解釋的是代碼的前兩行。在調用CGContextSetFillPattern
之前,你必須設置上下文的填充色色彩空間設置為模式的色彩空間。如果你忘記這一點,調用CGContextSetFillPattern
會得到一個錯誤。上面的代碼通過設置其填充顏色空間為模式顏色空間,這讓圖形上下文在一個不穩定的狀態。如果我們后來嘗試將填充顏色設置為正常顏色這會很麻煩。解決方案,像往常一樣,是將代碼封裝在對CGContextSaveState
和CGContextRestoreState
的調用之間。
你可以在上圖中看到,條紋并不完全適合箭頭三角形:最底層的條紋有點像半個藍色條紋。這是因為模式相對于你正在填充(或描邊)的形狀沒有很好的縮放,但相對于作為一個整體的圖形上下文它能很好的縮放。我們可以在繪制之前調用CGContextSetPatternPhase
移動模式的位置。
Graphics Context Transforms
正如一個UIView可以有一個轉換(transform
),圖形上下文也可以有。然而,將一個變換作用于圖形上下文不會影響已經存在的繪制,它只會影響繪制完成后的坐標系統映射到圖形上下文的區域轉換。圖形上下文的變換被稱為CTM
(current transform matrix.
)。
充分利用CTM
讓自己免于執行簡單的計算是很常用的。你可以通過CGContextConcatCTM
來疊加CGAffineTransform
到現有的變換上;也有便利的函數用于平移,縮放或旋轉變換到當前變換。
當你獲得圖形上下文時最基本的變換已經為你設置好;這就是系統能夠映射上下文繪制坐標到屏幕坐標的原因。任何變換都被疊加到當前變換,所以基本變換和繪制都是有效的。在應用你的變換后可以通過封裝你的代碼在調用CGContextSaveGState
和CGContextRestoreGState
之間而回到基本變換。
例如,迄今為止我們一直硬編碼我們的箭頭位置:矩形的左上角處(80,0)。這是愚蠢的。它使代碼難以理解,以及缺乏靈活性,很難重用。明智的做法是在(0,0)處繪制的箭頭,通過現有代碼從所有x值減去80。現在很容易在任何位置畫出箭頭,通過事先施加一個簡單的平移變換,映射(0,0)到箭頭左上角。因此,在(80,0)畫出箭頭,我們可以這樣:
CGContextTranslateCTM(con, 80, 0)
// now draw the arrow at (0,0)
旋轉變換特別有用,它可以讓你在一個旋轉的方向中繪制而不用關系任何關于幾何的計算。然而這也有點棘手,因為是圍繞原點旋轉的,這通常不是你想要的。所以你必須先做一個平移變換,把原點映射到你要旋轉的點。但旋轉之后,為了搞清楚在哪里繪制你可能要做個反向平移變換。
為了說明這一點,下面的代碼在多個角度重復的圍繞它的尾巴轉動來繪制箭頭。由于箭頭將被繪制多次,我會把繪制箭頭封裝成一個UIImage。這不僅是減少重復會使繪制效率更高;這也是因為我們希望整個箭頭旋轉,包括圖案的條紋,這是實現這種效果最簡單的方法:
func arrowImage() -> UIImage {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(40, 100), false, 0)
let context = UIGraphicsGetCurrentContext()
//draw arrow into the image context
//draw it at (0, 0)! adjust all x-values by subtracting 80
// ... actual code omitted...
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return im
}
我們只生成箭頭圖片一次,并把它存儲在某個地方 - 我將使用屬性為self.arrow
來獲取它。在我們的drawRect:
方法種,我們多次繪制箭頭圖像:
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
self.arrow.drawAtPoint(CGPointMake(0, 0))
for _ in 0 ..< 3 {
CGContextTranslateCTM(context, 20, 100)
CGContextRotateCTM(context, 30 * CGFloat(M_PI) / 180.0)
CGContextTranslateCTM(context, -20, -100)
self.arrow.drawAtPoint(CGPointMake(0, 0))
}
}
變換也是一種解決CGContextDrawImage
的“翻轉”問題?方案。我們可以旋轉上下文而不是我們的繪制。從本質上講,我們對上下文的坐標系統施加了一個翻轉變換。首先移動上下文的頂部向下,然后通過給y
軸乘以-1
讓y
軸向下:
CGContextTranslateCTM(context, 0, theHeight)
CGContextScaleCTM(context, 1.0, -1.0)
效果如下:
向下移動上下文的頂部(高度)多遠取決于你打算如何繪制圖像。
Shadows
在繪制前給上下文添加陰影值來給繪制添加陰影效果。陰影的位置被表示為一個CGSize
,其中對于兩個值的正方向表示向下和向右。模糊值是一個開放式的正數;蘋果并沒有解釋這是如何工作的,但實驗表明,值12是很好的效果,值99太過于模糊而沒有形狀,更高的值會變得有問題。
下圖顯示了在繪制之前先給上下文添加陰影的效果:
let con = UIGraphicsGetCurrentContext()!
CGContextSetShadow(con, CGSizeMake(7, 7), 12)
self.arrow.drawAtPoint(CGPointMake(0,0)) // ... and so on
效果如下圖:
從上圖中不能很明顯的發現我們每繪制都增加了一層陰影。因此,箭頭能夠在另一個箭頭下面投下陰影。然而,我們希望所有的箭頭集體投下一個陰影。實現這一目標的方法是使用一個透明層;這基本上是一個子上下文然后積累所有的繪制之后添加一個陰影。現在繪制陰影箭頭代碼如下所示:
let context = UIGraphicsGetCurrentContext()
CGContextSetShadow(context, CGSizeMake(7, 7), 12)
CGContextBeginTransparencyLayer(context, nil)
self.arrow.drawAtPoint(CGPointMake(0, 0))
for _ in 0 ..< 3 {
CGContextTranslateCTM(context, 20, 100)
CGContextRotateCTM(context, -20, 100)
CGContextTranslateCTM(context, -20, -100)
self.arrow.drawAtPoint(CGPointMake(0, 0))
}
CGContextEndTransparencyLayer(context)
Erasing
CGContextClearRect
函數能夠擦除矩形內的所有繪制;結合裁剪,它可擦除任意形狀的區域。結果是可以在現有的繪制中打洞
。
CGContextClearRect
的行為取決于上下文是否是透明的或不透明的。這在圖像上下文中特別明顯直觀。如果圖像背景是透明的 - UIGraphicsBeginImageContextWithOptions
的第二個參數是false
- CGContextClearRect
擦除繪制為透明背景;否則擦除繪制為黑色背景。
當在視圖中直接繪制時(通過drawRect:
或drawLayer:inContext:
),如果視圖的背景顏色是nil
或透明,甚至只有一點透明度,CGContextClearRect
擦除的結果將顯示為透明,穿透視圖的背景顏色打一個孔;如果背景顏色是完全不透明的,CGContextClearRect
的結果將是黑色的。這是因為,該視圖的背景色確定該視圖的圖形上下文是否是透明的或不透明;因此,這基本上和我在前面段落中描述的行為相投。
下圖顯示左側的藍色正方形已被部分切掉為黑色,而在右側的藍色正方形已部分切掉為透明。然而這都是一樣的UIView
子類,具有完全相同的繪制代碼!該UIView
子類的drawRect:
是這樣的:
let context = UIGraphicsGetCurrentContext()!
CGContextSetFillColorWithColor(context, UIColor.blueColor().CGColor)
CGContextFillRect(context, rect)
CGContextClearRect(context, CGRectMake(0, 0, 30, 30)))
效果如下:
上圖中2種視圖的差別是,第一個視圖的backgroundColor
是純紅色而且alpha
值為1
,而第二個視圖的backgroundColor
是純紅色alpha
值為0.99
。這種差異是眼睛完全察覺不到的(更不用說從未出現的紅色,因為它被藍色全部覆蓋),但它完全改變CGContextClearRect
的效果。
Points and Pixels
點是通過一個無量綱位置的x坐標和y坐標表示的。當你在圖形上下文種繪制時,可以指定在某些點處繪制,這和設備的分辨率沒有關系,因為Core graphics
使用基本CTM
和反鋸齒會很好的轉換你的繪制到屏幕上。因此在上面的例子中,我只關注圖形上下文中的點而忽略它們和屏幕像素的關系。
但是,確實存在像素。像素是物理的,整體的,現實世界中顯示的基本單元。點介于像素之間。例如,如果有線寬為1為豎直的坐標軸描邊,繪制的線會在路徑的兩邊,這在單分辨率的屏幕上繪制的線看起來是2像素寬(因為該設備無法點亮半個像素)。
這種效果有時候是令人反感的,有人建議應該嘗試偏移線的位置0.5,把線的中心和像素對齊。這個建議可能會有用,但它只是一些頭腦簡單的假設。一個更復雜的方法是獲得UIView
的contentScaleFactor
屬性。你可以除以這個數值從像素轉換為點。最準確的方法來繪制一個垂直或水平線不是為路徑描邊,而是填充矩形。因此,下嗎的UIView子類代碼可以在任何設備上繪制一個完美的1個像素寬的垂直線:
CGContextFillRect(context, CGRectMake(100, 0, 1.0 / self.contentScaleFactor, 100))
Content Mode
一個視圖自己繪制一些東西和其他僅僅有一個背景顏色和子視圖的視圖截然相反。這意味視圖的contentMode屬性就變得非常重要當它的大小改變的時候。正如我前面提到的,繪圖系統將避免重新繪制一個視圖,相反,它會使用以前的繪制操作(bitmap backing store
)緩存的結果。所以,如果視圖大小改變時,系統可能會簡單地伸展或收縮或移動緩存的繪制,如果你的contentMode設置指示它這樣做。
說明這一點有點棘手,視圖的內容來自于drawRect:
,因為我要為視圖準備好它的內容(在drawRect:
),然后改變它的大小但是不讓它被重繪(drawRect:
不能被再次調用)。下面是我的實現。當應用程序啟動時,我將創建一個UIView
子類,MyView
,它知道如何繪制我們的箭頭。然后,在窗口和界面已經顯示之后我會使用一個延時函數來改變我們視圖的大小:
delay(0.1) {
mv.bounds.size.height *= 2
}
我們把視圖的高度變為雙倍,而不會導致drawRect:
被調用。其結果是,該視圖的繪制顯示在其兩倍高度。
遲早,drawRect
:將被調用,根據我們的代碼繪制會被刷新。我們的代碼沒有指定箭頭的高度是相對于視圖的邊界高度;它繪制的箭頭在一個固定的高度。因而,箭頭會縮回到原來的大小。
因此視圖的contentMode屬性經常于如何繪制自身向一致。我們的drawRect:
代碼指示箭頭的大小和位置相對于視圖邊界原點,在視圖的左上角位置;所以我們可以設置contentMode
為.TopLeft
。另外,我們可以將其設置為.Redraw
;這將導致自動緩存內容被關閉 -當視圖大小改變時,它的setNeedsDisplay
方法將被調用,最終引發drawRect:
重繪內容。