所有示例代碼均可以在 Animations-Demo 下載到
上節(jié)提到 UIView 上所有動畫歸根結(jié)底都是發(fā)生在Layer 層,所以動畫的學(xué)習(xí)離不開Layer的學(xué)習(xí)。
我們平時開發(fā)中很少使用layer,但是我們卻一直在使用layer。view是不具備繪制能力的,真正繪制的是他的underlying layer 。 每個view都有一個layer屬性。view上顯示相關(guān)的屬性也是layer屬性的一個映射。屏幕顯示的時候 UIView將layer繪制上去。視圖不會被經(jīng)常重繪;相反,它的繪制會被緩存,在可用的地方都會使用緩存版本(bitmap backing store)。緩存的版本,實際上,就是layer。那么view的圖形上下文也就是layer的圖形上下文。
所以深入學(xué)習(xí)layer還是很有必要的,因為它可以完成一些view不能完成的任務(wù)(比如,陰影、圓角、3d變換、透明遮罩、多級非線性動畫、路徑動畫等)。尤其是在動畫方便表現(xiàn)突出。 CALyaer 前面的 "CA" 代表的 " Core Animation "。但是CALayer 并不清楚具體的響應(yīng)鏈(iOS通過視圖層級關(guān)系用來傳送觸摸事件的機(jī)制),于是它并不能夠響應(yīng)事件,即使它提供了一些方法來判斷是否一個觸點在圖層的范圍之內(nèi)。
我們平時使用layer最好是使用view的underlying layer 。 這樣既能享受 UIView的高級api,也能使用到layer的特性。 layer是不支持 AutoLayout 的。我們可以使用AutoLayout 為view布局,那么他的layer的frame會跟隨view frame改變。
layer的幾個基本屬性:
-
contents是一個Any?,但實際上接收一個CGImage對象,如果是其他對象,圖層將是空白。


圖層上將會顯示會對應(yīng)的圖像。
-
contentGravity相當(dāng)于UIView的contentMode屬性 , 有以下值- kCAGravityCenter
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
contentsScale圖的像素尺寸和視圖大小的比例,默認(rèn)情況下它是一個值為1.0的浮點數(shù),應(yīng)該設(shè)置為對應(yīng)的scale
view1.layer.contentsScale = #imageLiteral(resourceName: "rabbit").scale
// or
view1.layer.contentsScale = UIScreen.main.scale
-
maskToBounds相當(dāng)于UIView上clipsToBounds超出部分是否裁剪 -
contentsRect允許我們在圖層邊框里顯示寄宿圖的一個子域。這涉及到圖片是如何顯示和拉伸的.contentsRect不是按點來計算的,它使用了單位坐標(biāo),單位坐標(biāo)指定在0到1之間,是一個相對值。
view1.layer.contentsRect = CGRect(x: 0, y: 0, width: 0.5, height: 0.5)

x方向的 一半 y 方向的一半 相當(dāng)于1/4個兔子。。
可以通過下面的rect分別獲取其他的3/4
CGRect(x: 0.5, y: 0, width: 0.5, height: 0.5)
CGRect(x: 0, y: 0.5, width: 0.5, height: 0.5)
CGRect(x: 0.5, y: 0.5, width: 0.5, height: 0.5)
圖層跟view一樣也有層級樹,可以添加,可以有子layer但是最多只能有一個superlayer。layer使用了和視圖相似的一整套方法來讀取和操縱layer的層次結(jié)構(gòu)。layer有一個superlayer屬性和sublayers屬性,以及下面的方法
addSublayer:insertSublayer:atIndex:-
insertSublayer:below:,insertSublayer:above: replaceSublayer:with:removeFromSuperlayer
不同于視圖的subviews屬性,layer的sublayers屬性是可寫的。你可以通過sublayers屬性一次性給layer設(shè)置多個sublayer。通過設(shè)置sublayers為nil來移除layer的所以子layer。
雖然一個layer的子layer有順序,可以通過上面提到的方法和sublayers屬性來操縱順序,但這并不和繪制的順序完全相同。默認(rèn)情況下,layer有一個CGFloat類型的zPosition屬性值,這也決定了繪制順序。繪制規(guī)則是相同的zPosition的所有子layer在sublayers屬性所列的順序繪制,但較低的zPosition屬性比較高的zPosition屬性的layer先繪制。 (默認(rèn)的zPosition是0.0)
還有一些方法提供了用于在同一layer層次結(jié)構(gòu)內(nèi)各layer的坐標(biāo)系統(tǒng)之間的轉(zhuǎn)換方法:
-
convert:from:,convert:to:
可用來轉(zhuǎn)換 CGPoint 和 CGRect
position 和 anchorPoint
position 對應(yīng)于view的center屬性,anchorPoint相當(dāng)于一個錨點或者移動圖層的一個固定點。anchorPoint用單位坐標(biāo)來描述,也就是圖層的相對坐標(biāo),圖層左上角是{0, 0},右下角是{1, 1},
默認(rèn)來說,anchorPoint 位于圖層的中點,因此默認(rèn)坐標(biāo)是{0.5, 0.5}。所以圖層的將會以這個點為中心放置。但是圖層的anchorPoint可以被移動,比如設(shè)置為(0,0)。
那么圖層就會像右下角移動。

