UIView
有一個名叫 layer
昌阿,類型為 CALayer
的對象屬性山害,它們的行為很相似村生,主要區(qū)別在于:CALayer
繼承自 NSObject
嘉涌,不能夠響應事件妻熊。
這是因為 UIView
除了負責響應事件 ( 繼承自 UIReponder
) 外,它還是一個對 CALayer
的底層封裝仑最∪右郏可以說,它們的相似行為都依賴于 CALayer
的實現(xiàn)警医,UIView
只不過是封裝了它的高級接口而已亿胸。
那 CALayer
是什么呢?
CALayer(圖層)
<u>文檔對它定義是:管理基于圖像內(nèi)容的對象预皇,允許您對該內(nèi)容執(zhí)行動畫侈玄。</u>
概念
<u>圖層通常用于為 view 提供后備存儲,但也可以在沒有 view 的情況下使用以顯示內(nèi)容吟温。圖層的主要工作是管理您提供的可視內(nèi)容序仙,但圖層本身可以設置可視屬性(例如背景顏色、邊框和陰影)鲁豪。除了管理可視內(nèi)容外潘悼,該圖層還維護有關內(nèi)容幾何的信息(例如位置、大小和變換)爬橡,用于在屏幕上顯示該內(nèi)容治唤。</u>
和 UIView 之間的關系
示例1 - CALayer
影響 UIVIew
的變化:
let view = UIView(frame: CGRect(x: 44, y: 44, width: UIScreen.width - 88, height: 300))
view.backgroundColor = .red
view.layer.backgroundColor = UIColor.orange.cgColor
print("view: \(view.backgroundColor!)")
print("layer: \(view.layer.backgroundColor!)")
// Prints "view: 1 0.5 0 1"
// Prints "layer: 1 0.5 0 1"
view.layer.frame.origin.y = 100
print("view: \(view.frame.origin.y)")
print("layer: \(view.layer.frame.origin.y)")
// Prints "view: 100"
// Prints "layer: 100"
可以看到,無論是修改了 layer
的可視內(nèi)容或是幾何信息糙申,view 都會跟著變化宾添,反之也是如此。這就證明:UIView
依賴于 CALayer
得以顯示郭宝。
既然他們的行為如此相似辞槐,為什么不直接用一個 UIView
或 CALayer
處理所有事件呢?主要是基于兩點考慮:
職責不同
UIVIew
的主要職責是負責接收并響應事件粘室;而CALayer
的主要職責是負責顯示 UI榄檬。需要復用
在 macOS 和 App 系統(tǒng)上,NSView
和UIView
雖然行為相似衔统,在實現(xiàn)上卻有著顯著的區(qū)別鹿榜,卻又都依賴于CALayer
海雪。在這種情況下,只能封裝一個CALayer
出來舱殿。
CALayerDelegate
你可以使用 delegate (CALayerDelegate)
對象來提供圖層的內(nèi)容奥裸,處理任何子圖層的布局,并提供自定義操作以響應與圖層相關的更改沪袭。如果圖層是由 UIView
創(chuàng)建的湾宙,則該 UIView
對象通常會自動指定為圖層的委托。
注意:
- 在 iOS 中冈绊,如果圖層與
UIView
對象關聯(lián)侠鳄,則必須將此屬性設置為擁有該圖層的UIView
對象。delegate
只是另一種為圖層提供處理內(nèi)容的方式死宣,并不是唯一的伟恶。UIView
的顯示跟它圖層委托沒有太大關系。
-
func display(_ layer: CALayer)
當圖層標記其內(nèi)容為需要更新 (
setNeedsDisplay()
) 時毅该,調(diào)用此方法博秫。例如,為圖層設置contents
屬性:let delegate = LayerDelegate() lazy var sublayer: CALayer = { let layer = CALayer() layer.delegate = self.delegate return layer }() // 調(diào)用 `sublayer.setNeedsDisplay()` 時眶掌,會調(diào)用 `sublayer.display(_:)`挡育。 class LayerDelegate: NSObject, CALayerDelegate { func display(_ layer: CALayer) { layer.contents = UIImage(named: "rabbit.png")?.cgImage } }
那什么是
contents
呢?contents
被定義為是一個Any
類型畏线,但實際上它只作用于CGImage
静盅。造成這種奇怪的原因是,在 macOS 系統(tǒng)上寝殴,它能接受CGImage
和NSImage
兩種類型的對象蒿叠。你可以把它想象中
UIImageView
中的image
屬性,實際上是蚣常,UIImageView
在內(nèi)部通過轉(zhuǎn)換市咽,將image.cgImage
賦值給了contents
。注意:
如果是 view 的圖層抵蚊,應避免直接設置此屬性的內(nèi)容施绎。視圖和圖層之間的相互作用通常會導致視圖在后續(xù)更新期間替換此屬性的內(nèi)容。
-
func draw(_ layer: CALayer, in ctx: CGContext)
和
display(_:)
一樣贞绳,但是可以使用圖層的CGContext
來實現(xiàn)顯示的過程(官方示例):// sublayer.setNeedsDisplay() class LayerDelegate: NSObject, CALayerDelegate { func draw(_ layer: CALayer, in ctx: CGContext) { ctx.addEllipse(in: ctx.boundingBoxOfClipPath) ctx.strokePath() } }
-
和 view 中
draw(_ rect: CGRect)
的關系文檔對其的解釋大概是:
<u>此方法默認不執(zhí)行任何操作谷醉。使用 Core Graphics 和 UIKit 等技術(shù)繪制視圖內(nèi)容的子類應重寫此方法,并在其中實現(xiàn)其繪圖代碼冈闭。 如果視圖以其他方式設置其內(nèi)容俱尼,則無需覆蓋此方法。 例如萎攒,如果視圖僅顯示背景顏色遇八,或是使用基礎圖層對象直接設置其內(nèi)容等矛绘。</u>
<u>調(diào)用此方法時,在調(diào)用此方法的時候刃永,UIKit 已經(jīng)配置好了繪圖環(huán)境货矮。具體來說,UIKit 創(chuàng)建并配置用于繪制的圖形上下文斯够,并調(diào)整該上下文的變換囚玫,使其原點與視圖邊界矩形的原點匹配《凉妫可以使用
UIGraphicsGetCurrentContext()
函數(shù)獲取對圖形上下文的引用(非強引用)劫灶。</u>那它是如何創(chuàng)建并配置繪圖環(huán)境的?我在調(diào)查它們的關系時發(fā)現(xiàn):
/// 注:此方法默認不執(zhí)行任何操作掖桦,調(diào)用 super.draw(_:) 與否并無影響。 override func draw(_ rect: CGRect) { print(#function) } override func draw(_ layer: CALayer, in ctx: CGContext) { print(#function) } // Prints "draw(_:in:)"
這種情況下供汛,只輸出圖層的委托方法枪汪,而屏幕上沒有任何 view 的畫面顯示。而如果調(diào)用圖層的
super.draw(_:in:)
方法:/// 注:此方法默認不執(zhí)行任何操作怔昨,調(diào)用 super.draw(_:) 與否并無影響雀久。 override func draw(_ rect: CGRect) { print(#function) } override func draw(_ layer: CALayer, in ctx: CGContext) { print(#function) super.draw(layer, in: ctx) } // Prints "draw(_:in:)" // Prints "draw"
屏幕上有 view 的畫面顯示,為什么呢趁舀?首先我們要知道赖捌,在調(diào)用 view 的
draw(_:in:)
時,它需要一個載體/畫板/圖形上下文 (UIGraphicsGetCurrentContext
) 來進行繪制操作矮烹。所以我猜測是越庇,這個UIGraphicsGetCurrentContext
是在圖層的super.draw(_:in:)
方法里面創(chuàng)建和配置的。具體的調(diào)用順序是:
- 首先調(diào)用圖層的
draw(_:in:)
方法奉狈; - 隨后在
super.draw(_:in:)
方法里面創(chuàng)建并配置好繪圖環(huán)境卤唉; - 通過圖層的
super.draw(_:in:)
調(diào)用 view 的draw(_:)
方法。
此外仁期,還有另一種情況是:
override func draw(_ layer: CALayer, in ctx: CGContext) { print(#function) }
只實現(xiàn)一個圖層的
draw(_:in:)
方法桑驱,并且沒有繼續(xù)調(diào)用它的super.draw(_:in:)
來創(chuàng)建繪圖環(huán)境。那在沒有繪圖環(huán)境的時候跛蛋,view 能顯示嗎熬的?答案是可以的!這是因為:view 的顯示不依賴于UIGraphicsGetCurrentContext
赊级,只有在繪制的時候才需要堕阔。 - 首先調(diào)用圖層的
-
和
contents
之間的關系經(jīng)過測試發(fā)現(xiàn)昆婿,調(diào)用 view 中
draw(_ rect: CGRect)
方法的所有繪制操作,都被保存在其圖層的contents
屬性中:// ------ LayerView.swift ------ override func draw(_ rect: CGRect) { UIColor.brown.setFill() // 填充 UIRectFill(rect) UIColor.white.setStroke() // 描邊 let frame = CGRect(x: 20, y: 20, width: 80, height: 80) UIRectFrame(frame) } // ------ ViewController.swift ------ DispatchQueue.main.asyncAfter(deadline: .now() + 2) { print("contents: \(self.layerView.layer.contents)") } // Prints "Optional(<CABackingStore 0x7faf91f06e20 (buffer [480 256] BGRX8888)>)"
這也是為什么要
CALayer
提供繪圖環(huán)境做个、以及在上面介紹contents
這個屬性時需要注意的地方。重要:
如果委托實現(xiàn)了
display(_ :)
小渊,將不會調(diào)用此方法。 -
和
display(_ layer: CALayer)
之間的關系前面說過,view 的
draw(_:)
方法是由它圖層的draw(_:in:)
方法調(diào)用的预烙。但是如果我們實現(xiàn)的是display(_:)
而不是draw(_:in:)
呢?這意味著draw(_:in:)
失去了它的作用道媚,在沒有上下文的支持下扁掸,屏幕上將不會有任何關于 view 的畫面顯示,而display(_:)
也不會自動調(diào)用 view 的draw(_:)
最域,view 的draw(_:)
方法也失去了意義谴分,那display(_ layer: CALayer)
的作用是什么?例如:override func draw(_ rect: CGRect) { print(#function) } override func display(_ layer: CALayer) { print(#function) } // Prints "display"
這里
draw(_:)
沒有被調(diào)用镀脂,屏幕上也沒有相關 view 的顯示牺蹄。也就是說,此時除了在display(_:)
上進行操作外薄翅,已經(jīng)沒有任何相關的地方可以設置圖層的可視內(nèi)容了(參考 "1. func display(_ layer: CALayer)"沙兰,這里不再贅述,當然也可以設置背景顏色等)翘魄。當然鼎天,你可能永遠都不會這么做,除非你創(chuàng)建了一個單獨的圖層暑竟。至于為什么不在
display(_ layer: CALayer)
方法里面調(diào)用它的父類實現(xiàn)斋射,這是因為如果調(diào)用了會崩潰:// unrecognized selector sent to instance 0x7fbcdad03ba0
至于為什么?根據(jù)我的參考資料但荤,他們都沒有在此繼續(xù)調(diào)用
super
(UIView
) 的方法罗岖。我隨意猜測一下是這樣的:首先錯誤提示的意思翻譯過來就是:<u>無法識別的選擇器(方法)發(fā)送到實例</u>。那我們來分析一下腹躁,是哪一個實例中呀闻?是什么方法?
- 是
super
實例潜慎; - 是
display(_ layer: CALayer)
捡多。
也就是說,在調(diào)用
super.display(_ layer: CALayer)
方法的時候铐炫,super
中找不到該方法垒手。為什么呢?請注意UIView
默認已經(jīng)遵循了CALayerDelegate
協(xié)議(右鍵點擊UIView
查看頭文件)倒信,但是應該沒有實現(xiàn)它的display(_:)
方法科贬,而是選擇交給了子類去實現(xiàn)。類似的實現(xiàn)應該是:// 示意 `CALayerDelegate` @objc protocol LayerDelegate: NSObjectProtocol { @objc optional func display() @objc optional func draw() } // 示意 `CALayer` class Layer: NSObject { var delegate: LayerDelegate? } // 示意 `UIView` class BaseView: NSObject, LayerDelegate { let layer = Layer() override init() { super.init() layer.delegate = self } } // 注意:并沒有實現(xiàn)委托的 `display()` 方法。 extension BaseView: LayerDelegate { func draw() {} } // 示意 `UIView` 的子類 class LayerView: BaseView { func display() { // 同樣的代碼在OC上實現(xiàn)沒有問題榜掌。 // 由于Swift是靜態(tài)編譯的關系优妙,它會檢測在 `BaseView` 類中有沒有這個方法, // 如果沒有就會提示編譯錯誤憎账。 super.display() } } // ------ ViewController.swift ------ let layerView = LayerView() // 如果在方法里面調(diào)用了 `super.display()` 將引發(fā)崩潰套硼。 layerView.display() // 正常執(zhí)行 layerView.darw()
- 是
注意:
只有當系統(tǒng)在檢測到 view 的
draw(_:)
方法被實現(xiàn)時,才會自動調(diào)用圖層的display(_:)
或draw(_ rect: CGRect)
方法胞皱。否則就必須通過手動調(diào)用圖層的setNeedsDisplay()
方法來調(diào)用邪意。 -
-
func layerWillDraw(_ layer: CALayer)
在
draw(_ layer: CALayer, in ctx: CGContext)
調(diào)用之前調(diào)用,可以使用此方法配置影響內(nèi)容的任何圖層狀態(tài)(例如contentsFormat
和isOpaque
)反砌。 -
func layoutSublayers(of layer: CALayer)
和
UIView
的layoutSubviews()
類似雾鬼。當發(fā)現(xiàn)邊界發(fā)生變化并且其sublayers
可能需要重新排列時(例如通過frame
改變大小)宴树,將調(diào)用此方法策菜。 -
func action(for layer: CALayer, forKey event: String) -> CAAction?
CALayer
之所以能夠執(zhí)行動畫,是因為它被定義在 Core Animation 框架中酒贬,是 Core Animation 執(zhí)行操作的核心做入。也就是說,CALayer
除了負責顯示內(nèi)容外同衣,還能執(zhí)行動畫(其實是 Core Animation 與硬件之間的操作在執(zhí)行,CALayer
負責存儲操作需要的數(shù)據(jù)壶运,相當于 Model)耐齐。因此,使用CALayer
的大部分屬性都附帶動畫效果蒋情。但是在UIView
中埠况,默認將這個效果給關掉了,可以通過它圖層的委托方法重新開啟 ( 在 view animation block 中也會自動開啟 )棵癣,返回決定它動畫特效的對象辕翰,如果返回的是nil
,將使用默認隱含的動畫特效狈谊。示例 - 使用圖層的委托方法返回一個從左到右移動對象的基本動畫:
final class CustomView: UIView { override func action(for layer: CALayer, forKey event: String) -> CAAction? { guard event == "moveRight" else { return super.action(for: layer, forKey: event) } let animation = CABasicAnimation() animation.valueFunction = CAValueFunction(name: .translateX) animation.fromValue = 1 animation.toValue = 300 animation.duration = 2 return animation } } let view = CustomView(frame: CGRect(x: 44, y: 44, width: UIScreen.width - 88, height: 300)) view.backgroundColor = .orange self.view.addSubview(view) let action = view.layer.action(forKey: "moveRight") action?.run(forKey: "transform", object: view.layer, arguments: nil)
那怎么知道它的哪些屬性是可以附帶動畫的呢喜命?核心動畫編程指南列出了你可能需要考慮設置動畫的
CALayer
屬性:
CALayer 坐標系
CALayer
具有除了 frame
、bounds
之外區(qū)別于 UIView
的其他位置屬性河劝。UIView
使用的所謂 frame
壁榕、bounds
、center
等屬性赎瞎,其實都是從 CALayer
中返回的牌里,而 frame
只是 CALayer
中的一個計算型屬性而已。
這里主要說一下 CALayer
中的 anchorPoint
和 position
這兩個屬性务甥,也是 CALayer
坐標系中的主要依賴:
-
var anchorPoint: CGPoint ( 錨點 )
圖層錨點示意圖 看 iOS 部分即可牡辽≡可以看出,錨點是基于圖層的內(nèi)部坐標态辛,它取值范圍是 (0-1, 0-1) 麸澜,你可以把它想象成是
bounds
的縮放因子。中間的 (0.5, 0.5) 是每個圖層的anchorPoint
默認值因妙;而左上角的 (0.0, 0.0) 被視為是anchorPoint
的起始點痰憎。<u>任何基于圖層的幾何操作都發(fā)生在指定點附近</u>。例如攀涵,將旋轉(zhuǎn)變換應用于具有默認錨點的圖層會導致圍繞其中心旋轉(zhuǎn)铣耘,錨點更改為其他位置將導致圖層圍繞該新點旋轉(zhuǎn)。
錨點影響圖層變換示意圖 -
var position: CGPoint ( 錨點所處的位置 )
錨點影響圖層的位置示意圖 看 iOS 部分即可以故。圖1中的
position
被標記為了(100, 100)
蜗细,怎么來的?對于錨點來說怒详,它在父圖層中有著更詳細的坐標炉媒。對
position
通俗來解釋一下,就是錨點在父圖層中的位置昆烁。一個圖層它的默認錨點是
(0.5, 0.5)
吊骤,既然如此,那就先看下錨點x
在父圖層中的位置静尼,可以看到白粉,從父圖層x
到錨點x
的位置是 100,那么此時的position.x
就是 100鼠渺;而y
也是類似的鸭巴,從父圖層y
到錨點y
的位置也是 100;則可以得出拦盹,此時錨點在父圖層中的坐標是(100, 100)
鹃祖,也就是此時圖層中position
的值。對圖2也是如此普舆,此時的錨點處于起始點位置
(0.0, 0.0)
恬口,從父圖層x
到錨點x
的位置是 40;而從父圖層y
到錨點y
的位置是60
沼侣,由此得出楷兽,此時圖層中position
的值是(40, 60)
。這里其實計算
position
是有公式的华临,根據(jù)圖1可以套用如下公式:-
position.x = frame.origin.x + 0.5 * bounds.size.width
芯杀; -
position.y = frame.origin.y + 0.5 * bounds.size.height
。
因為里面的 0.5 是
anchorPoint
的默認值,更通用的公式應該是:-
position.x = frame.origin.x + anchorPoint.x * bounds.size.width
揭厚; -
position.y = frame.origin.y + anchorPoint.y * bounds.size.height
却特。
注意:
實際上,
position
就是UIView
中的center
筛圆。如果我們修改了圖層的position
裂明,那么 view 的center
會隨之改變,反之也是如此太援。 -
anchorPoint 和 position 之間的關系
前面說過闽晦,position
處于錨點中的位置(相對于父圖層)。這里就有一個問題提岔,那就是仙蛉,既然 position
相對于 anchorPoint
,那如果修改了 anchorPoint
會不會導致 position
的變化碱蒙?結(jié)論是不會:
let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
print(self.redView.layer.position) // Prints "(100.0, 100.0)"
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(self.redView.layer.position) // Prints "(100.0, 100.0)"
那修改了 position
會導致 anchorPoint
的變化嗎荠瘪?結(jié)論是也不會:
let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
print(redView.layer.anchorPoint) // Prints "(0.5, 0.5)"
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(redView.layer.anchorPoint) // Prints "(0.5, 0.5)"
經(jīng)過測試,無論修改了誰另一方都不會受到影響赛惩,受到影響的只會是 frame.origin
哀墓。至于為什么兩者互不影響,我暫時還沒想到喷兼。我隨意猜測一下是這樣的:
其實 anchorPoint
就是 anchorPoint
篮绰;position
就是 position
。他們本身其實是沒有關聯(lián)的季惯,因為它們默認處在的位置正好重疊了吠各,所以就給我們造成了一種誤區(qū),認為 position
就一定是 anchorPoint
所在的那個點星瘾。
和 frame 之間的關系
<u>CALayer
的 frame
在文檔中被描述為是一個計算型屬性,它是從 bounds
惧辈、anchorPoint
和 position
的值中派生出來的琳状。為此屬性指定新值時,圖層會更改其 position
和 bounds
屬性以匹配您指定的矩形盒齿。</u>
那它們是如何決定 frame
的念逞?根據(jù)圖片可以套用如下公式:
-
frame.x = position.x - anchorPoint.x * bounds.size.width
; -
frame.y = position.y - anchorPoint.y * bounds.size.height
边翁。
這就解釋了為什么修改 position
和 anchorPoint
會導致 frame
發(fā)生變化翎承,我們可以測試一下,假設把錨點改為處在左下角 (0.0, 1.0) :
let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(redView.frame.origin) // Prints "(100.0, 20.0)"
用公式來計算就是:frame.x (100) = 100 - 0 * 120
符匾、frame.y (20) = 100 - 1 * 80
叨咖;正好和打印的結(jié)果相符。反之,修改 position
屬性也會導致 frame.origin
發(fā)生如公式般的變化甸各,這里就不再贅述了垛贤。
注意:
如果修改了
frame
的值是會導致position
發(fā)生變化的,因為position
是基于父圖層定義的趣倾;frame
的改變意味著它自身的位置在父圖層中有所改變聘惦,position
也會因此改變。但是修改了
frame
并不會導致anchorPoint
發(fā)生變化儒恋,因為anchorPoint
是基于自身圖層定義的善绎,無論外部怎么變,anchorPoint
都不會跟著變化诫尽。
修改 anchorPoint 所帶來的困惑
對于修改 position
來說其實就是修改它的 "center
" 禀酱,這里很容易理解。但是對于修改 anchorPoint
箱锐,相信很多人都有過同樣的困惑比勉,為什么修改了 anchorPoint
所帶來的變化往往和自己想象中的不太一樣呢?來看一個修改錨點 x
的例子 ( 0.5 → 0.2 ):
仔細觀察一下 "圖2" 就會發(fā)現(xiàn)驹止,不管是新錨點還是舊錨點浩聋,它們在自身圖層中的位置中都沒有變化。既然錨點本身不會變化臊恋,那變化的就只能是 x
了衣洁。x
是如何變化的?從圖片中可以很清楚地看到抖仅,是把新錨點移動到舊錨點的所在位置坊夫。這也是大部分人的誤區(qū),以為修改 0.5 -> 0.2 就是把舊的錨點移動到新錨點的所在位置撤卢,結(jié)果恰恰相反环凿,這就是造成修改 anchorPoint
往往和自己想象中不太一樣的原因。
還有一種比較好理解的方式就是放吩,想象一下智听,假設 "圖1" 中的底部紅色圖層是一張紙,而中間的白點相當于一枚大頭釘固定在它中間渡紫,移動的時候到推,你就按住中間的大頭釘讓其保持不動。這時候假設你要開始移動到任意點了惕澎,那你會怎么做呢莉测?唯一的一種方式就是,<u>移動整個圖層</u>唧喉,讓新的錨點順著舊錨點中的位置靠攏捣卤,最終完全重合忍抽,就算移動完成了。
參考
徹底理解position與anchorPoint
核心動畫編程指南
iOS 核心動畫:高級技巧
蘋果 UIView 文檔
蘋果 CALayer 文檔