筆記主要來源iOS核心動畫高級技巧,感謝作者與翻譯的各位同學.
一、圖層樹
- UIView、NSView都有一個關聯的CALayer,不用CALayer處理所有事情的原因是為了職責分離,在iOS和Mac OS兩個平臺上,事件和用戶交互有很多地方的不同.
二、寄宿圖
-
UIView有個contentMode屬性,經常在它的子類UIImageView上使用,而實際上這個屬性是對應CALayer的contentGravity屬性的,并且CALayer的contentGravity屬性是NSString類型,可選的常量值如下:
* KCAGravityCenter * KCAGravityTop * KCAGravityBottom * KCAGravityLeft * KCAGravityRight * KCAGravityTopRight * KCAGravityBottomLeft * KCAGravityBottomRight * KCAGravityResize * KCAGravityResizeAspect * KCAGravityResizeAepectFill
contentScale屬性定義了寄宿圖的像素尺寸和視圖大小的比例,默認值為1.0,contentScale屬性屬于支持高分辨率屏幕機制的一部分(與圖片的2x,3x使用相對應,iPhone4、5、6系列均為2.0,iPhone6p系列均為3.0).為了使圖片顯示爭取可設置
layer.contentScale = [UISCreen mainScreen].scale;
maskToBounds屬性與UIView的
clipsToBounds
屬性對應,決定是否顯示超出邊界的內容.-
contentsRect屬性CALayer的
contentsRect
屬性允許我們在圖層邊框里顯示寄宿圖的一個子域.和bounds
,frame
不同,contentsRect
不是按點來計算的,它使用了單位坐標,單位坐標指定在0和1之間,是一個相對值.iOS的坐標系統:點 —— 在iOS和Mac OS中最常見的坐標體系。點就是虛擬的像素。也被稱作邏輯像素。在標準設備上,一個點就是一個像素點,但是在retina設備上,一個點等于2*2個像素。
像素——物理像素坐標并不會用來屏幕布局,但是仍然與圖片有相對關系。UIImage是一個屏幕分辨率解決方案,所以指定點來度量大小。但是一些底層的圖片表示如CGImage就會使用像素,所以你要清楚在Retina設備和普通設備上,他們表現出來了不同的大小。
-
單位——對于與圖片或者圖層邊界相關的顯示,單位坐標是一個方便的度量方式,當大小改變的時候,也不需要再次調整。單位坐標在OpenGL這種紋理坐標系統中用得很多,Core Animation中也用到了單位坐標。
?
默認的
contentsRect
是{0,0,1,1},這意味著整個寄宿圖默認都是可見的.詳細的使用參考Layer的寄宿圖contents屬性. contentsCenter屬性.可以用Interface Builder探測窗口控制
contentsCenter
屬性(View里).-
Custom Drawing
-drawRect:
方法沒有默認的實現,如果不需要自定義的繪制,就不要創建這個方法,這會造成CPU資源和內存浪費。雖然-drawRect:
方法是一個UIView方法,事實上都是底層的CALayer安排了重繪工作和保存了因此產生的圖片。CALayer有一個可選的delegate屬性,實現了CALayerDelegate
,當CALayer需要一個內容特定的信息時,就會從協議中請求。當需要被重繪時,CALayer會請求它的代理來給他一個寄宿圖來顯示。它通過調用這個方法做到:-(void)displayLayer:(CALayer *)layer
如果代理不實現
-displayLayer:
方法,CALayer就會轉而嘗試調用下面這個方法:-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
提供了以上方法,可以使用CALayer的
display
去強制layer去繪制。UIView創建了它的宿主圖層時,它就會自動地把圖層的delegate設置為它自己,并提供了一個
-displayLayer:
的實現。所以一般繪制調用UIView的-drawRect:
方法就可以。
三、圖層幾何學
-
布局
UIView的三個屬性
frame
,bounds
,center
對應CALayer的三個屬性frame
,bounds
,postion
。center
和position
都代表了相對于父圖層anchorPoint
所在的位置.視圖的
frame
,bounds
和center
屬性僅僅是存取方法,當操縱視圖的frame
,實際上是在改變視圖下方CALayer
的frame
,不能夠獨立于圖層之外改變視圖的frame
。對于視圖或者圖層來說,
frame
并不是一個非常清晰的屬性,它其實是一個虛擬屬性,是根據bounds
,bounds
和transform
計算而來,所以當其中任何一個值發生改變,frame都會變化。相反,改變frame的值同樣會影響到他們當中的值。 錨點(
anchorPoint
)可以參考這篇文章-
坐標系.
一個圖層的
position
依賴于它父圖層的bounds
,如果父圖層發生了變化,它的所有子圖層也會跟著移動.CALayer
給不同坐標系之間的圖層轉換提供了一些工具類方法:- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer; - (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer; - (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer; - (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;
-
Hit Testing.
CALayer
并不關心任何響應事件,所以不能直接處理觸摸事件或者手勢。但是它有一系列的方法幫你處理事件:-containsPoint:
和-hitTest:
.hitTest:
方法同樣接受一個CGPoint類型參數,而不是BOOL類型,它返回圖層本身,或者包含這個坐標點的葉子點圖層。
需要注意的是,當調用圖層的-hitTest:
方法時,測算的順序嚴格依賴于圖層樹當中的圖層順序(和UIView處理事件類似).zPosition
屬性可以明顯改變屏幕上圖層的順序,但不能改變事件傳遞的順序。
四、視覺效果
圓角.
-
圖層邊框.
邊框是繪制在圖層邊界里面的,而且在所有子內容之前,也在子圖層之前。邊框并會把寄宿圖或者子圖層的形狀計算進來,如果圖層的子圖層超過了邊界,或者是寄宿圖在透明區域有一個透明的蒙版,邊框仍然會沿著圖層的邊界繪制出來。
-
陰影.
shadowOpacity
shadowColor
-
shadowOffest
,該屬性控制著陰影的方向和距離。它是一個CGSize
的值,寬度控制著陰影橫向的位移,高度控制著縱向的位移。shadowOffese
的默認值是{0,-3},即陰影相對于Y軸有3個點的向上位移。 -
shadowRadius
,該屬性控制著陰影的模糊的,當它的值是0的時候,陰影就和視圖一樣有一個非常確定的邊界線。當值越來越大的時候,邊界線看上去就會越來越模糊和自然.(所以不要設置為0)。 -
shadowPath
.
-
圖層蒙版.
mask
圖層的color
屬性是無關緊要的,真正重要的是圖層的輪廓。mask
屬性就像是一個餅干切割機,mask
圖層實心的部分被保留下來,其他的則會被拋棄。 拉伸過濾.
-
組透明.
UIView
的alpha
屬性與CALayer
的opacity
屬性相對應。
五、變換
-
仿射變換.
UIView的
transform
與CALayer的affineTransform
相對應,都是CGAffineTransform
類型,用于在二維空間做旋轉,縮放和平移。CGAffineTransform
是一個可以和二維空間向量做乘法的3*2矩陣。 -
3D變換
CALayer的屬性
transform
是CATransform3D
類型 ,CATransform3D
也是一個矩陣,是一個可以在3維空間內做變換的4*4矩陣。
六、專用圖層
- CAShapeLayer(十分重要)
- CATextLayer
- CAGradientLayer.
CAGradientLayer
是用來生成兩種或更多顏色平滑漸變的。 - CAScrollLayer
- AVPlayerLayer(十分重要).
七、隱式動畫
-
事務
Core Animation基于一個假設,屏幕上的任何東西都可以(或者可能)做動畫。動畫并不需要你在Core Animation中手動打開,相反需要明確的關閉,否則它會一直存在.
事務事件上是Core Animation用來包含一系列屬性動畫集合的機制,任何用指定事務去改變可以做動畫的圖層屬性都不會立刻發送變化。而是當事務一旦提交的時候開始用一個動畫過渡到新值。
事務是通過
CATransaction
類來做管理,CATransaction
沒有屬性或者實例方法,并且也不能用+alloc
和-init
方法創建它。但是可以用+begin
和+commint
分別來入棧和出棧.Core Animation在每個run loop周期中自動開始一次新的事務(run loop是iOS負責收集用戶輸入,處理定時器或者網絡事件并且重新繪制屏幕的東西),即使你不顯式的用
[CATransaction begin]
開始一次事務,任何在一次run loop循環中屬性的改變都會被機種起來,然后做一次0.25秒的動畫。UIView
有兩個方法,+beginAnimations:context:
和+commitAnimations
,和CATransaction
的begin
和commit
方法類似。實際上在+beginAnimations:context
和+commitAnimations
之間所有視圖或者圖層屬性的改變而做的動畫都是由于設置了CATransaction
的原因。在iOS4中,蘋果對UIView添加了一種基于block的動畫:+animateWithDuration:animations:
.實質上它們都是在做同樣的事情。 完成塊
-
圖層行為
Core Animation通常對
CALayer
的所有屬性(可動畫的屬性)做隱式動畫,但UIView把它關聯的圖層的這個屬性關閉了。 -
呈現于模型
CALayer
的屬性行為其實很不正常,因為改變一個圖層的屬性并沒有立即生效,而是通過一段時間漸變更新。當你改變一個圖層的屬性,屬性值的確立刻更新的,但是在屏幕上并沒有馬上發生改變。這是因為你設置的屬性并沒有直接調整圖層的外觀,相反,它只是定義了圖層動畫結束之后將要變化的外觀。
在iOS中,屏幕每秒鐘重繪60次。如果動畫時長比60分之一要長,Core Animation就需要在設置一次新值和新值生效之間,對屏幕上的圖層進行重新組織。這意味著
CALayer
除了“真實”值(就是你設置的值)之外,必須要知道當前顯示在屏幕上的屬性值的記錄。通過
presentationLayer
來獲取屏幕上真正顯示出來的值;而在presentationLayer
上調用-modelLayer
將會返回它正在呈現所依賴的layer
.如果想讓做動畫的圖層響應用戶輸入,可以使用
-hitTest:
方法,來判斷呈現圖層(presentationLayer)是否被觸摸來響應。(具體代碼請參考原書籍)
八、顯式動畫
-
屬性動畫
-
CABasicAnimation
當更新屬性的時候,我們需要設置一個新的事務,并且禁用圖層行為。否則動畫會發生兩次,一個是因為顯式的CABasicAnimation,另一次是因為隱式動畫。(在設置屬性的時候)
通過
keyPath
、fromValue
、toValue
設置動畫,fromValue
可以不設置.byValue
是一個相對值,會從當前的值上動畫到byValue
的值。 -
CAKeyframeAnimation(關鍵幀動畫)
屬性動畫通過設置
keyPath
和values
屬性得以實現關鍵幀動畫;還可以通過設置CGPath
來實現一個路徑相關的動畫,另外,設置rotationMode
屬性為KCAAnimationRotateAuto
,圖層將會根據曲線的切線自動旋轉。 -
虛擬屬性
transform.rotation
transform.postion
transform.scale
keyPath
可以設置為以上三個虛擬屬性,然后再通過byValue
、toValue
來設置需要動畫的值。
-
CAAnimationGroup(動畫組)
-
過渡
最常見到的過渡就是在childViewController之間切換view.
為了創建一個過渡動畫,將使用
CATransiton
,同樣是另一個CAAnimation
的子類,和別的子類不同,CATransiton
有一個Type
和subtype
來標識變換效果.type
屬性是一個NSString
類型,可以被設置成以下值:KCATransitionFade KCATransitionMoveIn KCATransitionPush KCATransitionReveal
隱式過渡.當設置
CALayer
的content
屬性的時候,CATransition
的確是默認的行為。但是對于視圖關聯的圖層,或者其他隱式動畫的行為,這個特性依然是被禁用的。 -
在動畫過程中取消動畫
為了終止一個指定的動畫,你可以用如下方法把它從圖層移除掉:
-(void)removeAnimationForKey:(NSString *)key;
或者移除所有的動畫:
-(void)removeAllAnimations;
動畫一旦被移除,圖層的外觀就立刻更新到當先的模型圖層的值。一般來說,動畫在結束之后被自動移除,除非設置
removeAllAnimation
為NO
,如果你設置動畫在結束之后不被自動移除,那么當它不需要的時候你要手動移除它;否則它會一直存在于內存中,直到圖層被銷毀。
九、圖層時間
-
CAMediaTiming協議
CAMediaTiming
協議定義了在一段動畫內用來控制逝去時間的屬性的集合,CALayer
和CAAnimation
都實現了這個協議,所以時間可以被任意基于一個圖層或者一段動畫的類控制。-
持續和重復
duration
是一個CFTimeInterval
類型,對將要進行的動畫的一次迭代指定了時間;repeatCount
代表動畫重復的迭代次數。例如,duration
是2,repeatCount
是3.5,那么完整的動畫時長將是7秒。 -
相對時間
beginTime
指定動畫開始之前的延遲時間。這里的延遲是從動畫添加到可見圖層的那一刻開始測量,默認是0(就是說動畫會立刻執行)。speed
是一個時間的倍數,默認1.0,減少它會減慢圖層/動畫的時間,增加它會加快速度。如果speed
為2.0,那么對于一個duration
為1的動畫,實際上在0.5秒的時候就已經完成了。timeOffset
讓動畫快進到某一點,例如,對于一個持續1秒的動畫來說,設置timeOffset
為0.5意味著動畫將從一般的地方開始。需要注意的是,和beginTime
不同的是,timeOffset
并不受speed
的影響。所以如果你把speed
設置為2.0,把timeOffset
設置0.5,那么你的動畫將從動畫最后結束的地方開始,因為1秒的動畫實際上被縮短到了0.5秒。然而即使使用了timeOffset
讓動畫從結束的地方開始,它仍然播放了一個完整的時長,這個動畫僅僅是循環了一圈,然后從頭開始播放。 -
fillMode
fillModel
是一個NSString
類型,可以接受如下四種常量:KCAFillModeForwards KCAFillModeBackwards KCAFillModeBoth KCAFillModeRemoved
默認是
KCAFillModeRemoved
,當動畫不再播放的時候就顯示圖層模型指定的值,剩下的三種類型,向前、向后或者即向前又向后去填充動畫狀態,使得動畫在開始前或者結束后仍然保持開始和結束那一刻的值。這就對避免對動畫結束的時候急速返回提供了另一種方案。但是,當它來解決這個問題的時候,需要把
removeOnCompletion
設置為NO
,另外需要給動畫添加一個非空的鍵,于是可以在不需要動畫的時候把它從圖層上移除。
-
-
層級關系時間
每個動畫和圖層在時間上都有它自己的層級概念,相對于它的父親來測量,對圖層調整時間將會影響到它本身和子圖層的動畫,但不會影響到父圖層。
對
CALayer
或者CAGroupAnimation
調整duration
和repeatCount
、repeatDuration
屬性并不會影響到子動畫。但是beginTime
、timeOffset
和speed
屬相將會影響到子動畫。CoreAnimation有一個全局時間的概念—馬赫時間。馬赫時間在設備上所有進程都是全局的,但是在不同的設備上并不是全局的(手機休眠時,馬赫時間會暫停),但是比較兩次馬赫時間差很有價值。訪問馬赫時間:
CFTimeInterval time = CACurrentMediaTime();
-
手動動畫
timeOffset
一個很有用的功能在于它可以讓你手動控制動畫進程,通過設置speed
為0,可以禁用動畫的自動播放,然后用timeOffset
來來回顯示動畫序列。這可以使得運用手勢來控制動畫變得很簡單。 -
總結
十、緩沖
-
動畫速度
使用緩沖方程式需要設置
CAAnimation
的timingFunction
屬性,timingFunction
是CAMediaTimingFunction
類的一個對象;如果想改變隱式動畫的計時函數,同樣也可以使用CATransaction
的+setAnimationTimingFunctions:
方法。創建
CAMediaTimingFunction
的最簡單的方式是調用+timingFuctionWithName:
的構造方法。傳入的常量如下:KCAMediaTimingFunctionLinear KCAMediaTimingFunctionEaseIn KCAMediaTimingFunctionEaseOut KCAMediaTimingFunctionEaseInEaseOut KCAMediaTimingFunctionDefault
自定義緩沖函數
十一、基于定時器的動畫
-
定時幀
iOS按照每秒60次刷新屏幕,用定時器1/60秒去更新view的屬性便可以實現定時幀動畫.
-
NSTimer
iOS上的每個線程都管理了一個
NSRunloop
,字面上看就是通過一個循環來完成一些任務列表。但是對主線程,這些任務包含如下幾項:- 處理觸摸事件
- 發送和接受網絡數據包
- 執行使用GCD的代碼
- 處理計時器的行為
- 屏幕重繪
當設置一個
NSTimer
,他會被插入到當前任務列表中,然后知道指定的時間過去之后才會被執行。但是何時啟動定時器并沒有一個時間上限,而且它只會在列表中上一個任務完成之后開始執行。這通常會有幾毫秒的延遲,但是如果上一個任務過了很久才完成就會導致延遲很長一段時間。如何精確動畫:
- 可以用
CADisplayLink
讓更新頻率嚴格控制在屏幕刷新之后. - 基于真實幀的持續時間而不是假設的更新頻率來做動畫(通過兩次馬赫時間差來取動畫幀).
- 調整動畫計時器的
runloop
模式,這樣就不會被別的事件干擾.
-
CADisplayLink
CADisplayLink
是CoreAnimation提供的類似NSTimer的類,它總是在屏幕完成一次更新之前啟動。CADisplayLink
有一個整型的frameInterval
屬性,指定了間隔多少幀之后才執行。默認值是1,意味著每次屏幕更新之前都會執行一次。但是如果動畫的代碼執行起來超過了六十分之一秒,你可以指定frameInterval
為2,就是說動畫每隔一幀執行一次。 -
Run Loop模式
常見的run loop模式:
-
NSDefaultRunLoopModel
- 標準優先級 -
NSRunLoopCommonModes
- 高優先級 -
UITrackingRunLoopMode
- 用于UISCrollView
和別的控件的動畫
可以對
CADisplayLink
指定多個run loop模式,于是我們可以同時加入NSDefaultRunLoopMode
和UITrackingRunLoopMode
來保證它不會被滑動打斷,也不會被其他UIKit控件動畫影響性能,像這樣:self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
NSTimer
同樣:self.timer = [NSTimer timerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
-
-
十二、性能調優
CPU VS GPU
-
測量,而不是猜測
可以在程序中用
CADisplayLink
來測量幀率(用兩次馬赫時間差的倒數即為幀率) -
Instruments
- Time Profiler
- Leaks
- Core Animation
- Allocations
- GPU Driver
十三、高效繪圖
-
軟件繪圖
在iOS中,軟件繪圖通常是由Core Graphics框架完成。但是在一些必要的情況下,相比Core Animation和OpenGL, Core Graphics要慢不少。
一旦實現了
CALayerDelegate
協議中的-drawLayer:inContext:
方法或者UIView
中的-drawRect:
方法(其實就是前者的包裝方法),圖層就創建了一個繪制上下文,這個上下文需要的大小的內存可從這個算式得出:圖層寬圖層高4字節,寬高的單位均為像素。對于一個在Retina iPad上的全屏圖層來說,這個內存量就是 2048*1526*4字節,相當于12MB內存,圖層每次重繪的時候都需要重新抹掉內存然后重新分配(所以一般還是不要重寫view的-drawRect:
方法)。 -
矢量圖形
在某些情況下,需要使用Core Graphics來繪圖:
- 任意多邊形(不僅僅是一個矩形)
- 斜線或曲線
- 文本
- 漸變
但Core Animation實際上為這樣的繪制提供了專門的類,如
CAShapeLayer
繪制多邊形、直線和曲線,CATextLayer
繪制文本,CAGradientLayer
用來繪制漸變。 臟矩形
異步繪制
十四、圖像IO
- 加載和潛伏
-
+imageNamed:
方法會解壓圖片,并緩存圖片,但是+imageNamed:
只對應用資源束中的圖片有效,所以對用戶生成的圖片或者下載的圖片就沒法使用了。 -
+imageWithContentsOfFile:
加載大圖片比較耗時,并且不會解壓圖片。如果要實現主線程快速加載圖片,需要在后臺線程加載圖片數據并強制解壓。 - 第三方庫
SDWebImage
實現了圖片的下載、解壓、緩存,可以多學習。
-
- 緩存
- NSCache
-
-setCountLimit:
,設置緩存數量。 -
-setObject:forKey:cost:
,對每個存儲的對象指定消耗的值來提供一些暗示。 -
-setTotalCostLimit:
,設置全體緩存的尺寸。 -
NSCache
在系統低內存的時候自動丟棄存儲的對象。
-
- NSCache
- 文件格式
十五、圖層性能
隱式繪制
-
離屏渲染
當圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制時,屏幕外渲染就被喚起了。屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文中被渲染(不論CPU還是GPU)。圖層的以下屬性將會觸發屏幕外繪制:
- 圓角(當和
maskToBounds
一起使用時) - 圖層蒙板
- 陰影
對于那些需要動畫而且要在屏幕外渲染的圖層來說,你可以用
CAShapeLayer
,contentsCenter
或者shadowPath
來獲得同樣的表現而且較少地影響到性能。 - 圓角(當和
-
混合和過渡繪制
透明、半透明顏色的View,會增加GPU的計算,降低性能。
shouldRasterize
屬性的使用。 減少圖層數量