周末第二彈, 繼續(xù)Core Animation的基礎(chǔ)內(nèi)容復(fù)習(xí).
Layer對(duì)象是Core Animation中的核心概念. layer管理著app中的可視化內(nèi)容, 并且為你提供了改變這些內(nèi)容樣式的選擇. iOS app是自動(dòng)支持layer的, OS X的開發(fā)者們則需要手動(dòng)去設(shè)置是否支持layer(默認(rèn)是不支持的). 一旦開始支持layer, 那么你就需要理解如何配置和操作app中的各個(gè)layer以此來(lái)達(dá)到你想得到的效果.
App中支持Core Animation
在iOS app中, Core Animation是自動(dòng)支持的, 并且每一個(gè)view都自帶一個(gè)相對(duì)應(yīng)的layer. 在OS X中, 想要使用Core Animation則需要手動(dòng)去設(shè)置, 步驟如下:
- 添加
QuartzCore框架. - 想要
NSView對(duì)象支持layer需要做以下幾步:-
在nib文件中, 在View Effects inspector中勾選想要支持layer的view.
如果是用代碼創(chuàng)建的view, 調(diào)用view的
setWantsLayer:方法, 傳入YES來(lái)指定view會(huì)使用layer.
-
上面的任何一種方法都會(huì)創(chuàng)建一個(gè)基于layer的view. 之后系統(tǒng)會(huì)自動(dòng)創(chuàng)建layer對(duì)象, 并且保持其更新.
改變View對(duì)應(yīng)的layer對(duì)象
默認(rèn)情況下, 基于layer的view已經(jīng)創(chuàng)建了一個(gè)CALayer類的實(shí)例, 并且在絕大多數(shù)情況下, 你可能并不需要改變已經(jīng)創(chuàng)建好的這個(gè)layer實(shí)例對(duì)象. 然而, Core Animation是提供了不同的layer類供你選擇的, 每種layer類都有自己特殊的能力, 如果你用的到, 你可能就會(huì)改變默認(rèn)情況下創(chuàng)建出來(lái)的layer類. 選擇不同的layer類可以讓你以一種簡(jiǎn)單的方式提高內(nèi)容的展示效果或是支持特定類型的內(nèi)容展示. 比如CATiledLayer類就能夠以一種更有效的方式來(lái)展示大圖.
使用UIView來(lái)改變layer所屬的類
你可以通過(guò)重寫view的layerClass方法, 返回一個(gè)不同的類對(duì)象以此來(lái)改變layer的類型. 絕大數(shù)多iOS中的view都創(chuàng)建了CALayer對(duì)象, 并用其來(lái)存儲(chǔ)自身的內(nèi)容. 對(duì)于你自己創(chuàng)建的view來(lái)說(shuō), 默認(rèn)的選擇就夠了, 你也不需要去修改它. 但是在某些特殊的情景下, 你可能會(huì)發(fā)現(xiàn)其他的layer類會(huì)更加適合當(dāng)前的場(chǎng)景. 比如, 在以下場(chǎng)景下, 你可能就會(huì)想改變默認(rèn)的layer類:
- 你的view使用Metal或是OpenGL ES來(lái)繪圖, 這時(shí)你大概會(huì)使用
CAMetalLayer或CAEAGLLayer對(duì)象. - 有一個(gè)特定的layer類能夠提供更好的表現(xiàn).
改變一個(gè)view的layer所屬的類方法很直接, 代碼如下. 只需要重寫layerClass方法, 并且返回要替換的類就可以了. 在view展示之前, 首先就會(huì)調(diào)用layerClass方法, 使用該方法返回的類來(lái)創(chuàng)建layer對(duì)象, 一旦創(chuàng)建, layer對(duì)象就不能改變.
+ (Class)layerClass {
return [CAMetalLayer class];
}
注: 想要查看有哪些layer類并且如何使用它們, 請(qǐng)看Different Layer Classes Provide Specialized Behaviors
使用NSView來(lái)改變layer所屬的類
通過(guò)重寫NSView對(duì)象的makeBackingLayer方法也可以改變默認(rèn)的layer對(duì)象所屬的類. 在這個(gè)方法的實(shí)現(xiàn)中, 創(chuàng)建和返回你想要AppKit用來(lái)存儲(chǔ)你view的layer對(duì)象. 當(dāng)你想使用一個(gè)可定制的layer時(shí)可能就會(huì)重寫這個(gè)方法.
不同的layer類提供不同的能力
Core Animation定義了許多標(biāo)準(zhǔn)的layer類. 每一個(gè)類都是有其特定的使用場(chǎng)景的. 具體列表如下:
| 類名 | 用法 |
|---|---|
| CAEmitterLayer | 用來(lái)實(shí)現(xiàn)一個(gè)基于Core Animation的粒子發(fā)射系統(tǒng). 發(fā)射layer控制粒子的產(chǎn)生. |
| CAGradientLayer | 用來(lái)繪制一個(gè)顏色梯度, 以此來(lái)填充layer的形狀. |
| CAMetalLayer | 用來(lái)設(shè)置一個(gè)可以繪圖的紋理, 以此使用Metal來(lái)渲染layer上的內(nèi)容. |
| CAEAGLLayer/CAOpenGLLayer | 用來(lái)設(shè)置一個(gè)可以使用OpenGL的layer對(duì)象 |
| CAReplicatorLayer | 當(dāng)你想能夠自動(dòng)復(fù)制子layer的時(shí)候使用該類. |
| CAScrollLayer | 用來(lái)管理由多個(gè)子layer組成的大片可以滾動(dòng)的區(qū)域. |
| CAShapeLayer | 用來(lái)繪制貝塞爾曲線. 優(yōu)點(diǎn)就是可以繪制基于路徑的形狀. |
| CATextLayer | 用來(lái)渲染純文本或者是屬性文本. |
| CATiledLayer | 用來(lái)管理大圖. |
| CATransformLayer | 用來(lái)渲染3D layer層級(jí). |
| QCCompositionLayer | 用來(lái)渲染Quartz Composer composition.(只針對(duì)OS X) |
給layer填充內(nèi)容
layer是數(shù)據(jù)對(duì)象, 其存儲(chǔ)著app想要展現(xiàn)出來(lái)的內(nèi)容. 一個(gè)layer的內(nèi)容是由一個(gè)位圖所構(gòu)成, 位圖中包含著你想要展示的視覺(jué)數(shù)據(jù). 你可以通過(guò)以下三種方式來(lái)為位圖提供內(nèi)容:
- 直接給layer對(duì)象的
contents屬性賦值一個(gè)image對(duì)象. (這種方式適用于layer的內(nèi)容在之后不變, 或是很少改變的情況下使用) - 給layer對(duì)象指定一個(gè)代理對(duì)象, 然后讓代理來(lái)繪制layer的內(nèi)容. (這種方式適用于layer上的內(nèi)容可能會(huì)周期性的改變或是內(nèi)容又外部對(duì)象來(lái)提供, 比如view)
- 自定義一個(gè)layer的子類, 然后重寫它的繪圖方法. (這種方式適用于當(dāng)你必須要自定義layer的子類, 或者你想要改變layer自帶的基本的繪制功能時(shí)使用)
只有當(dāng)你是自己手動(dòng)創(chuàng)建layer對(duì)象的時(shí)候你才需要關(guān)注給layer對(duì)象提供內(nèi)容. 如果你的app只包含基于layer的view, 那你就不需要關(guān)注上面的三種能夠給layer對(duì)象提供內(nèi)容的方法, 因?yàn)橄到y(tǒng)會(huì)以最有效的方式為基于layer的view提供layer所需要的內(nèi)容.
用Image作為layer的內(nèi)容
因?yàn)橐粋€(gè)layer恰恰就是一個(gè)管理位圖的容器, 所以你可以直接把圖片賦值到layer的contents屬性上. 直接賦值圖片到layer上是非常簡(jiǎn)單的, 這樣你可以精確的指定任何圖片. layer對(duì)象會(huì)直接使用你賦值過(guò)來(lái)的圖片, 并且不會(huì)去創(chuàng)建圖片的副本. 當(dāng)你的app在多個(gè)不同的地方使用同一張圖片的時(shí)候, 這樣的處理方式可以有效的節(jié)約內(nèi)存.
賦值到layer上的圖片必須是CGImageRef類型的(OS X v10.6及之后的版本, 也可以賦值NSImage類型的圖片). 當(dāng)賦值圖片的時(shí)候, 需要注意的就是圖片的分辨率要和顯示圖片的設(shè)備的分辨率匹配. 如果是配備了Retina屏幕的設(shè)備, 你可能還需要調(diào)整圖片的contentsScale屬性. 關(guān)于分辨率更多的內(nèi)容, 可以查看Working with High-Resolution Images.
使用代理為layer提供內(nèi)容
如果layer上的內(nèi)容是動(dòng)態(tài)展示的, 即需要經(jīng)常變化的, 這種情況下你就可以使用代理來(lái)為layer提供所需要的內(nèi)容. 在要展示內(nèi)容的時(shí)候, layer就會(huì)呼叫代理讓其來(lái)展示內(nèi)容:
- 如果代理實(shí)現(xiàn)了
displayLayer:方法, 那么就需要在該方法中創(chuàng)建位圖, 并且把位圖賦值到layer的contents屬性上. - 如果代理實(shí)現(xiàn)了
drawLayer:inContext:方法, Core Animation會(huì)創(chuàng)建位圖和一個(gè)圖形上下文, 用來(lái)繪制位圖, 然后會(huì)調(diào)用代理的方法來(lái)填充位圖. 代理方法所需要做的就是在圖形上下文上面繪制內(nèi)容.
代理對(duì)象必須實(shí)現(xiàn)下列方法中的任意一個(gè), displayLayer:或drawLayer:inContext:. 如果兩個(gè)方法全部都實(shí)現(xiàn)了的話, layer也只會(huì)調(diào)用displayLayer:方法.
當(dāng)你的app想載入或是創(chuàng)建它想要展示的內(nèi)容時(shí), 適合重寫displayLayer:方法. 下面的代碼就簡(jiǎn)單的實(shí)現(xiàn)了該方法. 其中代理使用了一個(gè)幫助對(duì)象來(lái)加載和展示它所需要的圖片. 代理方法是基于內(nèi)部狀態(tài)來(lái)選擇到底展示哪張圖片, 這里的內(nèi)部狀態(tài)指的是一個(gè)自定義的屬性displayYesImage.
- (void)displayLayer:(CALayer *)theLayer {
// 檢查內(nèi)部狀態(tài)
if (self.displayYesImage) {
// 展示相應(yīng)的圖片
theLayer.contents = [someHelperObject loadStateYesImage];
}
else {
// 展示其它的圖片
theLayer.contents = [someHelperObject loadStateNoImage];
}
}
如果你還沒(méi)有預(yù)先渲染好的圖片或是一個(gè)幫助對(duì)象來(lái)幫你創(chuàng)建位圖, 你的代理可以動(dòng)態(tài)的使用drawLayer:inContext:方法繪制內(nèi)容. 看不懂就看下面的例子, 代理使用drawLayer:inContext:方法繪制了一條曲線.
- (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);
// 釋放路徑
CFRelease(thePath);
}
對(duì)于擁有自定義的內(nèi)容且基于layer的view來(lái)說(shuō), 你就應(yīng)該重寫view的方法來(lái)進(jìn)行繪制. 基于layer的view會(huì)自動(dòng)成為其代理, 并且實(shí)現(xiàn)所需要的代理方法, 你不應(yīng)該改變這個(gè)配置. 相反, 你應(yīng)該實(shí)現(xiàn)你自己view的drawRect:方法來(lái)繪制你的內(nèi)容.
通過(guò)子類給layer提供內(nèi)容
如果你實(shí)現(xiàn)了一個(gè)自定義的layer類, 那么你就可以重寫繪圖的方法. 對(duì)于一個(gè)layer對(duì)象來(lái)說(shuō), 為自己創(chuàng)建自定義的內(nèi)容是挺不常見(jiàn)的, 但是確實(shí)可以這樣做. 比如, CATiledLayer類可以通過(guò)將大圖分解成小部分然后再進(jìn)行管理和渲染. 因?yàn)橹挥衛(wèi)ayer知道何時(shí)渲染哪個(gè)部分, 所以layer直接管理著繪圖行為.
當(dāng)使用子類的時(shí)候, 可以使用下列方法來(lái)給layer繪制內(nèi)容:
- 重寫layer的
display方法, 用它直接設(shè)置layer的contents屬性. - 重寫layer的
drawInContext:方法, 用它直接在上下文中繪制內(nèi)容.
你要重寫哪個(gè)方法取決于你想如何控制繪制的過(guò)程. display方法是更新layer上內(nèi)容的主要入口, 所以重寫該方法意味著你可以對(duì)繪制過(guò)程進(jìn)行全權(quán)掌控. 并且重寫該方法也意味著你需要?jiǎng)?chuàng)建CGImageRef并且將其賦值給contents屬性. 如果你只是想繪制, 或是只是想讓你的layer管理繪制操作, 你可以重寫drawInContext:方法.
調(diào)整你提供的內(nèi)容
當(dāng)把圖片賦值給layer的contents屬性時(shí), layer的contentsGravity屬性會(huì)決定圖片如何操作以適應(yīng)當(dāng)前的bounds. 默認(rèn)情況下, 如果圖片比現(xiàn)在的bounds大或者小, layer對(duì)象會(huì)相應(yīng)的縮放圖片. 如果layerbounds的寬高比和圖片的寬高比不同, 圖片可能就會(huì)扭曲. 所以你必須使用contentsGravity屬性來(lái)確保你的內(nèi)容能以最佳的方式展現(xiàn).
能賦值給contentsGravity屬性的值分為兩類:
- 基于位置的重力常量允許你在沒(méi)有縮放圖片的情況下可以把圖片擺放在layer所在的矩形區(qū)域里的特定邊或角上.
- 基于尺度的重力常量允許你可以拉伸圖片, 并且可以選擇維持圖片的寬高比或不維持.
下圖展示了基于位置的重力設(shè)置是如何影響圖片的. 除了kCAGravityCenter常量, 剩下的都可以指定圖片到任意邊角. 而kCAGravityCenter會(huì)將圖片相對(duì)于layer居中. 這些選擇都不會(huì)改變圖片的大小, 所以圖片永遠(yuǎn)都是按照它的原始尺寸進(jìn)行渲染. 如果圖片大于layerbounds的大小, 那么多出來(lái)的部分就裁剪掉了, 如果小的話, 就覆蓋不完layer, 如果layer存在背景色的話, 就會(huì)看見(jiàn).

