每個UIView
有一個伙伴稱為layer
,一個CALayer
。UIView
實際上并沒有把自己畫到屏幕上;它繪制本身到它的layer
上,它的layer
被繪制到屏幕上。正如我已經提到的,視圖不會被經常重繪;相反,它的繪制會被緩存,在可用的地方都會使用緩存版本(bitmap backing store
)。緩存的版本,實際上,就是layer
。視圖的圖形上下文??話實際上是layer
的圖形上下文。
這似乎僅僅是一個實現細節,但layer
是非常重要和有趣。理解layer
能更深刻地理解視圖;layer
延伸了視圖的能力。 尤其是:
-
Layers have properties that affect drawing.
layer具有超出一個UIView
繪制相關的屬性。由于layer
是視圖的繪制的接收者和展現者,你可以通過訪問該圖層的屬性來修改視圖在屏幕上的繪制。換句話說,通過視圖的layer
,你可用實現UIView
的方法實現不了的東西。 -
Layers can be combined within a single view.
一個UIView
的layer
層可含有附加的layer
。因為層的目的是為了繪制,這允許UIView
把要顯示的分散到不同的layer
中。這可以使繪圖更容易。 -
Layers are the basis of animation.
動畫可以讓你的界面更加簡潔更加酷炫。而layer
就是天生能做動畫的;在“CALayer"
的”CA“
就代表“Core Animation.”
。
例如,假設我們要添加一個指南針到我們的應用程序的界面。下圖展示了一個指南針的一個簡單的版本。它利用了我們前面例子中繪制的箭頭;箭頭被繪制到自身的layer中。羅盤的其它部分也是layer:圓是一個layer,并且每個基點字母是一個layer。這樣繪制代碼易于復合;更耐人尋味,不同的部分可用獨立放置,并分別動畫,所以很容易不移動圓圈就旋轉箭頭。
View and Layer
一個UIView
實例有一個CALayer
的實例,通過視圖layer
屬性訪問。這一layer
具有特殊的地位:它與視圖合作來顯示所有視圖的繪制。該layer
沒有相應的視圖屬性,但該視圖是layer的委托。文檔有時也會說layer
是視圖的underlying layer
。
默認情況下,當一個UIView
被實例化,其layer
是CALayer
的實例。如果你的UIView
子類,想改變underlying layer
的類型,實現UIView
子類的layerClass
類方法并返回CALayer
的子類。
下面是創建指南針的代碼。我們有一個UIView
子類,CompassView
和CALayer
的子類,CompassLayer
。這里是CompassView
的實現:
class CompassView: UIView {
override class func layerClass() -> AnyClass {
return CompassLayer.self
}
}
效果如下圖:
因此,當CompassView
實例化時,其下層是一個CompassLayer
。在這個例子中,CompassView
中沒有任何繪制。它的工作-在此情況下,其一的工作 - 是讓CompassLayer
在界面上顯示,因為一個層不能脫離視圖單獨出現在界面上。
因為每個視圖有一個underlying layer
,這兩者之間緊密結合。layer
描繪所有視圖的繪制;如果視圖繪制,它通過layer
來繪制。視圖是層的委托。視圖的屬性往往只是用于訪問層的屬性。例如,當你設置視圖的的backgroundColor
,你只是在設置layer
的backgroundColor
,如果你直接設置layer
的backgroundColor
,視圖的backgroundColor
會自動匹配。同樣,視圖的frame
其實是該layer
的frame
,反之亦然。
一個
CALayer
的delegate
屬性是可設置的,可以是任何基于NSObject
類的一個實例(CALayerDelegate
是一個非正式的協議,通過分類注入NSObject
類中)。但一個UIView
和它的layer
有一種特殊的關系。一個UIView
必須是layer
的delegate
;而且,它不能是任何其它layer
的delegate
。不要做任何事情,如果你搞砸了,繪圖將不會正常工作。
視圖繪制到它的layer
,然后layer
緩存這些繪制;然后layer
可以被操縱,從而改變視圖的外觀,而不必要求視圖重繪自身。這是繪圖系統高效率的原因。這也解釋了前面例子中視圖拉神的原因:當視圖大小變化時,默認情況下,繪圖系統只是簡單的伸展或重新定位緩存的layer
圖像,直到視圖被告知刷新(drawRect:
),從而替換layer
的內容。
Layers and Sublayers
layer
可以有子layer
但是最多只能有一個superlayer
。因此存在一個layer
的樹形結構。這和視圖的樹形結構是類似的。事實上,視圖和它的layer
如此緊密,這些層次結構是相同的層次結構。給定一個視圖及其layer
,該layer
的superlayer
是該view
的superview
的layer
,該layer
的子layer
是view
的subview
的layer
。事實上,由于layer
展示了view
如何被繪制,有人可能會說,視圖層次只是一個layer
層次結構。如下圖:
同時,layer
的層次結構可以超越視圖的層次結構。一個視圖只有一個layer
,但layer
可以有不屬于任何視圖的underlying layer
子層。因此視圖的underlying layer
層次結構和視圖的層次完全匹配,但總的layer
樹型結構可以是這個結構的一個超集。在下圖中有和上圖一樣的層級結構,但有兩個layer
具有它們自己單獨的layer
子層(即子layer
不屬于任何視圖的underlying layer
)。
如下圖:
從視覺角度看,layer
的層次結構和視圖的層次結構沒有區別。例如,在前面例子中,我們三個重疊視圖來繪制重疊的矩形。下面的代碼通過操作layer
來實現相同的視覺效果:
let layer1 = CALayer()
layer1.backgroundColor = UIColor(red: 1, green: 0.4, blue: 1, alpha: 1).CGColor
layer1.frame = CGRectMake(113, 111, 132, 194)
mainview.layer.addSublayer(layer1)
let layer2 = CALayer()
layer2.backgroundColor = UIColor(red: 0.5, green: 1, blue: 0, alpha: 1).CGColor
layer2.frame = CGRectMake(41, 56, 132, 194)
layer1.addSublayer(layer2)
let layer3 = CALayer()
layer3.backgroundColor = UIColor(red: 1, green: 0, blue: 1, alpha: 1).CGColor
layer3.frame = CGRectMake(43, 197, 160, 230)
mainview.addSublayer(layer3)
效果如下圖:
視圖的子視圖的layer
是這一視圖的underlaying layer
的sublayer
,就像該視圖的underlaying layer
的任何其他子layer
。因此,在繪制順序中可以把它們放在任何地方。視圖可以被分散到superview
的layer
的sublayer
中,這通常令初學者非常驚訝。例如,讓我們重新構造上圖,但是在layer2
和layer3
中間,我們將添加一個子視圖:
// ...
layer1.addSublayer(layer2)
let iv = UIImageView(image: UIImage(named: "smiley"))
mainview.addSubview(iv)
iv.frame.origin = CGPointMake(180, 180)
let layer3 = CALayer()
效果如下圖:
笑臉在紅色矩形前面被添加到界面上所以看來在矩形的后面。通過顛倒其中紅色矩形和笑臉加入到該界面的順序,笑臉可以出現在該矩形的前面。笑臉是一個視圖,而矩形只是一個layer
;所以他們沒有像視圖之間的兄弟關系,因為矩形不是一個視圖。但笑臉是視圖及其layer
;作為layer
,笑臉和矩形是兄弟關系,因為它們具有相同的superlayer
,所以一個可以出現在另一個的前面。
layer
是否超出自己的邊界之外的子layer
的區域取決于它的masksToBounds
屬性的值。這和視圖的clipsToBounds
屬性相似,而事實上,因為layer
是視圖的underlying layer
,所以它們是同一個東西。在上面2張圖中,layers
的clipsToBounds
都設置為false
(默認值);這就是為什么右側layer
超出的中間layer
的原因。
和UIView
類似,一個CALayer
的具有一個hidden
屬性可以把它和它的子layer
在界面中隱藏而不用從它的superlayer
中移除。
Manipulating the Layer Hierarchy
layer
使用了和視圖相似的一整套方法來讀取和操縱layer
的層次結構。layer
有一個superlayer
屬性和sublayers
屬性,以及下面的方法:
addSublayer:
insertSublayer:atIndex:
insertSublayer:below:, insertSublayer:above:
replaceSublayer:with:
removeFromSuperlayer
不同于視圖的subviews
屬性,layer
的sublayers
屬性是可寫的;因此,你可以通過sublayers
屬性一次性給layer
設置多個sublayer
。通過設置sublayers
為nil
來移除layer
的所以子layer
。
雖然一個layer
的子layer
有順序,可以通過上面提到的方法和sublayers
屬性來操縱順序,但這并不和繪制的順序完全相同。默認情況下,layer
有一個CGFloat
類型的zPosition
屬性值,這也決定了繪制順序。繪制規則是相同的zPosition
的所有子layer
在sublayers
屬性所列的順序繪制,但較低的zPosition
屬性比較高的zPosition
屬性的layer
先繪制。 (默認的zPosition
是0.0
)。
有時,zPosition
屬性比兄弟順序更方便的決定繪制順序。例如,如果layer
代表一個紙牌游戲的撲克牌,可能會更容易和方便通過設置zPosition
而不是子layer
自己的兄弟關系。此外,子視圖的layer
本身只是一個layer
,這樣你就可以通過設置它們的zPosition
來重新排列子視圖的繪制順序。在上圖中,如果我們指定圖像視圖的layer
的zPosition
為1
,它會被繪制在紅色矩形的前面:
mainview.addSubview(iv)
iv.layer.zPosition = 1
還有一些方法提供了用于在同一layer
層次結構內各layer
的坐標系統之間的轉換方法:
convertPoint:fromLayer:, convertPoint:toLayer:
convertRect:fromLayer:, convertRect:toLayer:
Positioning a Sublayer
layer
坐標系統和定位和視圖的那些類似。layer
有自己的內部坐標系統是由它bounds
屬性表示,就像視圖一樣。它的大小是它的bounds
大小,內部坐標系統的原點在它的左上角。
然而,sublayer
在它的super layer
中的位置不是由它的centre
屬性決定的;因為layer
沒有centre
。相反,sublayer
在super layer
中的位置由2個屬性聯合決定:
-
position
一個super layer
坐標系統中的點 -
anchorPoint
其中position
的位置,相對于該layer
自身的邊界比率。它是描述layer
自身的邊界的寬度和高度的比率的一個CGPoint
。因此,例如,(0.0,0.0)
是layer
的邊界的左上角,(1.0,1.0)
是layer
的邊界的右下角。
這里有一個比喻;并不是我創造的,但它是相當貼切。想象把sublayer
用圖釘固定到superlayer
;那么你不得不說這個針在什么地方穿過sublayer
(anchorPoint
),并固定在superlayer
的哪個位置(position
)。
如果anchorPoint
為(0.5,0.5)
(默認值),position
屬性和view
的center
屬性一樣。因此視圖的center
是layer
的position
的一種特殊情況。這是比較典型的視圖屬性和圖層特性之間的關系;視圖屬性往往是一個簡單的 - 但不那么強大 - 的layer
屬性的版本。
圖層的position
和anchorPoint
是正交(獨立的);改變一個不會改變另一個。因此,改變它們中的任一個,都可以改變layer
在superlayer
中的繪制的位置。
例如,在第一張圖中,圓圈的最重要的一點是其中心;所有其他對象需要相對于它來定位。因此,它們都具有相同的position:
該圓的中心。但它們的anchorPoint
不同。例如,箭頭的anchorPoint
是(0.5,0.8)
軸的中間靠近尾部。在另一方面,數字基點anchorPoint
為(0.5,3.0)
,已經超過字母的邊界,在圓形表盤的邊緣附件。
layer
的frame
屬性是一個純粹的衍生屬性。當你獲取frame
時,它是從邊界尺寸與position
和anchorPoint
計算出來的。當你設置frame
時,你設置邊界的大小和position
。一般情況下,你應該把frame
作為一個便利的屬性,這非常方便!例如,定位一個子層,使其恰好重疊superlayer
,你可以設置子層的``frame為superlayer
的bounds
。
在代碼中創建(而不是一個視圖的
underlying layer
)的layer
的frame
和bounds
都是(0.0,0.0,0.0,0.0)
,當你把它添加到屏幕上的superlayer
中它都是不可見的。如果你希望能夠看到它給你的layer
非零寬度和高度。創建layer
并將它添加到一個superlayer
然后發現它為什么沒有在界面中出現是一種常見的初學者錯誤。
CAScrollLayer
如果你將要移動一個layer
的邊界原點作為重新定位其子層位置的方式,你可能想使用CAScrollLayer
,一個CALayer
的子類,提供了這樣的方便的方法。(盡管是這樣的名字,一個CAScrollLayer
不提供滾動界面,用戶無法通過拖拽來滾動它。)默認情況下,CAScrollLayer
的masksToBounds
屬性為true
;因此,CAScrollLayer
就像window
一樣你只能看到它邊界以內的東西。(你可以設置它的masksToBounds
為false
,但是這是一件奇怪的事,因為它有點和目的相背。)
要移動CAScrollLayer
的邊界,你可以直接告訴它或者它的任何sublayer
:
-
Talking to the CAScrollLayer
-
scrollToPoint:
改變CAScrollLayer
的邊界原點到那個點。 -
scrollToRect:
最低限度地改變CAScrollLayer
邊界原點,使得邊界矩形的給定部分是可見的。
-
-
Talking to a sublayer
-
scrollPoint:
改變CAScrollLayer
邊界原點,使得layer
的給定的點是在CAScrollLayer
的左上角。 -
scrollRectToVisible:
改變CAScrollLayer
的邊界原點,這樣子層邊界的給定的區域在CAScrollLayer
的邊界區域內。你也可以訪問子層的visibleRect
,sublayer
在CAScrollLayer
的可見區域的部分。
-
Layout of Sublayers
視圖層次結構實際上是一個layer
層次結構。視圖在父視圖的位置實際上是其layer
在superlaye
內的定位。一個視圖可以被重新定位,通過其autoresizingMask
或通過根據它的約束自動調整大小。因此,如果layer
是視圖的underlying layer
它會自動調整大小。否則,iOS
不會對layer
自動調整大小。因此不是視圖的underlying layer
的sublayer
只能用代碼手動調整大小。
layer
的邊界更改或者調用setNeedsLayout
,此時layer
需要調整布局,你可以通過以下兩種方式響應:
- 該``layer
的
layoutSublayers方法被調用;通過重寫
CALayer的子類中
layoutSublayers`方法來響應布局變化。 - 或者在
layer
的delegate
中實現layoutSublayersOfLayer:
方法。 (請記住,如果layer
是一個視圖的underlying layer
,那么視圖是layer
的delegate
。)
為了有效布局sublayer
,你可能需要一種方法來識別或引用子layer
。layer
中沒有viewWithTag:
這樣的方法,因此怎樣識別和引用layer
完全取決于你。鍵-值編碼可能是有用的;layer
以一種特殊的方式實現了鍵值編碼。
對于視圖的underlying layer
來說,在視圖的layoutSubviews
被調用后,layer
的layoutSublayers
或layoutSublayersOfLayer:
也會被調用。在自動布局中,你必須調用super
否則自動布局會崩潰。此外,這些方法在自動布局過程中可能被調用一次以上;如果你正在尋找手動布局layer
的時機,視圖的布局事件可能是更好的選擇。
Drawing in a Layer
在layer
中顯示一些東西的最簡單的方法是通過它的contents
屬性。這和UIImageView
的image
屬性很相似。它期望一個CGImage
(或nil
,表示沒有圖像)。因此,舉例來說,下面是通過layer
而不是視圖來生成笑臉的代碼:
let layer4 = CALayer()
let im = UIImage(named: "smiley")!
layer4.frame = CGRect(origin: CGPointMake(180, 180), size: im.size)
layer4.contents = im.CGImage
mainview.layer.addSublayer(layer4)
設置layer的contents為一個UIImage,而不是一個CGImage,會默默地失敗 -- 影像不會出現,但沒有任何錯誤。這絕對會發瘋,每一個操作我都已經做到了,然后浪費時間搞清楚為什么我的layer沒有出現。
這有4種方法來為layer
提供需要的內容,類似于UIView
的drawRect:
方法.layer
會非常保守的調用這些方法(你不能直接調用其中的任何方法)。當layer
調用了這些方法,這就是說layer
重新顯示自己。下面是引起layer
重新顯示自己的方式:
- 如果
layer
的needsDisplayOnBoundsChange
屬性為false
(默認值),那么只有通過調用setNeedsDisplay
(或setNeedsDisplayInRect:
)才能讓它重新顯示自己。即使這樣可能也不會導致layer
馬上重新顯示自己;如果重新顯示自己非常重要,那么你可以調用displayIfNeeded
。 - 如果
layer
的needsDisplayOnBoundsChange
屬性為true
,那么當layer
的邊界變化是layer
也會重新顯示自己(類似視圖的.Redraw
模式)。
下面的四種方法可以被調用用來使layer
重新顯示自己;選擇一個實現(不要重復實現它們,否則你會奔潰):
-
display
in a subclass
你的CALayer
的子類可以重寫display
方法。這個時候沒有任何圖形上下文,因此display
方法只能限制于設置contents
的圖片。 -
displayLayer:
in the delegate
您可以設置CALayer
的delegate
然后在delegate
種實現DisplayLayer:
方法。和display
方法一樣,沒有圖形上下文,所以你只能給contents
設置圖像。 -
drawInContext:
in a subclass
你的CALayer
子類可以重寫drawInContext:
.此參數是一個圖形上下文,因此你可以在其中直接繪制;它不會自動成為當前上下文。 -
drawLayer:inContext:
in the delegate
你可以設置CALayer
的delegate
然后實現drawLayer:InContext:
.第二個參數是一個圖形上下文,你可以在其中直接繪制;它不會自動成為當前上下文。
給layer
的contents
分配一個圖像和直接在layer
里繪制在效果上是相互排斥的。 所以:
- 如果
layer
的contents
被分配一個圖像,這個圖像會立即顯示出來,并替換掉已被顯示在layer
上的繪制。 - 如果一個
layer
重新顯示本身,drawInContext:
或者drawLayer:inContext:
會在layer
里繪制,那么繪制會替換掉layer
種顯示的任何圖片。 - 如果一個
layer
重新顯示自己然而這四種方法沒有那一個能提供任何內容,那么layer
會是空的。
如果layer
是視圖的underlying layer
,你通常不會使用四種方法種的任何一種來繪制到layer
:你會使用視圖的drawRect:
.但是,你也可以使用這些方法,如果你真的想。在這種情況下,你可能會想實現一個空的drawRect:
方法。其原因是,這會導致layer
在適當的時刻重新顯示本身。當一個視圖被發送setNeedsDisplay
消息 - 包括當視圖首次出現時 - 視圖的underlying layer
也會被發送setNeedsDisplay
消息,除非視圖沒有實現drawRect:
(因為在這種情況下,假定該視圖永遠不需要重繪)。所以,如果直接使用視圖的underlying layer
來繪制整個視圖,而且當視圖需要重繪自己時視圖的underlying layer
在某個時刻要自動重新顯示自己,那么你應該實現一個空的drawRect:
方法。 (該技術對underlying layer
的子layer
沒有任何影響。)
下面這些都是能夠繪制到視圖(但不常用的)方法:
- 視圖的子類實現一個空的
drawRect:
方法,然后實現displayLayer:
或者drawLayer:inContext:
. - 視圖的子類實現一個空的
drawRect
方法和layerClass
方法返回自定義的layer
子類 - 在自定義的layer
子類種實現display
或者drawInContext:
方法.
切記,你不能設置視圖的underlying layer
的delegate
屬性!視圖是它的layer
的delegate
,并且必須保持其delegate
。通過delegate
繪制到layer
的一個有用的架構是將一個視圖當作layer-hosting
:視圖及其underlying layer
只用保持一個sublayer
,所以的繪制都方法在sublayer
中。如下圖:
layer
有一個contentsScale
屬性,這會把layer
中的圖形上下文中的點距映射到設備上的像素距離,由Cocoa
管理的layer
,如果它由內容其contentsScale
屬性會被自動調整;例如,一個實現drawRect:
的視圖,在雙分辨率的設備上其underlying layer
的contentsScale
屬性會被設置為2
.你自己創建并管理的layer
是沒有這種福利的,都得你自己手動設置;如果你想在layer
中繪制,那么正確的設置layer
的contentsScale
。contentsScale
為1
的的layer
的繪制內容在高分辨率的設置上看起來很模糊。如果layer
的contents
屬性為一個UIImage
的CGImage
,而去UIImage
的scale
屬性和layer
的scale
屬性不匹配,那么圖片會以一個錯誤的大小顯示。
三個layer
的屬性強烈地影響層layer
的顯示,而去非常不好理解:
-
backgroundColor
和視圖的backgroundColor
相似(如果該layer
是視圖的underlying layer
,那么它就是視圖的backgroundColor
)。改變backgroundColor
會立即生效。可以這樣認為backgroundColor
和layer
自己的繪制是分開的, 而且在layer
自己的繪制的下面。 -
opacity
這會影響layer
整體的透明度。它相當于一個視圖的alpha
屬性(并且如果該layer
是一個視圖的underlying layer
,它就是視圖的alpha
)。它也會影響layer
的子layer
的透明度。它分別影響layer
的背景顏色和內容的透明度(和視圖的alpha屬性類似)。改變opacity
屬性立即生效。 -
opaque
確定layer
的圖形上下文是否是不透明的。不透明的圖形上下文是黑色的;你可以在黑色的背景上繪制,但是黑色背景會被保留。非不透明的圖形上下文是clear
的;沒有繪制時,它是完全透明的。改變opaque
屬性不會馬上起作用,直到重新顯示layer
自己。視圖的underlying layer
的opaque
屬性完全獨立視圖的opaque
屬性;他們是不相關的,做完全不同的事情。
Content Resizing and Positioning
layer
的內容會被做為圖片緩存為位圖, 然后根據layer
的各種屬性繪制到layer
的bounds
內:
- 如果
layer
的內容是通過給contents
屬性設置一張圖片,那么緩存的內容就是這張圖片,大小就是CGImage
的大小。 - 如果
layer
的內容是直接繪制到layer
的圖形上下文(drawInContext:
,drawLayer:inContext:
)中的,緩存的內容是layer
的整個圖形上下文;它的大小是在執行繪制時layer
的大小。
layer
的屬性問題會導致layer
重新顯示自己時,緩存的內容會被調整大小,重新定位,裁剪等等,這些屬性是:
-
contentsGravity
這個屬性是一個字符串,類似于UIView
的contentMode
屬性,它描述了layer
的content
相對于bounds
如何被定位或者拉伸。例如,kCAGravityCenter
意味著內容在邊界居中而且不改變大小;kCAGravityResize
(默認值)意味著內容調整大小以適合bounds
,即使拉伸;等等。
由于歷史原因,
contentsGravity
值中Bottom
和Top
于它們的字面意思相反。
-
contentsRect
一個CGRect
表示內容被顯示的比例。默認值是(0.0,0.0,1.0,1.0)
,意思是顯示的全部內容。指定的內容部分根據contentsGravity
的設置會相對于bounds
重新調整大小和定位。因此通過設置contentsRect
,可以擴大部分內容來填充整個bounds
,或者不重新繪制或改變contents
的圖片來把大圖片整個縮小到視圖中。
你還可以通過指定較大contentsRect
如(-0.5,-0.5,1.5,1.5)
來縮減內容;但是內容靠近contentRect
邊緣的像素將被向外延伸到layer
的邊緣(以防止這一點,確保內容的最外像素都是空的)。 -
contentsCenter
一個CGRect
,結構類似contentsRect
,表示如果contentsGravity
設置為拉伸,被contentsRect
區分的九個拉伸區域的中間區域。中央區域(contentsCenter
的實際值)在兩個方向上拉伸。其他八個區域中,四個角的區域不拉伸,四條邊的區域向一個方向拉伸。 (這和resizible image
的拉伸方式類似)
如果layer
的內容來自于直接在layer
的圖形上下文中的繪制,那么contentsGravity
就沒有任何影響,因為圖形上下文的大小就是layer
自身的大小,所以就沒有拉伸和重新定位的問題。但contentsGravity
對于contentsRect
不是(0.0,0.0,1.0,1.0)
的情況就會有影響,因為現在我們指定了一個其他大小的矩形;contentsGravity
就是描述如何把這個矩形大小和layer
相適應。
而且,如果一個layer
的內容來自直接繪制到其圖形上下文中的繪制,那么當該layer
被調整大小時,如果該layer
被要求再次顯示本身,繪制會被再次執行使layer
的內容和layer
的大小相匹配。但是,如果當needsDisplayOnBoundsChange
是false
的時layer
的大小被調整的時候,則該layer
不重新顯示本身,所以其緩存的內容不再適合layer
的大小,那么contentsGravity
就會起作用。
通過一些聰明的設置,你就可以執行一些平時很難執行的繪制任務。例如,代碼如下:
arrow.needsDisplayOnBoundsChange = false
arrow.contentsCenter = CGRectMake(0.0, 0.4, 1.0, 0.6)
arrow.contentsGravity = kCAGravityResizeAspect
arrow.bounds.insetInPlace(dx: -20, dy: -20)
效果如下圖:
由于needsDisplayOnBoundsChange
是false
,當箭頭的邊界增加時不會重新顯示內容;相反,會使用緩存的內容。contentsGravity
屬性顯示要按比例調整大小;因此,箭頭會更長更寬,而不是一種扭曲的比例。然而,請注意,雖然三角形箭頭更寬,但它沒有變更長;因為contentsCenter
包括箭頭的軸,所以軸就整個拉伸了。
layer
的masksToBounds
屬性在其自己的內容和子layer
上有相同的效果。如果值是false
,則顯示全部內容,即使該內容超過layer
的大小。如果值是true
,將只顯示該layer
的邊界內的內容。
Layers that Draw Themselves
一些內置的CALayer
子類提供一些基本的但非常有用的自我繪制能力:
-
CATextLayer
一個CATextLayer
有一個字符串屬性,它可以是一個NSString
或NSAttributed?Strin
g,與其他文本格式屬性一起,有點像一個簡化的UILabel
;它繪制它的字符串。默認的文本顏色,就是ForegroundColor
屬性,是白色的,這不太可能是你想要的效果。text
和contents
是不同的而且是互斥的:內容圖片和文字只有一個會被繪制,所以一般你不應該給任何CATextLayer
設置內容圖像。上面圖片中,基點字母就是CATextLayer
實例。 -
CAShapeLayer
CAShapeLayer
有一個path
屬性,這是一個CGPath
。它填充或描邊此路徑,或兩者,這取決于其fillColor
和則strokeColor
值,并顯示描邊或填充的結果;fillColor
默認是黑色,默認沒有storkeColor
。它有線寬,虛線樣式,端帽樣式屬性,和連接樣式,類似于圖形上下文;它也有繪制其路徑(strokeStart
和strokeEnd
)的一部分的非凡能力,例如,繪制一個橢圓的一段弧線。一個CAShapeLayer
也可能有contents
;形狀被顯示在內容圖像的頂部,但沒有屬性指定一個合成模式。在上面的圖片中,背景圓是一個CAShapeLayer
實例,灰色描邊,明亮和稍微透明的灰色填充。 -
CAGradientLayer
CAGradientLayer
用簡單的線性漸變覆蓋它的背景;因此,在界面上用它繪制簡單的漸變很容易。漸變和Core graphics
中的差不多,有一個顏色的數組,與一個起始和結束點沿。可以將mask
添加到CAGradientLayer
上裁剪形狀。CAGradientLayer
不會顯示contents
的內容。
下圖顯示一個漸變效果的指南針:
Transforms
通過變換(transform
)可以修改layer
在屏幕上的繪制。因為視圖可以有一個transfrom
,而且視圖是通過其layer
繪制到屏幕上的。但是的layer
的變換比視圖的變換功能更強大;你可以使用它來完成你不能用一個視圖變換獨自完成的事。
在最簡單的情況下,當變換是二維的時候,你可以訪問layer
的AffineTransform
方法來訪問layer
的變換。變換施加于anchorPoint
的。
你現在已經知道了生成指南針的所以代碼含義。在這段代碼中,self
是CompassLayer
;它沒有繪制自己,而僅僅只是配置它的子layer
。這四個基本點的字母分別由CATextLayer
繪制;它們在相同的坐標系統中繪制,但它們具有不同的旋轉變換,而且被固定使得它們的旋轉是以圓的中心為中心。為了生成箭頭,我們使自己成為箭頭layer
的delegate
,并調用setNeedsDisplay;
這導致drawLayer:inContext
在CompassLayer
被調用。箭頭layer
由anchorPoint
釘扎其尾部在圓的中心,并且通過變換圍繞固定點旋轉:
// gradient
let grad = CAGradientLayer()
grad.contentScale = UIScreen.mainScreen().scale
grad.frame = self.bounds
grad.colors = [
UIColor.blackColor().CGColor,
UIColor.redColor().CGColor
]
grad.locations = [0.0, 1.0]
self.addSublayer(grad)
// circle
let circle = CAShapeLayer()
circle.contentsScale = UIScreen.mainScreen().scale
circle.linewidth = 2.0
circle.fillColor = UIColor(red: 0.9, green: 0.95, blue: 0.93, alpha: 0.9).CGColor
circle.strokeColor = UIColor.grayColor().CGColor
let p = CGPathCreateMutable()
CGPathAddEllipseInRect(circle)
circle.path = p
self.addSublayer(circle)
circle.bounds = self.bounds
circle.position = self.bounds.center
// four cardinal points
let pts = "NESW"
for (ix, c) in pts.characters.enumerate() {
let t = CATextLayer()
t.contentsScale = UIScreen.mainScreen().scale
t.string = String(c)
t.bounds = CGRectMake(0, 0, 40, 40)
t.position = circle.bounds.center
let vert = circle.bounds.midY / t.bounds.height
t.anchorPoint = CGPointMake(0.5, vert)
t.alignmentMode = kCAAlignmentCenter
t.foregroundColor = UIColor.blackColor().CGColor
t.setAffineTransform = CGAffineTransform(
CGAffineTransformMakeRotation(CGFloat(ix) * CGFloat(M_PI) / 2.0))
circle.addSublayer(t)
}
// arrow
let arrow = CALayer()
arrow.contentsScale = UIScreen.mainScreen().scale
arrow.bounds = CGRectMake(0, 0, 40, 100)
arrow.position = self.bounds.center
arrow.anchorPoint = CGPointMake(0.5, 0.8)
arrow.delegate = self //will draw arrow in delegate method
arrow.setAffineTransform(CGAffineTransformMakeRotation(CGFloat(M_PI) / 5.0))
self.addSublayer(arrow)
arrow.setNeedDisplay()
一個完備的layer
變換會發生在三維空間;其包括一個z
軸,垂直于x
軸和y
軸。 (默認情況下,z軸正方向指向屏幕外面,指向用戶的臉)。layer
不會奇跡般地給你逼真的三維渲染--你可以使用OpenGL
來實現三維渲染,這不在本文的討論范圍之內。layer
是二維對象,它們被設計的足夠簡單和快速。盡管如此,它們也可以在三維上操作,而且特別的快速真實,特別是執行動畫時尤其如此。我們都看到過屏幕上的圖像翻轉像翻一張紙一樣的效果而且可以顯示背面的東西;這是在三維空間的旋轉。
三維變換需要圍繞anchorPoint
,其z
分量由anchorPointZ
屬性提供。因此,在anchorPointZ
為0.0
的默認情況下,anchorPoint
是足夠的,正如我們使用CGAffineTransform
時已經看到。
transform
本身被一個稱為CAtransform3D
的數學結構描述。Core Animation Function Reference
中列出了一些操作此結構的函數。他們很像CGAffineTransform
,除了它們有第三個維度。例如,用于制造二維尺度變換的函數,CGAffineTransformMakeScale
,有兩個參數;用于制作3D尺寸變換的函數,CATransform3DMakeScale
,有三個參數。
旋轉3D轉換是有點復雜。除了角度,你也必須提供三個坐標描述圍繞其旋轉發生的向量。也許你已經從你的高中數學的知識中忘了什么是向量,或者試圖在你的腦袋中可視化三維向量。這真的很復雜。。。
假設該錨點為原點,(0.0,0.0,0.0)
。現在想象一下從錨點發出一個箭頭;它的另一端,它的結束點,由你你提供的三個坐標表示。現在,假設有一個相交于錨點并且垂直于箭頭的平面。這就是旋轉發生的平面;正角度值是順時針旋轉,就像下圖中平面的側面看到的效果。在效果上,你提供的三個坐標(相對于錨固點)你的眼睛都不得不將這張旋轉看成以前的二維旋轉。
矢量指定一個方向,而不是一個點。因此它對于你給的坐標標量沒有區別:(1.0,1.0,1.0)
和(10.0,10.0,10.0)
是相同的方向。如果三個值是(0.0,0.0,1.0)
,那么就是一個簡單的CGAffineTransform
,因為旋轉平面就是屏幕。如果這三個值是(0.0,0.0,-1.0)
,這是一個反向的CGAffineTransform
,使得正值角度看起來是逆時針旋轉的(因為我們在旋轉平面的背面看到的效果)
layer
可以通過旋轉顯示它的背面。例如,下面的layer
旋轉翻轉繞其y軸:
someLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
默認情況下,該層被認為是雙面的,所以當它被翻轉,以顯示背面的時候,顯示的是它的layer
的內容的逆向版本(連同子layer
和它們的所以坐標系統)。但是,如果該layer
的doubleSided
屬性為false
,那么當它被翻轉顯示其背面的時候;它的“背面”是透明的而且也是空的。
Depth
有兩種方式來放置layer
在不同的深度。一種是通過它們的位置,就是zPosition
屬性。另一種是在z
軸上施加一個平移變換來改變layer
的位置。layer
的position
的z
分量(zPosition
)和在z
軸的偏移量這兩個量是相關的;在某種意義上說,zPosition
是在z
方向的平移變換的簡寫形式。 (如果你同時提供zPosition
和z
方向平移變換,那么你會非常迷惑。)
在現實世界中,改變一個對象的zPosition
會使其顯示更大或更小,因為它和眼睛的距離更近或更遠;但是layer
的繪制和真實世界不一樣。這里沒有視角的概念;layer
在平面上按照它們真實的大小繪制而且疊在一起沒有間隙。(這就是所謂的正投影,并且藍圖經常以這樣的方式從側面顯示一個物體)。
然而,有一個廣泛使用的技巧對于layer
的繪制:使它的子layer
的sublayerTransform
屬性映射所以的點到一個“遠端”的平面。(這可能是關于sublayerTransform
屬性唯一的作用。)與正投影相結合,效果是將點透視應用到繪制中,使得z軸負方向視角更小。
例如,讓我們嘗試采用一種“翻頁”旋轉到我們的指南針上:我們會在它的右側固定,然后繞Y軸旋轉。這里,我們旋轉的子層(通過屬性,rotationLayer
訪問)是漸變層,并且圓和箭頭是其子層,使得它們一起旋轉:
self.rotationLayer.anchorPoint = CGPointMake(1, 0.5)
self.rotationLayer.position = CGPointMake(self.bounds.maxX, self.bounds.midY)
self.rotationLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI) / 4.0, 0, 1, 0)
結果如上圖;指南針看起來被壓扁。然而,現在,我們將適用距離映射轉換。這里的superlayer
是就是self
:
var transform = CATransform3DIdentity
transform.m34 = -1.0 / 1000.0
self.sublayerTransform = transform
結果如上圖顯示還可以,你可以用其他值來代替
1000.0
試試各種效果;例如,500.0
給出了一個更夸張的效果。此外,rotationLayer
的zPosition
也會影響它得大小。
繪制layer
隨深度改變大小的另一種方法是使用CATransformLayer
。這CALayer
的子類沒有做任何關于自己的繪制;它的目的僅僅是作為其它layer
的宿主。它最顯著的特征是你對它應用一個變換,它就會保持自己的子層之間的深度關系。 例如:
// layer1 is a layer, f is a CGRect
let layer2 = CALayer()
layer2.frame = f
layer2.backgroundColor = UIColor.blueColor().CGColor
layer1.addSublayer(layer2)
let layer3 = CALayer()
layer3.frame = f.offsetBy(dx: 20, dy: 30)
layer3.backgroundColor = UIColor.greenColor().CGColor
layer3.zPosition = 10
layer1.addSublayer(layer3)
layer1.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)
在代碼中,superlayer
layer1
有兩個子層,layer2
和layer3。子層以上述順序加入,所以
LAY3在
lay2的前面繪制。然后通過設置
layer1的
transform給
layer1執行了一個翻書頁的翻轉變換。如果
lay1是正常的
CALayer的子層則繪制順序不會改變;
LAY3仍然繪制在
layer2的前面即使添加翻轉變換后。但是,如果
lay1是
CATransformLayer,在翻轉變換后
layer3會繪制在
layer2的后面;因為它們都是
lay1`的子層,因此它們的深度關系保持不變。
下圖仍然通過給self
設置sublayerTransform
執行翻轉變換,不過這一次self
唯一的子layer
是CATransformLayer
:
var transform = CATransform3DIdentity
transform.m34 = -1.0 / 1000.0
self.sublayerTransform = transform
let master = CATransformLayer()
master.frame = self.bounds
self.addSublayer(master)
self.rotationLayer = master
效果如下圖:
執行翻轉變換的CATransformLayer
,持有漸變layer
,circle layer
,箭頭layer
。這三個層在不同深度(使用不同的zPosition
設置),給箭頭添加陰影從圓形表盤中分離:
circle.zPosition = 10
arrow.shadowOpacity = 1.0
arrow.shadowRadius = 10
arrow.zPosition = 20
你可以明顯的看到圓圈層漂浮在漸變層上面,在旋轉變換執行的過程中使用動畫可能效果更好。
為了更顯著,我添加了一個白色的小掛鉤,通過固定箭頭然后扎入圓圈里面!這是一個CAShapeLayer
,旋轉到垂直于CATransformLayer
:
let peg = CAShapeLayer()
peg.contentsScale = UIScreen.mainScreen().scale
peg.bounds = CGRectMake(0, 0, 3.5, 50)
let p2 = CGPathCreateMutable()
CGPathAddRect(p2, nil, peg.bounds)
peg.path = p2
peg.fillColor = UIColor(red: 1.0, green: 0.95, blue: 1.0, alpha: 0.95).CGColor
peg.anchorPoint = CGPointMake(0.5, 0.5)
peg.position = master.bounds.center
master.addSublayer(peg)
peg.setValue(M_PI / 2, forKeyPath: "transform.rotation.x")
peg.setValue(M_PI / 2, forKeyPath: "transform.ratation.z")
peg.zPosition = 15
上面的代碼實際上給我們的layer
做了一個3d模型。
Shadows, Borders, and Masks
一個CALayer
具有很多影響繪制細節的屬性。這也是UIView
高效繪制的原因,因為它們能作用于view
的underlying layer
。
一個CALayer
可以有陰影,由shadowColor
,shadowOpacity
,shadowRadius
和shadowOffset
屬性定義。為使該層繪制陰影,shadowOpacity
應該設置為非零值。陰影通常是根據該層的不透明區域的形狀繪制,但得到該形狀是cpu
密集型的。您可以通過自己定義形狀和把形狀做為CGPath
賦值給shadowPath
屬性,這會大大提高性能。
如果圖層的masksToBounds是true,邊界之外的陰影不會被繪制。。
CALayer
可以有一個邊框(borderWidth
,borderColor
);borderWidth
在邊框的里面繪制,這可能會遮蓋一部分內容,除非你有其他的處理。
CALayer
可通過cornerRadius
來設置圓角矩形。如果該層有邊框,也有圓角。如果該層具有backgroundColor
,那么背景顏色會被剪裁到圓角矩形的形狀。如果該層的masksToBounds
是true
,圖層的內容和它的子層會被圓角裁剪。
CALayer
可以有一個遮罩(mask
)。如果它本身就是一個層,其內容必須被以某種方式提供。mask
的內容在特定部分的透明度會成為layer
在相應部分的透明度。mask
的顏色沒有任何用處,只有透明度有用,要放置一個mask
,把它做為一個子layer
。
下圖顯示了我們的箭頭,一個灰色圓形層在它下面,并且施加了一個mask
:它是一個橢圓,用不透明顏色填充并且厚的半透明的顏色描邊。代碼如下:
let mask = CAShapeLayer()
mask.frame = arrow.bounds
let path = CGPathCreateMutable()
CGPathAddEllipseInRect(path, nil, CGRectMake(mask.bounds, 10, 10))
mask.strokeColor = UIColor(white: 0.0, alpha: 0.5).CGColor
mask.lineWidth = 20
mask.path = path
arrow.mask = mask
效果如下:
結合cornerRadius
,masksToBounds
和mask
,可以以更通用的方式來執行。例如,下面是產生遠角mask
的方法:
func maskOfSize(size: CGSize, roundingCorners rad: CGFloat) -> CALayer {
let r = CGRect(origin: CGPointZero, size: size)
UIGraphicsBeginImageContextWithOptions(r.size, false, 0)
let context = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, UIColor(white: 0, alpha: 0).CGColor)
CGContextFillRect(context, r)
CGContextSetFillColorWithColor(context, UIColor(white: 0, alpha: 1).CGColor)
let p = UIBezierPath(roundedRect: r, cornerRadius: rad)
p.fill()
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let mask = CALayer()
mask.frame = r
mask.contents = im.CGImage
return mask
}
從上面方法返回的layer
可以做為任何layer
的mask
。其結果是,layer
的所以內容包括子layer
都被剪切到圓角矩形的形狀;該形狀之外的一切都沒有繪制。這只是使用mask
實現的一個例子。mask
可以具有不透明和透明的之間的值,并且它可以是任何形狀。透明區域不一定非得在mask
區域外面;可以使用一個外部不透明內部透明的mask
來給layer
打孔。
你可以給視圖設置mask
通過maskView
屬性。這可能很便利但是沒有layer
層來的高效;這本質上還是給底層的layer
設置mask
。因此,這并不能解決mask
的大小調整問題。
Layer Efficiency
現在,你可能對layer
實現各種mask
。這沒有什么不妥,但是當iOS
設備把繪圖從一個地方轉移到另一個地方,設置可能不能迅速的響應這些請求。這類問題很可能出現尤其是當你執行的動畫或當用戶能夠通過觸摸動態繪制,滾動表視圖時。您可以通過肉眼來發現這些問題,你可以通過使用Instruments
的Core Animation template
來顯示動畫期間所取得的幀速率來發現這些問題。模擬器的DEBUG菜單也能讓你發現一些顏色疊加導致的繪圖效率問題。
在一般情況下,不透明繪制是最有效的。(非不透明繪制在Instruments
上用紅色標記為“blended layers
”。)如果一個圖層將始終顯示在單一顏色的背景上,你可以給它設置相同顏色的背景;
另一種方法來獲取效率提升是通過“凍結”繪圖的全部做為位圖。實際上,你先繪制到一個二級緩存,然后把緩存繪制到屏幕。從緩存繪制比直接繪制到屏幕效率低,但是如果layer層次很深很復雜就不用每次都去渲染整棵樹了。要做到這一點,設置圖層的shouldRasterize
為true
,設置rasterizationScale
一些有意義的值(可能UIScreen.mainScreen().scale
)。您可以隨時設置shouldRasterize
為false
,來關閉柵格化,所以很容易在一些很混亂的屏幕重排之前開啟然后在關閉它。
此外,還有一個圖層屬性drawsAsynchronously
。默認為false
。如果設置為true
,該層的圖形上下文積累繪圖命令,然后在某個恰當的時刻在后臺線程繪制。因此,您的繪圖命令運行速度非常快,因為他們實際上不是在你發送繪制命令時繪制。我還沒有機會使用這個,但是可能在你需要時間比較長的繪制的時候有效果。
Layers and Key–Value Coding
所有圖層屬性都可以通過具有相同名稱的屬性鍵的鍵值編碼來訪問。因此,為layer
添加mask
,可以這樣:
layer.mask = mask
也可以這樣:
layer.setValue(mask, forKeyPath: "mask")
此外,CATransform3D
和CGAffineTransform
值可以通過鍵 - 值編碼和key path
表示。例如,:
self.ratationLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI) / 4.0, 0, 1, 0)
也可以這樣:
self.rotationLayer.setValue(M_PI / 4, forKeyPath: "transform.rotation.y")
這種表示法是行的通的,因為CATransform3D
是鍵--值編碼兼容。這些都不是屬性,因為CATransform3D
不具有屬性。它沒有任何屬性,因為它都不是一個對象。你不能說:
self.ratationLayer.transform.rotation.y = ... //.. fail.
你經常會這樣使用transform
:
-
rotation.x
,rotation.y
,rotation.z
-
rotation
(和rotation.z
一樣) -
scale.x
,scale.y
,scale.z
-
translation.x
,translate.y
,translate.z
translation
甚至你可以把CALayer
作為一種字典,獲取和設置任意鍵的值。這意味著你可以將任意信息附加到一個單獨的層實例,并在以后檢索。例如,手動布局layer需要先引用到此layer。那么可以這樣做:
myLayer1.setValue("Foo", forKey: "name")
myLayer2.setVlaue("Foo2", forKey: "name")
圖層沒有一個name
屬性;'name'
屬性是我附加給layer
的。現在,我可以通過獲取各自的“name”鍵的值后確定這些層。
另外,CALayer
有defaultValueForKey:
類方法;實現它,你需要繼承和覆蓋此方法。在需要提供特定鍵一個默認值的情況下,返回默認值,;否則,返回來自調用super
的返回值。因此,即使從來沒有顯式提供值給某個特定鍵,它也可以有一個非零值。