programming iOS - layer

每個UIView有一個伙伴稱為layer,一個CALayerUIView實際上并沒有把自己畫到屏幕上;它繪制本身到它的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.
    一個UIViewlayer層可含有附加的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被實例化,其layerCALayer的實例。如果你的UIView子類,想改變underlying layer的類型,實現UIView子類的layerClass類方法并返回CALayer的子類。

下面是創建指南針的代碼。我們有一個UIView子類,CompassViewCALayer的子類,CompassLayer。這里是CompassView的實現:

class CompassView: UIView {
    override class func layerClass() -> AnyClass {
        return CompassLayer.self
    }
}

效果如下圖:


因此,當CompassView實例化時,其下層是一個CompassLayer。在這個例子中,CompassView中沒有任何繪制。它的工作-在此情況下,其一的工作 - 是讓CompassLayer在界面上顯示,因為一個層不能脫離視圖單獨出現在界面上。

因為每個視圖有一個underlying layer,這兩者之間緊密結合。layer描繪所有視圖的繪制;如果視圖繪制,它通過layer來繪制。視圖是層的委托。視圖的屬性往往只是用于訪問層的屬性。例如,當你設置視圖的的backgroundColor,你只是在設置layerbackgroundColor,如果你直接設置layerbackgroundColor,視圖的backgroundColor會自動匹配。同樣,視圖的frame其實是該layerframe,反之亦然。

一個CALayerdelegate屬性是可設置的,可以是任何基于NSObject類的一個實例(CALayerDelegate是一個非正式的協議,通過分類注入NSObject類中)。但一個UIView和它的layer有一種特殊的關系。一個UIView必須是layerdelegate;而且,它不能是任何其它layerdelegate。不要做任何事情,如果你搞砸了,繪圖將不會正常工作。

視圖繪制到它的layer,然后layer緩存這些繪制;然后layer可以被操縱,從而改變視圖的外觀,而不必要求視圖重繪自身。這是繪圖系統高效率的原因。這也解釋了前面例子中視圖拉神的原因:當視圖大小變化時,默認情況下,繪圖系統只是簡單的伸展或重新定位緩存的layer圖像,直到視圖被告知刷新(drawRect:),從而替換layer的內容。

Layers and Sublayers

layer可以有子layer但是最多只能有一個superlayer。因此存在一個layer的樹形結構。這和視圖的樹形結構是類似的。事實上,視圖和它的layer如此緊密,這些層次結構是相同的層次結構。給定一個視圖及其layer,該layersuperlayer是該viewsuperviewlayer,該layer的子layerviewsubviewlayer。事實上,由于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 layersublayer,就像該視圖的underlaying layer的任何其他子layer。因此,在繪制順序中可以把它們放在任何地方。視圖可以被分散到superviewlayersublayer中,這通常令初學者非常驚訝。例如,讓我們重新構造上圖,但是在layer2layer3中間,我們將添加一個子視圖:

// ...
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張圖中,layersclipsToBounds都設置為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屬性,layersublayers屬性是可寫的;因此,你可以通過sublayers屬性一次性給layer設置多個sublayer。通過設置sublayersnil來移除layer的所以子layer

雖然一個layer的子layer有順序,可以通過上面提到的方法和sublayers屬性來操縱順序,但這并不和繪制的順序完全相同。默認情況下,layer有一個CGFloat類型的zPosition屬性值,這也決定了繪制順序。繪制規則是相同的zPosition的所有子layersublayers屬性所列的順序繪制,但較低的zPosition屬性比較高的zPosition屬性的layer先繪制。 (默認的zPosition0.0)。

有時,zPosition屬性比兄弟順序更方便的決定繪制順序。例如,如果layer代表一個紙牌游戲的撲克牌,可能會更容易和方便通過設置zPosition而不是子layer自己的兄弟關系。此外,子視圖的layer本身只是一個layer,這樣你就可以通過設置它們的zPosition來重新排列子視圖的繪制順序。在上圖中,如果我們指定圖像視圖的layerzPosition1,它會被繪制在紅色矩形的前面:

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。相反,sublayersuper layer中的位置由2個屬性聯合決定:

  • position
    一個super layer坐標系統中的點
  • anchorPoint
    其中position的位置,相對于該layer自身的邊界比率。它是描述layer自身的邊界的寬度和高度的比率的一個CGPoint。因此,例如,(0.0,0.0)layer的邊界的左上角,(1.0,1.0)layer的邊界的右下角。