下圖展示了基于尺度的重力設(shè)置是如何影響圖片的. 如果圖片和layer所在的矩形區(qū)域不匹配, 所有的選項(xiàng)都縮放圖片. 選項(xiàng)間的區(qū)別在于圖片的原始寬高比有沒(méi)有改變. 默認(rèn)情況下, layer的contentsGravity屬性是被設(shè)置成kCAGravityResize的, 這也是唯一一個(gè)不保存圖片原始寬高比的一個(gè)選項(xiàng).

調(diào)整layer的視覺(jué)樣式和表現(xiàn)
layer對(duì)象有自己內(nèi)置的視覺(jué)裝飾, 比如邊界和背景色, 你可以用來(lái)填充layer的內(nèi)容. 因?yàn)檫@些視覺(jué)裝飾不需要渲染, 所以在一些情景中可以將layer作為一個(gè)獨(dú)立的實(shí)體來(lái)使用. 你需要做的就是給屬性賦值, 然后layer就會(huì)處理必要的繪制, 包括動(dòng)畫. 關(guān)于layer其他的視覺(jué)裝飾, 可以看Layer Style Property Animations.
layer有自己的背景色和邊界
一個(gè)layer除了其自身上的圖片外, 其還可以填充背景色和邊框. 背景色在圖片的下面, 邊框則在圖片的上面, 如下圖. 如果layer包含子layer, 其也會(huì)顯示在邊框的下面. 因?yàn)楸尘吧秋@示在圖片的下面, 所以背景色會(huì)對(duì)圖片產(chǎn)生一些影響.

