iOS中的動畫默認是指Core Animation
,當然還有第三方的比如Facebook的Pop
等。Core Animation
是作用在圖層Layer
上的,所以本文分別介紹Layer
和Animation
。
Layer 與 View
在iOS中,每一個UIView
背后都有一個Layer
,這個我們可以通過view.layer
獲得。而View
是Layer
的delegate
。這個delegate
是這樣定義的:
@interface NSObject (CALayerDelegate)
...
/* If defined, called by the default implementation of the
* -actionForKey: method. Should return an object implementating the
* CAAction protocol. May return 'nil' if the delegate doesn't specify
* a behavior for the current event. Returning the null object (i.e.
* '[NSNull null]') explicitly forces no further search. (I.e. the
* +defaultActionForKey: method will not be called.) */
- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
@end
Layer
是真正做顯示和動畫的,而View
是一種高級封裝,并提供用戶交互功能。
**Q: **既然每個View
都有一個Layer
,為什么要分開成兩種對象呢,為什么不把這些功能全部放到View
本身去?
A: 這是蘋果為了跨平臺考慮的。 因為iOS和Mac OS 對用戶的交互處理是有很大區(qū)別的,一個是多點觸控,一個是鍵盤鼠標。而兩者對于界面元素顯示和動畫處理確是相似的。這樣,將Layer
分離出來可以起到職責分離、代碼復(fù)用的作用,同時也方便第三方庫的開發(fā)者。
大家也許注意到了,圖中的Layer
標記為Root Layer
。我們可以把View本身攜帶的既創(chuàng)建View
時創(chuàng)建的Layer
稱為Root Layer
,相反,把那些單獨的Layer
稱為非Root Layer
。
Q: Root Layer 和 非 Root Layer有什么區(qū)別?
A: 改變一個非Root Layer的可做動畫屬性(Animatable Property)時,屬性值從起點到終點有一個平滑過渡的過程,既隱式動畫
,默認時長是0.25秒。而改變一個Root Layer的可做動畫屬性時,是直接改變的,沒有動畫的。我們可以用下面的代碼演示改變兩種layer的顏色。
- (void)changeColor {
[CATransaction begin];
//為了方便觀察,將時長改為2秒
[CATransaction setAnimationDuration:2.0];
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
//改變 非Root Layer的背景色 會有隱式動畫
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//改變 Root Layer的背景色 沒有隱式動畫
self.colorView.layer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
[CATransaction commit];
}
當然,要想讓Root Layer
改變顏色時有動畫也是辦法的,我們只需要把它放在一個block中。
//在block中, 改變 Root Layer的背景色 會有隱式動畫
[UIView animateWithDuration:2.0 animations:^{
self.colorView.layer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
}];
這又是為什么呢?
其實,官方文檔已經(jīng)對此有簡單的說明。
The UIView Class disables layer animation by default but reenables them inside animation blocks.
繼續(xù)死磕,會發(fā)現(xiàn)原因跟上面提到的CALayerDelegate
里面的actionForLayer:forKey
方法有關(guān)。這個方法有三種返回結(jié)果:
- 返回非空值,既某種行為。這樣就是動畫效果。
- 返回nil,不做什么行為,繼續(xù)去其他地方尋找合適的actions。
- 返回Null,停止尋找。
至此,我們知道了根因,就是默認情況下,UIview的actionForLayer:forKey
方法返回nil。而在block中時,返回一個非空值。
Layer Tree
每一個視圖都有一個父視圖以及若干個子視圖,這形成了一個樹狀的層級關(guān)系。對應(yīng)地,每個視圖的圖層也有一個平行的層級關(guān)系,稱之為圖層樹(Layer Tree)
。直接創(chuàng)建的或者通過UIView獲得的(view.layer)用于顯示的圖層樹,稱之為模型樹(Model Tree)
,模型樹的背后還存在兩份圖層樹的拷貝,一個是呈現(xiàn)樹(Presentation Tree)
,一個是渲染樹(Render Tree)
。
模型樹則可以通過modelLayer屬性獲得,而呈現(xiàn)樹可以通過模型樹的layer.presentationLayer獲得。模型樹的屬性值就是我們看到的動畫起始和結(jié)束時的值,是靜態(tài)的;呈現(xiàn)樹的屬性值和動畫運行過程中界面上看到的是一致的,是動態(tài)的。而渲染樹是私有的,你無法訪問到,渲染樹是對呈現(xiàn)樹的數(shù)據(jù)進行渲染,為了不阻塞主線程,渲染的過程是在單獨的進程或線程中進行的,所以你會發(fā)現(xiàn)Animation的動畫并不會阻塞主線程。
Layer Property
Layer有很多屬性,這里強調(diào)兩個屬性:
anchorPoint
錨點是按照layer的bounds
比例取值的,其值是左上角(0,0)到右下角(1,1),默認是(0.5, 0.5)既中心點。形象地,我們可以認為是動畫(平移、縮放、旋轉(zhuǎn))的支點。
下面的動畫演示了使用默認的錨點(0.5,0.5)和(0,1)的區(qū)別。
position
position
有點類似于UIView的center
,但不總是中心點,它是anchorPoint
相對于父layer的位置。所以layer的frame不變時,改變anchorPoint也會改變position的值;同樣,position不變時,改變anchorPoint值也會改變frame的origin的值。
Animations
動畫有隱式動畫
和顯式動畫
之分。前面我們以及介紹了隱式動畫
的原理了,接下來主要講顯式動畫
。
顯式動畫
就是我們在layer上調(diào)用了addAnimation:forKey
方法。這里一旦一個layer添加了一個動畫時,就拷貝了一份Animation對象,所以接下來的對Animation對象的修改只會對后面添加的layer起作用。
動畫的基類是CAAnimation
,它與各派生類的關(guān)系見下圖:
我們看一個簡單的CABasicAnimation的例子,平移一個圓塊。
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"position.x";
animation.fromValue = @0;
animation.toValue = @200;
animation.duration = 1;
[circle.layer animation forKey:@"basic"];
我們發(fā)現(xiàn),動畫結(jié)束后立馬回到原點。 這就牽扯到我們前面提到的Model Tree
和Presentation Tree
了。
圓塊移動過程中,改變的是Presentation Tree
的layer屬性值,而Model Tree
的layer值沒變,動畫結(jié)束時默認是刪除的,所以又變回Model Tree
的layer屬性值了,既回到原點。解決辦法有兩個:
- 動畫結(jié)束后,手動修改Model Tree的layer屬性值
circle.layer.position = CGPointMake(200, 220);
- 動畫結(jié)束時不刪除
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
兩種方案都可以使圓塊保持在最末端,但推薦第一種,因為第二種沒有刪除動畫,會浪費渲染資源,而且也造成Model Tree
與 Presentation Tree
不同步。
時間系統(tǒng)
CAMediaTiming
是一個協(xié)議,控制了動畫運行時間相關(guān)的系數(shù)。CALayer
與CAAnimtion
都實現(xiàn)了這個協(xié)議。它有一些重要的參數(shù)。
- beginTime
動畫開始的延遲時間,相對于父layer的時間。一般取值為
layer.beginTime = CACurrentMediaTime() + 延遲的秒數(shù)
speed
動畫執(zhí)行的速度,有疊加效果。比如layer的速度是2,父layer的速度是2,那這個layer上動畫執(zhí)行的速度就是4。speed還可以是負值,這會導(dǎo)致動畫反向執(zhí)行。repeatCount
動畫執(zhí)行的次數(shù),可以為小數(shù),0.5代表動畫執(zhí)行一般就結(jié)束。
- repeatDuration
動畫重復(fù)的時長,可以比duration小,那就中途結(jié)束。
- timeOffset
可以把動畫時間想象成一個圓環(huán),從中間一個位置開始執(zhí)行,到結(jié)尾再循環(huán)執(zhí)行到剛剛開始的地方。
- autoreverses
為True時,動畫再反向執(zhí)行一遍。
- fillMode
動畫開始之前或者結(jié)束之后的填充行為,默認是kCAFillModeRemoved
。前面用到的kCAFillModeForwards
是動畫結(jié)束之后保持最后狀態(tài),kCAFillModeBackwards
是動畫開始之前就保持最開始的狀態(tài)。
下圖可以清晰地看出各個參數(shù)的含義,動畫演示的是從橘色變成藍色的過程,橫向帶變動畫時刻。
特別指出,這里的speed
為0代表動畫暫停,與timeOffset
一起可以暫停/恢復(fù) 動畫。
- (void)pauseAnimation:(CALayer *)layer {
CFTimeInterval pauseTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed = 0;
layer.timeOffset = pauseTime;
}
- (void)resumeAnimation:(CALayer *)layer {
CFTimeInterval pauseTime = [layer timeOffset];
layer.speed = 1;
layer.timeOffset = 0;
layer.beginTime = 0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pauseTime;
layer.beginTime = timeSincePause;
}
CAMediaTimingFunction
用于計算起點與終點之間的插值,控制動畫的節(jié)奏,基本有四種,起變換節(jié)奏曲線如下圖:
POP
POP是facebook開源獨立于CoreAnimation的動畫方案。與CoreAnimation的區(qū)別主要是:
1.POP 在動畫的任意時刻,可以保持Model Layer與 Presentation Layer同步,CoreAnimation做不到。
- POP可以應(yīng)用于任意NSObject對象,CoreAnimation只能應(yīng)用于CALayer。
基本的POP動畫有
- POPBasicAnimation
用法類似于CABasicAnimation。 - POPSpringAnimation
有彈簧效果,節(jié)奏曲線如下圖:
可以用springSpeed
,springBounciness
等控制彈簧的效果。
POPDecayAnimation
衰減效果,常見于ScrollView滑動時停止的衰減效果。POPCustomAnimation
自定義動畫。
Shimmer
Shimmer也是facebook出品的實現(xiàn)閃動效果的動畫,iPhone滑動解鎖的效果就可以用這個實現(xiàn)。
shimmerView.contentView = shimmerLabel;
shimmerView.shimmeringOpacity = 0.1;
shimmerView.shimmeringAnimationOpacity = 1.0;
shimmerView.shimmeringBeginFadeDuration = 0.3;
shimmerView.shimmering = YES;
其原理也很簡單,就是添加contentView 作為subView, 然后創(chuàng)建一個CAGradientLayer 作為contentView.layer的mask。移動gradientLayer就可以有這個效果。
參考文章
ios core animation advanced techniques
obj.io
controlling-animation-timing
POP 介紹與實踐
談?wù)刬OS Animation