這里有一個比喻;并不是我創造的,但它是相當貼切。想象把sublayer用圖釘固定到superlayer;那么你不得不說這個針在什么地方穿過sublayeranchorPoint),并固定在superlayer的哪個位置(position)。

如果anchorPoint(0.5,0.5)(默認值),position屬性和viewcenter屬性一樣。因此視圖的centerlayerposition的一種特殊情況。這是比較典型的視圖屬性和圖層特性之間的關系;視圖屬性往往是一個簡單的 - 但不那么強大 - 的layer屬性的版本。

圖層的positionanchorPoint是正交(獨立的);改變一個不會改變另一個。因此,改變它們中的任一個,都可以改變layersuperlayer中的繪制的位置。

例如,在第一張圖中,圓圈的最重要的一點是其中心;所有其他對象需要相對于它來定位。因此,它們都具有相同的position:該圓的中心。但它們的anchorPoint不同。例如,箭頭的anchorPoint(0.5,0.8)軸的中間靠近尾部。在另一方面,數字基點anchorPoint(0.5,3.0),已經超過字母的邊界,在圓形表盤的邊緣附件。

layerframe屬性是一個純粹的衍生屬性。當你獲取frame時,它是從邊界尺寸與positionanchorPoint計算出來的。當你設置frame時,你設置邊界的大小和position。一般情況下,你應該把frame作為一個便利的屬性,這非常方便!例如,定位一個子層,使其恰好重疊superlayer,你可以設置子層的``frame為superlayerbounds

在代碼中創建(而不是一個視圖的underlying layer)的layerframebounds都是(0.0,0.0,0.0,0.0),當你把它添加到屏幕上的superlayer中它都是不可見的。如果你希望能夠看到它給你的layer非零寬度和高度。創建layer并將它添加到一個superlayer然后發現它為什么沒有在界面中出現是一種常見的初學者錯誤。

CAScrollLayer

如果你將要移動一個layer的邊界原點作為重新定位其子層位置的方式,你可能想使用CAScrollLayer,一個CALayer的子類,提供了這樣的方便的方法。(盡管是這樣的名字,一個CAScrollLayer不提供滾動界面,用戶無法通過拖拽來滾動它。)默認情況下,CAScrollLayermasksToBounds屬性為true;因此,CAScrollLayer就像window一樣你只能看到它邊界以內的東西。(你可以設置它的masksToBoundsfalse,但是這是一件奇怪的事,因為它有點和目的相背。)

要移動CAScrollLayer的邊界,你可以直接告訴它或者它的任何sublayer

  • Talking to the CAScrollLayer
    • scrollToPoint:
      改變CAScrollLayer的邊界原點到那個點。
    • scrollToRect:
      最低限度地改變CAScrollLayer邊界原點,使得邊界矩形的給定部分是可見的。
  • Talking to a sublayer
    • scrollPoint:
      改變CAScrollLayer邊界原點,使得layer的給定的點是在CAScrollLayer的左上角。
    • scrollRectToVisible:
      改變CAScrollLayer的邊界原點,這樣子層邊界的給定的區域在CAScrollLayer的邊界區域內。你也可以訪問子層的visibleRectsublayerCAScrollLayer的可見區域的部分。

Layout of Sublayers

視圖層次結構實際上是一個layer層次結構。視圖在父視圖的位置實際上是其layersuperlaye內的定位。一個視圖可以被重新定位,通過其autoresizingMask或通過根據它的約束自動調整大小。因此,如果layer是視圖的underlying layer它會自動調整大小。否則,iOS不會對layer自動調整大小。因此不是視圖的underlying layersublayer只能用代碼手動調整大小。