給layer設(shè)置背景色和邊框的示例代碼如下:
myLayer.backgroundColor = [NSColor greenColor].CGColor;
myLayer.borderColor = [NSColor blackColor].CGColor;
myLayer.borderWidth = 3.0;
如果你把layer的背景色設(shè)置成了一個(gè)不透明的顏色, 那么最好把layer的opaque屬性值設(shè)置成YES. 這樣做是因?yàn)楫?dāng)把layer顯示到屏幕上時(shí), 可以提高顯示效果. 如果layer有一個(gè)非0的圓角半徑, 那么你也可以不設(shè)置opaque屬性.
layer支持圓角半徑
通過(guò)添加圓角半徑可以給layer創(chuàng)建一個(gè)圓角矩形的效果. 圓角半徑是一個(gè)視覺(jué)裝飾, 它可以掩蓋layer所處的矩形區(qū)域的角, 如下圖所示. 因?yàn)樗婕暗酵该餮谏w, 所以圓角半徑并不會(huì)影響顯示在layer上的圖片, 除非masksToBounds屬性設(shè)置成了YSE. 然而, 圓角半徑總是會(huì)影響layer的背景色和邊框的繪制.

在layer上應(yīng)用圓角半徑的話, 需要指定cornerRadius屬性的值. 你指定的這個(gè)半徑的值是以點(diǎn)為單位計(jì)量的, 并且會(huì)被應(yīng)用到這個(gè)layer的所有4個(gè)角上.
layer支持內(nèi)置的陰影
CALayer類包含了一些屬性來(lái)設(shè)置陰影效果. 陰影效果在一些情景下對(duì)你的app也是十分有必要的, 它也屬于是視覺(jué)裝飾. 通過(guò)layer, 你可以控制陰影的顏色, 相對(duì)于layer上內(nèi)容的位置, 透明度以及形狀.
不透明度的值默認(rèn)是0, 這就是完全看不見(jiàn)陰影效果. 當(dāng)不透明度的值變?yōu)榉?時(shí), Core Animation就會(huì)開始繪制陰影. 因?yàn)槟J(rèn)情況下, 陰影的位置是在layer下面的, 所以在你想看見(jiàn)陰影效果之前, 你需要改變陰影的偏移量. 并且需要牢記的一點(diǎn)是, 偏移量的值是基于layer自身所處的坐標(biāo)系統(tǒng)內(nèi)而言的. 并且在iOS和OS X中是不同的, 下圖展示了下邊緣和右邊緣帶陰影效果的樣子. 在iOS中, 在y軸方向的偏移量需要指定正值; OS X中則是負(fù)值.

