layer對象是你進行CoreAnimation操作的核心. layer對象管理著界面中顯示的內容, 并提供機會讓你去修改內容的風格和外觀. 雖然iOS中的view默認有一個layer支持, 但是開發(fā)OS X系統(tǒng)上的APP時需要自己顯示啟用layer來使用它. 當你啟用layer時,你需要了解如何去設置layer, 以便讓layer發(fā)揮應有的功能.
讓你APP使用CoreAnimation
iOS中的App總是開啟了layer支持, 所以CoreAnimation也是開啟的. OS X中的App需要自己通過下面操作來顯示開啟:
- 鏈接到QuartzCore框架(iOS App要鏈接這個框架需要顯示的調用CoreAnimation中的API)
- 如果要讓layer支持NSView, 需要完成如下操作:
- 在nib文件中, 通過View Effects檢測器來開啟layer支持. 在該檢測器中會顯示checkbox來表示是否選擇view或者它的subview. 強烈推薦開啟window中content view的layer支持
- 如果通過代碼創(chuàng)建的view, 你可以調用
setWantsLayer:
(傳YES)方法來開啟layer支持.
使用前面方法開啟layer支持, 會創(chuàng)建layer-backed視圖, 對于layer-backed view系統(tǒng)會負責創(chuàng)建底層layer, 并同步view信息. 在OS X中, 你還可以創(chuàng)建layer-hosting view, 這就意味著APP需要自己負責創(chuàng)建底層支持的layer, 并同步view的信心, 另外在iOS中是無法創(chuàng)建layer-hosting view的, 因為iOS中的view都是layer-backed.
修改和view關聯(lián)的layer對象
layer-backed view會默認創(chuàng)建一個CALayer
實例, 而且大多數(shù)情況下, 都是使用這個類來創(chuàng)建. 但是CoreAnimation提供了其他類型的Layer, 在某些情況下, 你可以發(fā)現(xiàn)某些類型的layer還是蠻有用的, 比如, 當你的view需要展示一張超大的圖片是, 你可以使用CATiledLayer
, 這樣可以提高性能.
修改UIView使用的layer
你可以通過重寫view中的layerClass
類方法來修改view背后layer的類型. iOS中大部分view背后的layer都是CALayer
類型. 大多數(shù)情況下, 使用默認類型的layer是好的, 但是某些情況下, 你需要使用其他類型的layer, 比如下面列舉的幾種情況:
- 如果你使用Metal或OpenGL ES技術來繪制view中的內容, 你需要使用CAMetalLayer和CAEAGLLayer類型的layer對象.
- 有幾種layer是用來改善性能的, 比如
CATiledLayer
- 你需要使用某種layer的特性, 比如粒子發(fā)射器和復制器.
修改view的layer類型很簡單, 如代碼2-1所示. 你只需要重寫layerClass
類方法, 在方法中返回一個你想要的layer類型即可. 該方法會在view顯示之前調用, 來獲取layer的類型, 然后通過該類創(chuàng)建一個新的layer對象. 當layer創(chuàng)建后, view的layer對象就不能被改變了.
代碼清單2-1 設置iOS中view的layer
+ (Class) layerClass {
return [CAMetalLayer class];
}
在OS X中通過layer-hosting技術來修改layer對象
如果你的NSView
對象是一個layer-hosting view. 那么, 需要App自己管理view底層的layer對象. 在你需要控制與view關聯(lián)的layer對象類型的情況下, 你可以使用hosting layer. 比如, 你創(chuàng)建一個layer-hosting視圖, 以便可以分配除默認CALayer
類型外的layer. 你還可能在為了用單個view管理一個獨立的layer樹的情況下使用layer-hosting技術.
當你調用view的setLayer:
方法設置一個layer時, AppKit就不會干涉這個layer了. 正常情況下, AppKit會更新view的layer對象, 但是在layer-hosting情況下, AppKit不會更新layer.
為了創(chuàng)建一個layer-hosting視圖, 你需要在view顯示之創(chuàng)建一個layer, 并將layer和view關聯(lián)起來, 如代碼2-2所示. 另外, 為了設置layer對象, 你必須調用setWantsLayer:
方法, 讓view知道要開啟layer支持了.
代碼清單2-2 創(chuàng)建layer-hosting視圖
// Create myView...
[myView setWantsLayer:YES];
CATiledLayer* hostedLayer = [CATiledLayer layer];
[myView setLayer:hostedLayer];
// Add myView to a view hierarchy.
如果你使用host layer時, 你需要給layer設置好contentScale
, 并在高分辨的屏幕上提供高分辨率的內容.
不同類型的layer擁有不同的特性
CoreAnimation定義了好幾種layer類, 每個類都是為了不同需求而定義的. CALayer
是這些類的root類, 該類定義了多種layer類的共有的特性, 并支持layer的基本特性. 下面列舉了CALayer
幾個子類的特性, 和適用場景, 如表2-1
表2-1 CALayer
子類和使用場景
Class | 使用 |
---|---|
CAEmitterLayer | 用于使用CoreAnimation中粒子發(fā)射器系統(tǒng). emitter layer負責創(chuàng)建粒子和控制這些粒子的origin |
CAGradientLayer | 用于繪制漸變顏色的layer |
CAMetalLayer | 用于使用Metal技術繪制文本內容的layer |
CAEAGLLayer/CAOpenGLLayer | 用于使用OpenGL ES(iOS)或OpenGL(OS X)技術繪制內容的layer |
CAReplicatorLayer | 如果你想自動復制sublayer. replicator為你創(chuàng)建副本,并提供屬性供你修改外觀和其他屬性. |
CAScrollLayer | 用于管理大片可滾動的由大量sublayer組成的區(qū)域 |
CAShapeLayer | 用于繪制三次bezierpath, shape layer對于繪制基于路徑的圖形是有利的. |
CATextLayer | 用于渲染純文本, 或者attributed string |
CATiledLayer | 用于顯示和管理大型圖片, 它會將大圖切割成小圖,然后逐個渲染, 而且支持縮放. 能提高性能 |
CATransformLayer | 用于渲染真正的3D圖形 |
QCCompositionLayer | 用于渲染Quartz Composer中的合成內容(僅OS X) |
設置layer中的內容
layer是管理App內容的數(shù)據(jù)對象. layer的內容由一個bitmap組成, bitmap包含了你想顯示的數(shù)據(jù)信息. 你可以通過下面三種方式來為layer提供bitmap:
- 直接將一個圖像設置到layer的
contents
屬性中. (這種方式適合當layer的內容不變, 或者很少改變情況) - 給layer設置一個delegate, 然后讓delegate去繪制layer中內容. (這種方式適合layer的內容會周期性地改變并且layer的內容可以由外部對象提供, 比如view)
- 定義一個layer子類, 并重寫layer中的繪制方法來自己提供layer的content. (這種方式適合你需要創(chuàng)建一個自定義的layer, 并且你想改變layer的底層繪制行為)
當你自己創(chuàng)建layer對象時, 你唯一需要操心的是為layer提供內容. 但是如果你的App只包含layer-backed視圖時, 這個操心你也省了, 因為UIView會以最好的方式為view關聯(lián)的layer提供內容.
使用一個image來作為layer的content
因為layer對象是一個管理bitmap圖像的容器, 所以你可以直接將一個image對象賦值給layer的contents
屬性. 這種操作比較簡單, 也能讓你直接將image顯示在屏幕上, 而不需要UIImageView來顯示. layer對象會直接使用這個image對象, 而不是創(chuàng)建一個副本image(普通內容話, 最終會創(chuàng)建內容image副本), 這樣也可以節(jié)省內存, 因為App可能在多個地方使用同一個image.
你給layer賦值的image類型必須是CGImageRef
類型. (在 OS X v10.6及以后, 你也可以使用NSImage
來賦值). 在使用image最為layer的content時, 注意圖像的分辨率要和設備屏幕分辨率相匹配. 如果設備屏幕時retina屏幕, 那么你需要調整image的contentsScale
屬性.
使用一個delegate對象來為layer提供內容
如果layer中的內容會經常改變, 那么你可以使用一個delegate對象來提供并且更新layer中的內容. 在準備顯示之前, layer會調用delegate中的方法來獲取所需的內容:
- 如果你的delegate實現(xiàn)了
displayLayer:
方法, 該方法就可以layer創(chuàng)建一個bitmap, 然后將其賦值給layer的contents
屬性. - 如果你的delegate實現(xiàn)了
drawLayer:inContext:
方法, CoreAnimation會創(chuàng)建一個bitmap繪圖上下文, 你需要在該方法中將你想要的內容繪制到bitmap中.
layer的delegate必須實現(xiàn)上面兩個方法中的一個, 如果兩個都實現(xiàn)了的話, layer只會調用displayLayer:
方法.
重寫displayLayer:
方法適用于App偏向創(chuàng)建或加載要顯示的的內容的bitmap. 代碼2-3是displayLayer:
方法的一個實現(xiàn)例子. 在該例中, delegate會使用一個工具類來幫忙加載并且顯示想要的image. delegate根據(jù)內部的一個狀態(tài)displayYesImage
來選擇要展示的image.
代碼清單2-3 直接設置layer的contents
- (void)displayLayer:(CALayer *)theLayer {
// Check the value of some state property
if (self.displayYesImage) {
// Display the Yes image
theLayer.contents = [someHelperObject loadStateYesImage];
}
else {
// Display the No image
theLayer.contents = [someHelperObject loadStateNoImage];
}
}
如果你沒有渲染好的image圖像, 也沒有工具類幫助你創(chuàng)建一個bitmap, 那么delegate可以通過drawLayer:inContext:
自己繪制想要的內容. 代碼2-4是一個drawLayer:inContext:
實現(xiàn)的一個例子. 在該例子中的delegate使用固定的線寬和描邊顏色來繪制一條曲線.
代碼清單2-4 繪制layer中的內容
- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)theContext {
CGMutablePathRef thePath = CGPathCreateMutable();
CGPathMoveToPoint(thePath,NULL,15.0f,15.f);
CGPathAddCurveToPoint(thePath,
NULL,
15.f,250.0f,
295.0f,250.0f,
295.0f,15.0f);
CGContextBeginPath(theContext);
CGContextAddPath(theContext, thePath);
CGContextSetLineWidth(theContext, 5);
CGContextStrokePath(theContext);
// Release the path
CFRelease(thePath);
}
對于自定義內容的layer-backed視圖來說, 需要重寫view的方法來完成繪制. layer-backed視圖會自動將自己設置為它的layer的delegate并實現(xiàn)了delegate方法, 而你需要做的, 就是實現(xiàn)drawRect:
方法, layer的配置你不能動.
在OS X v10.8及以后, 有一種自己繪制的替代方法, 就是重寫view的wantsUpdateLayer
和updateLayer
方法. 在wantsUpdateLayer
中返回YES, 這回告訴NSView使用替代流程. 然后你再實現(xiàn)updateLayer
方法, 在該方法中, 你需要使用bitmap賦值給layer的contents
屬性. 這是AppKit希望直接設置視圖的layer內容的一種情況
通過子類來提供layer的內容
如果打算自定義layer, 那么你可以通過實現(xiàn)layer中的繪制方法來繪制任何你需要的內容. layer通常不會自己生產定制的內容, 但是layer可以管理要顯示的內容. 像CATiledLayer
類就可以將一個大圖分解許多小圖(tile), 然后單獨將這些小圖渲染出來. 因為只有l(wèi)ayer知道這些小圖的渲染需要的信息, 所以layer直接參與管理繪制的過程.
如果你使用繼承的方式, 那么你可以使用技術之一來繪制layer中的內容:
- 重寫layer的
display
方法, 然后直接給屬性contents
賦值. - 重寫layer的
drawInContext:
方法, 使用該方法在繪圖上下文中繪制內容.
根據(jù)需要控制layer繪制內容的程度來選擇上面的兩個方法. display
方法是layer更新內容的主入口, 所以你重寫該方法可以完全控制layer的內容繪制. 另外, 你重寫display
方法的話, 你要負責創(chuàng)建CGImageRef
對象并負責給contents
屬性. 如果你只是向控制layer的內容的繪制操作, 你可以重寫drawInContext:
, 這樣可以讓layer為你創(chuàng)建后備緩存.
對你提供的layer內容進行微調
當你給contents
屬性賦值時, layer會根據(jù)屬性contentsGravity
來控制content來適配layer的bounds的. 默認情況下, 如果圖片大于或小于layer的bounds, layer會縮放圖片來適配當前的bounds. 如果圖片的寬高比和bounds的寬高比不一致的話, 這樣會導致圖片變形. 所以你可以使用contentsGravity
來控制contents在layer中顯示適當.
contentsGravity
的值可以分為兩類:
- 基于位置的gravity常量讓你將圖片對齊layer的bounds區(qū)域內的邊或頂角, 不會進行縮放.
- 基于縮放的gravity常量讓你可以將圖片進行拉伸, 某些配置可以讓拉伸按照寬高比來進行, 也有些配置不會.
圖2-1展示了基于位置的gravity設置圖片的影響. 除了kCAGravityCenter
常量外, 其它常量都是讓圖片緊靠bounds的四邊或四角. kCAGravityCenter
常量讓圖片處于layer的中心位置, 圖片不會拉伸, 如果圖片的size大于layer的bounds, 那么圖片超出bounds的部分將會被剪切; 如果圖片的size小于bounds, 那么圖片外的layer區(qū)域會顯示背景顏色(如果layer設置了背景顏色).
圖2-3展示了縮放gravity設置對layer內容(圖片)的影響. 這些基于縮放的gravity常量會根據(jù)當前圖片是否適配layer的bounds來縮放圖片, 區(qū)別是-這些常量對寬高比的處理方式不一樣, 有些保持原寬高比不變, 有些會改變寬高比. 屬性contentsGravity
的默認值為kCAGravityResize
常量, 這個常量是唯一不會維持圖片的寬高比不變的.
使用高分辨率圖片
layer并不知道設備屏幕分辨率. layer只是保存了一個指向bitmap的指針然后將其用合適的方式將bitmap顯示出來. 如果你給layer的contents
屬性賦值了, 你必須通過layer的屬性contentsScale
來告訴CoreAnimation關于image的分辨率的. layer的contentsScale
屬性的默認值是1.0, 適用在普通屏幕上顯示圖像. 如果你的圖片是retina版本的, 那么該屬性的值應該設置為2.0.
當且僅當你直接給layer設置一個bitmap時, 你才改變contentsScale
的值. 像UIKit和AppKit中的layer-backed視圖, 你是不需要關注contentsScale
的, 因為layer-backed視圖會自動更加屏幕分辨率來給layer設置合適的scale. 比如, 在OS X中, 你將一個NSImage
對象賦值給layer的contents
屬性, AppKit會檢測image的分辨率, 然后使用合適的值來設置layer的contentsScale
.
在OS X中, 基于位置的gravity常量會影響賦值給layer的NSImage
選擇表現(xiàn)的方式. 因為這些常量都不會對image進行縮放, CoreAnimation根據(jù)contentsScale
屬性來選擇合適的像素密度來展示image.
在OS X中, layer的delegate可以通過實現(xiàn)方法layer:shouldInheritContentsScale:fromWindow:
來響應scale因子的改變. 當window的分辨率改變(可能因為window的分辨率在普通和高倍之間切換)時, AppKit會調用這個方法, 在方法中, 如果delegate支持image的分辨率的變化, 你應該返回YES, 然后你還應該更新layer的內容以適配新的分辨率.
調整layer視覺風格和外觀
layer對象內置了許多視覺上的修飾, 比如邊界(border)/背景顏色, 通過修改這些來裝飾layer中的主體內容. 因為這些視覺裝飾部分由layer自己控制, 所以這些效果都是通過layer的屬性來設置的, 你不需要你自己進行繪制, 包括部分動畫. 關于如何使用這些視覺裝飾來影響layer的外觀的詳細知識, 請看后續(xù)系列文章-補充中的Layer Style Property Animations
layer有自己的背景和邊界
layer除了可以展示基于圖像的內容外, 還可以顯示背景和描邊. layer中, 背景在主體內容后面, 但描邊顯示在主體內容前面, 如圖2-3所示. 如果layer包含子layer, 它們也會出現(xiàn)在描邊的下面. 因為背景是在內容的下面, 如果內容有透明部分, 將會透過內容看到背景.
代碼2-5展示如何設置layer的背景和描邊, 下面涉及的屬性都是可動畫的.
代碼清單2-5 設置layer的背景顏色和描邊顏色
myLayer.backgroundColor = [NSColor greenColor].CGColor;
myLayer.borderColor = [NSColor blackColor].CGColor;
myLayer.borderWidth = 3.0;
注意:layer的背景可使用任何類型的顏色, 比如漸變, 圖案等都可以, 在使用圖案最為背景時, 記得CoreAnimation能使用硬件加速能很好的處理背景圖案, 但是要記得CoreAnimation中的坐標系和iOS中默認坐標系不一樣, 可能需要翻轉坐標系.
如果你的layer的背景是透明的顏色, 考慮可以將layer的opaque屬性設置為YES, 這樣做免去layer內容和layer背后的圖片進行合成, 也可以省去layer開啟后備存儲來處理alpha通道(alpha channel). 但是你layer有個非零的圓角,那么layer不能設置為opaque.
layer支持圓角
你可以給layer叫上圓角(corner radius)來創(chuàng)建圓角矩形. 一個圓角是通過在layer的bounds矩形的四個角上加一個一個圓形遮罩(mask), 讓layer四角上的內容透過遮罩顯示, 如圖2-4所示. 因為涉及使用的是透明遮罩, 所以corner radius對layer中的contents
不會影響, 除非你將屬性masksToBounds
設置為YES. 但一定會影響layer的背景和border的繪制.
layer提供
cornerRadius
屬性讓你來設置圓角, 該屬性值為圓角半徑, 單位是點.
layer的內置陰影
類CALayer
包含好幾個屬性用來設置layer的陰影效果. 陰影效果使的layer中的內容好像懸浮界面上其他內容之上, 讓界面看起來又具有深度. 這種效果在某些場景比較有用. 通過layer, 你可以控制陰影的顏色, 便宜位置, 透明度, 和形狀.
默認情況下, 陰影的opacity為0, 完全隱藏的, 所以你需要將印象的opacity設置為非零值. 另外, 陰影是直接位于內容的正下方, 如果想看到陰影, 你需要設置陰影的offset, 這樣才能看到陰影. 你在設置陰影時, 需要記得這兩點. 在設置陰影的offset時, iOS和OS X是有區(qū)別的, 因為坐標系不一樣. 如圖2-5, 同樣的效果, 在iOS中y的值為正, 而在OSX中為負.
當你給layer加上陰影后, 陰影也屬于內容的一部分, 但是陰影的區(qū)域實際上是超出layer的bounds區(qū)域的. 如果masksToBuounds
設置為YES的話, 陰影會被bounds裁剪. 如果你即想要陰影, 又想要bounds masking的話, 你可以使用兩個相同內容的layer疊加在一起, 上面那個開啟masksToBounds
, 下面那個設置陰影. 也可以將第一層添加到第二層中.
關于如何設置shadow的列子, 可以看補充中的Shadow Properties
使用過濾器(filters)向OS X視圖中添加視覺效果
在OS X應用程序中, 你可以直接向layer中的content上應用一個Core Image過濾器. 這樣你可以進行模糊或者銳化layer中的內容, 修改顏色, 扭曲內容等一些操作. 比如, 一個圖片處理進程可能使用過濾器來無損地修改圖像, 而視頻編輯程序可以使用它來實現(xiàn)不同類型視頻切換小姑. 并且由于過濾器會使用硬件加速, 所以渲染時快速且平滑的.
注意:你不能往iOS中的layer對象中添加過濾器
對于給定的layer, 你可以將過濾添加到layer的foreground content和background content中. foreground content包括layer本身所包含的所有內容, 包括屬性contents
中的image, 背景顏色, border, 和sublayer中的內容. background content是直接在layer的下面, 實際上并不是layer本身的一部分. 大多數(shù)layer的background content是其直接super layer的內容, 這些內容部分或者全部地被遮住. 例如, 當你希望用戶應該關注layer的foreground部分時, 你可以將模糊過濾器加入background content中.
你可以通過下面的一些屬性來設置一個CIFilter過濾器:
- 屬性
filters
包含一組過濾器, 加入的是layer的foreground content. - 屬性
backgroundFilters
包含的一組過濾器, 加入是layer的background content中. - 屬性
compositingFilter
中的過濾器定義如何將foreground content和background content組合在一起.
為了往layer中添加過濾, 第一步是創(chuàng)建一個CIFilter
對象, 之后配置該對象. CIFilter
類提供了好幾個類方法用來配置CIFilter
對象, 比如filterWithName:
. 所以創(chuàng)建過濾器對象只是第一步, 好多過濾器需要許多參數(shù)來定義如何修改一個圖像. 比如, 一個box blur過濾器與一個輸入半徑的參數(shù), 定義了模糊的程度. 所以, 添加過濾器之前你總是要配置好這些參數(shù). 而且這些過濾器的有一個共同的參數(shù)不需要你設置, 那就是輸入的image, 這個參數(shù)由layer自己來設置.
所以在添加過濾器之前, 你需要配置好過濾器, 因為, 一旦過濾器添加到layer后, 你是不能通過CIFilter
提供的API來修改過濾器的. 但是, 你可以通過layer的setValue:forkeyPath:
方法來改變過濾器的值.
代碼2-6展示了如何創(chuàng)建一個pinch distortion過濾器并添加到layer對象中. 該過濾可以通過向內捏合使中心點像素失真. 注意, 在這個示例中, 不需要為過濾器指定輸入圖像, 因為layer中的圖像會被自動使用.
代碼清單2-6 往layer中添加過濾器
CIFilter* aFilter = [CIFilter filterWithName:@"CIPinchDistortion"];
[aFilter setValue:[NSNumber numberWithFloat:500.0] forKey:@"inputRadius"];
[aFilter setValue:[NSNumber numberWithFloat:1.25] forKey:@"inputScale"];
[aFilter setValue:[CIVector vectorWithX:250.0 Y:150.0] forKey:@"inputCenter"];
myLayer.filters = [NSArray arrayWithObject:aFilter];
關于更多過濾的使用方法, 請見Core Image Filter Reference
OS X中view的重繪策略會影響性能
在OS X中,layer-backed視圖支持幾個不同的策略來確定何時更新底層layer的內容闸度。因為原生AppKit繪圖模型和CoreAnimation引入的模型之間存在差異莺禁,所以這些策略使得將舊代碼遷移到CoreAnimation上來更加容易哟冬。您可以在逐個視圖的基礎上配置這些策略浩峡,以確保每個視圖的最佳性能。
每個視圖定義了一個layerContentsRedrawPolicy
方法青柄,該方法返回視圖layer的重繪策略致开。使用setLayerContentsRedrawPolicy:
方法設置策略双戳。為了保持與傳統(tǒng)繪圖模型的兼容性飒货,AppKit默認將重繪策略設置為NSViewLayerContentsRedrawDuringViewResize
塘辅。但是扣墩,可以將策略更改為表2-2中的任何值呻惕。請注意亚脆,推薦的重繪策略不是默認策略濒持。
表2-2 layer的重繪策略
策略 | 使用 |
---|---|
NSViewLayerContentsRedrawOnSetNeedsDisplay | 推薦使用,具體見The Layer Redraw Policy for OS X Views Affects Performance |
NSViewLayerContentsRedrawDuringViewResize | 默認,見The Layer Redraw Policy for OS X Views Affects Performance |
NSViewLayerContentsRedrawBeforeViewResize | 見The Layer Redraw Policy for OS X Views Affects Performance |
NSViewLayerContentsRedrawNever | 見The Layer Redraw Policy for OS X Views Affects Performance |
給layer添加自定義屬性
CAAnimation和CALayer這兩個類擴展了KVC(key-value coding)方便支持自定義屬性. 你使用這種特性往layer中添加數(shù)據(jù)并用你定義的key來獲取該數(shù)據(jù). 你也可以將一個動作和自定義屬性關聯(lián)起來, 以便在更改屬性時執(zhí)行相應的動畫. 有關如何設置和獲取自定義屬性的知識, 請參考Key-Value Coding Compliant Container Classes, 有關如何向layer對象添加action的知識, 請參考Changing a Layer’s Default Behavior
打印layer-backed視圖中的內容
在打印期間郁竟,layer根據(jù)需要重新繪制其內容以適應打印環(huán)境棚亩。雖然CoreAnimation通常依賴于緩存的位圖,當渲染到屏幕上時纺阔,它會重畫打印時的內容笛钝。特別地玻靡,如果layer-backed視圖使用drawRect:
方法提供層內容囤捻,那么CoreAnimation在打印期間會再次調用drawRect:
來生成打印的layer內容蝎土。