我們將棕色view的anchorPoint設(shè)置為 0,0
view3.layer.anchorPoint = CGPoint.zero

anchorPoint位于圖層的中點,所以圖層的將會以這個點為中心放置
來看一個鐘表的例子 。
我們在界面上放兩個view都是基于AutoLayout布局的。

藍(lán)色view表示表盤,白色表示指針,居中顯示。
然后在代碼中進(jìn)行設(shè)置一下
clockView.backgroundColor = UIColor.clear
clockView.layer.contents = #imageLiteral(resourceName: "clock").cgImage
clockView.layer.contentsScale = #imageLiteral(resourceName: "clock").scale
arrowView.layer.contents = #imageLiteral(resourceName: "arrow").cgImage
arrowView.layer.contentsScale = #imageLiteral(resourceName: "arrow").scale
arrowView.layer.backgroundColor = UIColor.clear.cgColor
arrowView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.9)
let opts: UIViewAnimationOptions = [ .autoreverse , .repeat ]
UIView.animate(withDuration: 1 , delay: 0, options: opts, animations: {
self.arrowView.transform = CGAffineTransform.identity.rotated(by: CGFloat( Double.pi/2 ) )
}, completion: nil)
就會得到如下效果。

Cool~ ! 我們用了很少代碼實現(xiàn)了不錯的效果,view配合layer實現(xiàn)的。
這節(jié)里只談layer不談layer的動畫。
視覺效果
圓角
使用cornerRadius配合masksToBounds可以達(dá)到圓角的效果。這個應(yīng)該都會經(jīng)常用到,會造成離屏渲染。邊框
borderWidth默認(rèn)是0,黑色 ,可以通過borderColor設(shè)置顏色-
陰影
由shadowColor,shadowOpacity,shadowRadius和shadowOffset屬性定義,為使該層繪制陰影,shadowOpacity應(yīng)該設(shè)置為非零值。陰影通常是根據(jù)該層的不透明區(qū)域的形狀繪制,但得到該形狀是cpu密集型的。您可以通過自己定義形狀和把形狀做為CGPath賦值給shadowPath屬性,這會大大提高性能。如果圖層的masksToBounds是true,邊界之外的陰影不會被繪制
給shadowOpacity屬性一個大于默認(rèn)值(也就是0)的值,陰影就可以顯示在任意圖層之下。shadowOpacity是一個必須在0.0(不可見)和1.0(完全不透明)之間的浮點數(shù)。如果設(shè)置為1.0,將會顯示一個有輕微模糊的黑色陰影稍微在圖層之上。
shadowOffset屬性控制著陰影的方向和距離。它是一個CGSize的值,寬度控制這陰影橫向的位移,高度控制著縱向的位移。shadowOffset的默認(rèn)值是 {0, -3},意即陰影相對于Y軸有3個點的向上位移。
view4.backgroundColor = UIColor.white
view4.layer.shadowColor = UIColor.black.cgColor
view4.layer.shadowOffset = CGSize(width: 0, height: 3)
view4.layer.shadowOpacity = 0.6

shadowRadius屬性控制著陰影的模糊度(默認(rèn)值是3),當(dāng)它的值是0的時候,陰影就和視圖一樣有一個非常確定的邊界線。當(dāng)值越來越大的時候,邊界線看上去就會越來越模糊和自然。蘋果自家的應(yīng)用設(shè)計更偏向于自然的陰影,所以一個非零值再合適不過了。
通常來講,如果你想讓視圖或控件非常醒目獨立于背景之外(比如彈出框遮罩層),你就應(yīng)該給shadowRadius設(shè)置一個稍大的值。陰影越模糊,圖層的深度看上去就會更明顯
view4.layer.shadowRadius = 10