layer的邊界更改或者調用setNeedsLayout,此時layer需要調整布局,你可以通過以下兩種方式響應:

  • 該``layerlayoutSublayers方法被調用;通過重寫CALayer的子類中layoutSublayers`方法來響應布局變化。
  • 或者在layerdelegate中實現layoutSublayersOfLayer:方法。 (請記住,如果layer是一個視圖的underlying layer,那么視圖是layerdelegate。)

為了有效布局sublayer,你可能需要一種方法來識別或引用子layerlayer中沒有viewWithTag:這樣的方法,因此怎樣識別和引用layer完全取決于你。鍵-值編碼可能是有用的;layer以一種特殊的方式實現了鍵值編碼。

對于視圖的underlying layer來說,在視圖的layoutSubviews被調用后,layerlayoutSublayerslayoutSublayersOfLayer:也會被調用。在自動布局中,你必須調用super否則自動布局會崩潰。此外,這些方法在自動布局過程中可能被調用一次以上;如果你正在尋找手動布局layer的時機,視圖的布局事件可能是更好的選擇。

Drawing in a Layer

layer中顯示一些東西的最簡單的方法是通過它的contents屬性。這和UIImageViewimage屬性很相似。它期望一個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提供需要的內容,類似于UIViewdrawRect:方法.layer會非常保守的調用這些方法(你不能直接調用其中的任何方法)。當layer調用了這些方法,這就是說layer重新顯示自己。下面是引起layer重新顯示自己的方式:

  • 如果layerneedsDisplayOnBoundsChange屬性為false(默認值),那么只有通過調用setNeedsDisplay(或setNeedsDisplayInRect:)才能讓它重新顯示自己。即使這樣可能也不會導致layer馬上重新顯示自己;如果重新顯示自己非常重要,那么你可以調用displayIfNeeded
  • 如果layerneedsDisplayOnBoundsChange屬性為true,那么當layer的邊界變化是layer也會重新顯示自己(類似視圖的.Redraw模式)。

下面的四種方法可以被調用用來使layer重新顯示自己;選擇一個實現(不要重復實現它們,否則你會奔潰):

  • display in a subclass
    你的CALayer的子類可以重寫display方法。這個時候沒有任何圖形上下文,因此display方法只能限制于設置contents的圖片。
  • displayLayer: in the delegate
    您可以設置CALayerdelegate然后在delegate種實現DisplayLayer:方法。和display方法一樣,沒有圖形上下文,所以你只能給contents設置圖像。
  • drawInContext: in a subclass
    你的CALayer子類可以重寫drawInContext:.此參數是一個圖形上下文,因此你可以在其中直接繪制;它不會自動成為當前上下文。
  • drawLayer:inContext: in the delegate
    你可以設置CALayerdelegate然后實現drawLayer:InContext:.第二個參數是一個圖形上下文,你可以在其中直接繪制;它不會自動成為當前上下文。

layercontents分配一個圖像和直接在layer里繪制在效果上是相互排斥的。 所以:

  • 如果layercontents被分配一個圖像,這個圖像會立即顯示出來,并替換掉已被顯示在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 layerdelegate屬性!視圖是它的layerdelegate,并且必須保持其delegate。通過delegate繪制到layer的一個有用的架構是將一個視圖當作layer-hosting:視圖及其underlying layer只用保持一個sublayer,所以的繪制都方法在sublayer中。如下圖:

layer有一個contentsScale屬性,這會把layer中的圖形上下文中的點距映射到設備上的像素距離,由Cocoa管理的layer,如果它由內容其contentsScale屬性會被自動調整;例如,一個實現drawRect:的視圖,在雙分辨率的設備上其underlying layercontentsScale屬性會被設置為2.你自己創建并管理的layer是沒有這種福利的,都得你自己手動設置;如果你想在layer中繪制,那么正確的設置layercontentsScalecontentsScale1的的layer的繪制內容在高分辨率的設置上看起來很模糊。如果layercontents屬性為一個UIImageCGImage,而去UIImagescale屬性和layerscale屬性不匹配,那么圖片會以一個錯誤的大小顯示。

三個layer的屬性強烈地影響層layer的顯示,而去非常不好理解:

  • backgroundColor
    和視圖的backgroundColor相似(如果該layer是視圖的underlying layer,那么它就是視圖的backgroundColor)。改變backgroundColor會立即生效。可以這樣認為backgroundColorlayer自己的繪制是分開的, 而且在layer自己的繪制的下面。
  • opacity
    這會影響layer整體的透明度。它相當于一個視圖的alpha屬性(并且如果該layer是一個視圖的underlying layer,它就是視圖的alpha)。它也會影響layer的子layer的透明度。它分別影響layer的背景顏色和內容的透明度(和視圖的alpha屬性類似)。改變opacity屬性立即生效。
  • opaque
    確定layer的圖形上下文是否是不透明的。不透明的圖形上下文是黑色的;你可以在黑色的背景上繪制,但是黑色背景會被保留。非不透明的圖形上下文是clear的;沒有繪制時,它是完全透明的。改變opaque屬性不會馬上起作用,直到重新顯示layer自己。視圖的underlying layeropaque屬性完全獨立視圖的opaque屬性;他們是不相關的,做完全不同的事情。

Content Resizing and Positioning

layer的內容會被做為圖片緩存為位圖, 然后根據layer的各種屬性繪制到layerbounds內:

  • 如果layer的內容是通過給contents屬性設置一張圖片,那么緩存的內容就是這張圖片,大小就是CGImage的大小。
  • 如果layer的內容是直接繪制到layer的圖形上下文(drawInContext:,drawLayer:inContext:)中的,緩存的內容是layer的整個圖形上下文;它的大小是在執行繪制時layer的大小。

layer的屬性問題會導致layer重新顯示自己時,緩存的內容會被調整大小,重新定位,裁剪等等,這些屬性是:

  • contentsGravity
    這個屬性是一個字符串,類似于UIViewcontentMode屬性,它描述了layercontent相對于bounds如何被定位或者拉伸。例如,kCAGravityCenter意味著內容在邊界居中而且不改變大小;kCAGravityResize(默認值)意味著內容調整大小以適合bounds,即使拉伸;等等。

由于歷史原因,contentsGravity值中BottomTop于它們的字面意思相反。

  • 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的大小相匹配。但是,如果當needsDisplayOnBoundsChangefalse的時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)

效果如下圖:


由于needsDisplayOnBoundsChangefalse,當箭頭的邊界增加時不會重新顯示內容;相反,會使用緩存的內容。contentsGravity屬性顯示要按比例調整大小;因此,箭頭會更長更寬,而不是一種扭曲的比例。然而,請注意,雖然三角形箭頭更寬,但它沒有變更長;因為contentsCenter包括箭頭的軸,所以軸就整個拉伸了。

layermasksToBounds屬性在其自己的內容和子layer上有相同的效果。如果值是false,則顯示全部內容,即使該內容超過layer的大小。如果值是true,將只顯示該layer的邊界內的內容。

Layers that Draw Themselves

一些內置的CALayer子類提供一些基本的但非常有用的自我繪制能力:

  • CATextLayer
    一個CATextLayer有一個字符串屬性,它可以是一個NSStringNSAttributed?String,與其他文本格式屬性一起,有點像一個簡化的UILabel;它繪制它的字符串。默認的文本顏色,就是ForegroundColor屬性,是白色的,這不太可能是你想要的效果。textcontents是不同的而且是互斥的:內容圖片和文字只有一個會被繪制,所以一般你不應該給任何CATextLayer設置內容圖像。上面圖片中,基點字母就是CATextLayer實例。
  • CAShapeLayer
    CAShapeLayer有一個path屬性,這是一個CGPath。它填充或描邊此路徑,或兩者,這取決于其fillColor和則strokeColor值,并顯示描邊或填充的結果;fillColor默認是黑色,默認沒有storkeColor。它有線寬,虛線樣式,端帽樣式屬性,和連接樣式,類似于圖形上下文;它也有繪制其路徑(strokeStartstrokeEnd)的一部分的非凡能力,例如,繪制一個橢圓的一段弧線。一個CAShapeLayer也可能有contents;形狀被顯示在內容圖像的頂部,但沒有屬性指定一個合成模式。在上面的圖片中,背景圓是一個CAShapeLayer實例,灰色描邊,明亮和稍微透明的灰色填充。
  • CAGradientLayer
    CAGradientLayer用簡單的線性漸變覆蓋它的背景;因此,在界面上用它繪制簡單的漸變很容易。漸變和Core graphics中的差不多,有一個顏色的數組,與一個起始和結束點沿。可以將mask添加到CAGradientLayer上裁剪形狀。CAGradientLayer不會顯示contents的內容。
    下圖顯示一個漸變效果的指南針:

Transforms

通過變換(transform)可以修改layer在屏幕上的繪制。因為視圖可以有一個transfrom,而且視圖是通過其layer繪制到屏幕上的。但是的layer的變換比視圖的變換功能更強大;你可以使用它來完成你不能用一個視圖變換獨自完成的事。

在最簡單的情況下,當變換是二維的時候,你可以訪問layerAffineTransform方法來訪問layer的變換。變換施加于anchorPoint的。

你現在已經知道了生成指南針的所以代碼含義。在這段代碼中,selfCompassLayer;它沒有繪制自己,而僅僅只是配置它的子layer。這四個基本點的字母分別由CATextLayer繪制;它們在相同的坐標系統中繪制,但它們具有不同的旋轉變換,而且被固定使得它們的旋轉是以圓的中心為中心。為了生成箭頭,我們使自己成為箭頭layerdelegate,并調用setNeedsDisplay;這導致drawLayer:inContextCompassLayer被調用。箭頭layeranchorPoint釘扎其尾部在圓的中心,并且通過變換圍繞固定點旋轉:

// 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屬性提供。因此,在anchorPointZ0.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和它們的所以坐標系統)。但是,如果該layerdoubleSided屬性為false,那么當它被翻轉顯示其背面的時候;它的“背面”是透明的而且也是空的。

Depth

有兩種方式來放置layer在不同的深度。一種是通過它們的位置,就是zPosition屬性。另一種是在z軸上施加一個平移變換來改變layer的位置。layerpositionz分量(zPosition)和在z軸的偏移量這兩個量是相關的;在某種意義上說,zPosition是在z方向的平移變換的簡寫形式。 (如果你同時提供zPositionz方向平移變換,那么你會非常迷惑。)

在現實世界中,改變一個對象的zPosition會使其顯示更大或更小,因為它和眼睛的距離更近或更遠;但是layer的繪制和真實世界不一樣。這里沒有視角的概念;layer在平面上按照它們真實的大小繪制而且疊在一起沒有間隙。(這就是所謂的正投影,并且藍圖經常以這樣的方式從側面顯示一個物體)。

然而,有一個廣泛使用的技巧對于layer的繪制:使它的子layersublayerTransform屬性映射所以的點到一個“遠端”的平面。(這可能是關于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給出了一個更夸張的效果。此外,rotationLayerzPosition也會影響它得大小。

繪制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有兩個子層,layer2layer3。子層以上述順序加入,所以LAY3lay2的前面繪制。然后通過設置layer1transformlayer1執行了一個翻書頁的翻轉變換。如果lay1是正常的CALayer的子層則繪制順序不會改變;LAY3仍然繪制在layer2的前面即使添加翻轉變換后。但是,如果lay1CATransformLayer,在翻轉變換后layer3會繪制在layer2的后面;因為它們都是lay1`的子層,因此它們的深度關系保持不變。