當(dāng)給layer添加陰影時(shí), 陰影就是layer內(nèi)容的一部分, 但實(shí)際上它是延伸在layer所處的矩形以外的. 所以, 如果你設(shè)置了layer的masksToBounds屬性, 陰影效果就會(huì)從邊緣被裁剪掉. 而如果你的layer包含了任何透明的內(nèi)容, 那么就會(huì)導(dǎo)致一種奇怪的效果, 即處在layer下面的陰影部分是可以看見(jiàn)的, 但是延伸在外面的卻看不見(jiàn)了. 如果你又想看見(jiàn)陰影, 又想設(shè)置masksToBounds, 你可以使用兩個(gè)layer而不是一個(gè). 把掩蓋加到包含內(nèi)容的layer上, 然后再把這個(gè)layer鑲嵌到第二個(gè)同樣大小并且?guī)в嘘幱靶Ч膌ayer里.
至于陰影如何應(yīng)用到layer上, 看一下Shadow Properties.
給layer添加一個(gè)自定義的屬性
CAAnimation和CALayer這兩個(gè)類支持使用KVC來(lái)設(shè)置自定義的屬性. 你可以通過(guò)KVC給layer添加數(shù)據(jù), 然后通過(guò)特定的鍵來(lái)檢索. 關(guān)于具體如何設(shè)置和獲得自定義的屬性, 請(qǐng)看Key-Value Coding Compliant Container Classes.