實時計算陰影也是一個非常消耗資源的,尤其是圖層有多個子圖層,每個圖層還有一個有透明效果的寄宿圖的時候。如果你事先知道你的陰影形狀會是什么樣子的,你可以通過指定一個shadowPath來提高性能。
view4.layer.shadowPath = // 一個CGPath類型
-
圖層蒙版 mask 屬性 :
CALayer有一個屬性叫做mask, 這個屬性本身就是個CALayer類型,有和其他圖層一樣的繪制和布局屬性。它類似于一個子圖層,相對于父圖層(即擁有該屬性的圖層)布局,但是它卻不是一個普通的子圖層。不同于那些繪制在父圖層中的子圖層,mask圖層定義了父圖層的部分可見區(qū)域。mask圖層的Color屬性是無關(guān)緊要的,真正重要的是圖層的輪廓。mask屬性就像是一個餅干切割機(jī),mask圖層實心的部分會被保留下來,其他的則會被拋棄。
比如我們有這樣一張貓咪圖片

一張星星圖片

我們想讓貓咪顯示星星形狀。
view5.backgroundColor = UIColor.clear
view5.layer.contents = #imageLiteral(resourceName: "cat").cgImage
view5.layer.contentsScale = #imageLiteral(resourceName: "cat").scale
由于要指定mask layer的frame
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let layer = CALayer()
layer.contents = #imageLiteral(resourceName: "star").cgImage
layer.contentsScale = #imageLiteral(resourceName: "star").scale
layer.frame = view5.bounds
view5.layer.mask = layer
}
效果

圖層的Transform
不同于view的transform 圖層是可以做3d變換的。如果只是想執(zhí)行2d的變換可以調(diào)用
layer.setAffineTransform(_:)
傳入CGAffineTransform 參數(shù),和view的變換方式一樣的。如果要有可能用到3d變換就要使用 transform屬性 一個 CAtransform3D對象,也可以進(jìn)行2d變換,指定z為默認(rèn)。
CATransform3DMakeScaleCATransform3DMakeRotationCATransform3DMakeTranslationCATransform3D.init(m11: , m12: , m13: , m14: , m21: , m22: , m23: , m24: , m31: , m32: , m33: , m34: , m41: , m42: , m43: , m44: )
最后一個是他的初始化方法,需要設(shè)置一個4X4的矩陣,如果你的數(shù)學(xué)功底足夠厲害,你可以那么干。
有兩種方式來放置layer在不同的深度。一種是通過它們的位置,就是zPosition屬性。另一種是在z軸上施加一個平移變換來改變layer的位置。layer的position的z分量(zPosition)和在z軸的偏移量這兩個量是相關(guān)的;在某種意義上說,zPosition是在z方向的平移變換的簡寫形式。
在現(xiàn)實世界中,改變一個對象的zPosition會使其顯示更大或更小,因為它和眼睛的距離更近或更遠(yuǎn);但是layer的繪制和真實世界不一樣。這里沒有視角的概念;layer在平面上按照它們真實的大小繪制而且疊在一起沒有間隙。(這就是所謂的正投影,并且藍(lán)圖經(jīng)常以這樣的方式從側(cè)面顯示一個物體)。
CATransform3D的透視效果通過一個矩陣中一個很簡單的元素來控制:m34。m34 用于按比例縮放X和Y的值來計算到底要離視角多遠(yuǎn)。
m34的默認(rèn)值是0,我們可以通過設(shè)置m34為-1.0 / d來應(yīng)用透視效果,d代表了想象中視角相機(jī)和屏幕之間的距離,以像素為單位,那應(yīng)該如何計算這個距離呢?實際上并不需要,大概估算一個就好了。通常500-1000就已經(jīng)很好了
var transform = CATransform3DIdentity
transform.m34 = -1.0 / 500.0
transform = CATransform3DRotate(transform, CGFloat(Double.pi / 4), 0, 1, 0)
view4.layer.transform = transform
對上面帶陰影的layer做了變換后的效果