下圖仍然通過給self設置sublayerTransform執行翻轉變換,不過這一次self唯一的子layerCATransformLayer

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,持有漸變layercircle 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高效繪制的原因,因為它們能作用于viewunderlying layer

一個CALayer可以有陰影,由shadowColorshadowOpacityshadowRadiusshadowOffset屬性定義。為使該層繪制陰影,shadowOpacity應該設置為非零值。陰影通常是根據該層的不透明區域的形狀繪制,但得到該形狀是cpu密集型的。您可以通過自己定義形狀和把形狀做為CGPath賦值給shadowPath屬性,這會大大提高性能。

如果圖層的masksToBounds是true,邊界之外的陰影不會被繪制。。

CALayer可以有一個邊框(borderWidthborderColor);borderWidth在邊框的里面繪制,這可能會遮蓋一部分內容,除非你有其他的處理。

CALayer可通過cornerRadius來設置圓角矩形。如果該層有邊框,也有圓角。如果該層具有backgroundColor,那么背景顏色會被剪裁到圓角矩形的形狀。如果該層的masksToBoundstrue,圖層的內容和它的子層會被圓角裁剪。

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,masksToBoundsmask,可以以更通用的方式來執行。例如,下面是產生遠角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可以做為任何layermask。其結果是,layer的所以內容包括子layer都被剪切到圓角矩形的形狀;該形狀之外的一切都沒有繪制。這只是使用mask實現的一個例子。mask可以具有不透明和透明的之間的值,并且它可以是任何形狀。透明區域不一定非得在mask區域外面;可以使用一個外部不透明內部透明的mask來給layer打孔。

你可以給視圖設置mask通過maskView屬性。這可能很便利但是沒有layer層來的高效;這本質上還是給底層的layer設置mask。因此,這并不能解決mask的大小調整問題。

Layer Efficiency

現在,你可能對layer實現各種mask。這沒有什么不妥,但是當iOS設備把繪圖從一個地方轉移到另一個地方,設置可能不能迅速的響應這些請求。這類問題很可能出現尤其是當你執行的動畫或當用戶能夠通過觸摸動態繪制,滾動表視圖時。您可以通過肉眼來發現這些問題,你可以通過使用InstrumentsCore Animation template來顯示動畫期間所取得的幀速率來發現這些問題。模擬器的DEBUG菜單也能讓你發現一些顏色疊加導致的繪圖效率問題。

在一般情況下,不透明繪制是最有效的。(非不透明繪制在Instruments上用紅色標記為“blended layers”。)如果一個圖層將始終顯示在單一顏色的背景上,你可以給它設置相同顏色的背景;

另一種方法來獲取效率提升是通過“凍結”繪圖的全部做為位圖。實際上,你先繪制到一個二級緩存,然后把緩存繪制到屏幕。從緩存繪制比直接繪制到屏幕效率低,但是如果layer層次很深很復雜就不用每次都去渲染整棵樹了。要做到這一點,設置圖層的shouldRasterizetrue,設置rasterizationScale一些有意義的值(可能UIScreen.mainScreen().scale)。您可以隨時設置shouldRasterizefalse,來關閉柵格化,所以很容易在一些很混亂的屏幕重排之前開啟然后在關閉它。