CALayer有一個屬性 sublayerTransform 他允許對他所有的子圖層做變幻,參考示例: iOS 3D變換 -- CALayer的transform
圖層的 KVC
所有圖層屬性都可以通過具有相同名稱的屬性鍵的鍵值編碼來訪問。因此,為layer添加mask,可以這樣:
layer.mask = mask
也可以這樣:
layer.setValue(mask, forKeyPath: "mask")
當(dāng)然你也可以用swift4 的新語法
layer[keyPath:\CALayer.mask] = layer
此外,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")
transform相關(guān)屬性可以這樣使用
-
rotation.x,rotation.y,rotation.z -
rotation(和rotation.z一樣) -
scale.x,scale.y,scale.z -
translation.x,translate.y,translate.z translation
甚至你可以把CALayer作為一種字典,獲取和設(shè)置任意鍵的值。這意味著你可以將任意信息附加到一個單獨的層實例,并在以后檢索。例如,手動布局layer需要先引用到此layer。那么可以這樣做:
myLayer1.setValue("Foo", forKey: "name")
myLayer2.setVlaue("Foo2", forKey: "name")
圖層沒有一個name屬性;'name'屬性是我附加給layer的?,F(xiàn)在,我可以通過獲取各自的“name”鍵的值后確定這些層。
其他Layer
iOS 系統(tǒng)為我們提供了很多有特殊功能的layer。如CAGradientLayer 可以生成兩種或更多顏色平滑漸變的圖層。用Core Graphics復(fù)制一個CAGradientLayer并將內(nèi)容繪制到一個普通圖層的寄宿圖也是有可能的,但是CAGradientLayer的真正好處在于繪制使用了硬件加速。
如果我們想要實現(xiàn)一個 view 自帶漸變背景,那么我們可以改變view自身的 underlying layer 。
class CustomView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
}
這個默認(rèn)是什么 CALayer 。
那么我們用這個方法就可以實現(xiàn)一個漸變色的view
class CustomView: UIView {
override class var layerClass: AnyClass {
return CAGradientLayer.self
}
override init(frame: CGRect) {
super.init(frame: frame)
prepareView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
prepareView()
}
func prepareView(){
if let gradientLayer = self.layer as? CAGradientLayer{
gradientLayer.colors = [ UIColor.red.cgColor,UIColor.blue.cgColor ]
gradientLayer.startPoint = CGPoint.zero
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
}
}
}
當(dāng)然這個顏色可以為多個通過locations指定每個漸變顏色改變的點(相對坐標(biāo))
gradientLayer.locations = [0,0.5]

CAShapeLayer
這個layer非常厲害,可以使用CGPath 來定義想要繪制的圖形 ,最后CAShapeLayer就自動渲染出來了。當(dāng)然,你也可以用Core Graphics直接向原始的CALyer的內(nèi)容中繪制一個路徑,相比直下,使用CAShapeLayer有以下一些優(yōu)點:
- 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會比用Core Graphics快很多。
- 高效使用內(nèi)存。一個CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個寄宿圖形,所以無論有多大,都不會占用太多的內(nèi)存。
- 不會被圖層邊界剪裁掉。一個CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會像在使用CoreGraphics的普通CALayer一樣被剪裁掉
- 不會出現(xiàn)像素化。當(dāng)你給CAShapeLayer做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化。
用CAShapeLayer 做一些路徑動畫的時候?qū)⒎浅S杏?。目前只看下基本使?/p>
let shapeLayer = CAShapeLayer()
func configShapelayer(){
let rect = view7.bounds
let path = UIBezierPath(ovalIn: rect)
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.frame = view7.bounds
view7.layer.addSublayer(shapeLayer)
}

其他layer
還有一些其他的layer 這里不一一舉例了,感興趣的可以一一查看文檔
-
CATextLayer用來顯示文本 -
CATransformLayer用來做變換 -
CAReplicatorLayer高效生成許多相似的圖層 -
CAScrollLayer可以實現(xiàn)滾動 -
CATiledLayer載入大圖造成的性能問題提供了一個解決方案:將大圖分解成小片然后將他們單獨按需載入 -
CAEmitterLayer高性能的粒子引擎,被用來創(chuàng)建實時例子動畫如:煙霧,火,雨等等這些效果。 -
CAEAGLLayerOpenGL相關(guān)的替代, 還有一個CLKView -
AVPlayerLayerAVPlayerLayer是有別的框架(AVFoundation)提供的,它和Core Animation緊密地結(jié)合在一起,提供了一個CALayer子類來顯示自定義的內(nèi)容類型。AVPlayerLayer是用來在iOS上播放視頻的。他是高級接口例如MPMoivePlayer的底層實現(xiàn)。 如果想要哪個View的背景播放一段視頻。可以考慮使用它。