此外,還有一個圖層屬性drawsAsynchronously。默認為false。如果設置為true,該層的圖形上下文積累繪圖命令,然后在某個恰當的時刻在后臺線程繪制。因此,您的繪圖命令運行速度非常快,因為他們實際上不是在你發送繪制命令時繪制。我還沒有機會使用這個,但是可能在你需要時間比較長的繪制的時候有效果。

Layers and Key–Value Coding

所有圖層屬性都可以通過具有相同名稱的屬性鍵的鍵值編碼來訪問。因此,為layer添加mask,可以這樣:
layer.mask = mask
也可以這樣:
layer.setValue(mask, forKeyPath: "mask")
此外,CATransform3DCGAffineTransform值可以通過鍵 - 值編碼和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”鍵的值后確定這些層。

另外,CALayerdefaultValueForKey:類方法;實現它,你需要繼承和覆蓋此方法。在需要提供特定鍵一個默認值的情況下,返回默認值,;否則,返回來自調用super的返回值。因此,即使從來沒有顯式提供值給某個特定鍵,它也可以有一個非零值。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容

  • 轉載:http://www.lxweimin.com/p/32fcadd12108 每個UIView有一個伙伴稱為l...
    F麥子閱讀 6,257評論 0 13
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,541評論 6 30
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺iOS動畫全貌。在這里你可以看...
    F麥子閱讀 5,130評論 5 13
  • Core Animation其實是一個令人誤解的命名。你可能認為它只是用來做動畫的,但實際上它是從一個叫做Laye...
    小貓仔閱讀 3,749評論 1 4
  • 城市下著雨,回家的路總是崎嶇。 小時候,很懼怕下雨,不是不喜歡雨,而是不喜歡泥濘。無論多大的風雨,總要一個人去上學...
    孫冬雪silense閱讀 250評論 0 0