動畫由CoreAnimation
框架做爲基礎支持,理解動畫以前要先理解CALayer
這個東西的扮演的角色,瞭解它是負責呈現視覺內容的東西,它有3個圖層樹,還有知道CATransaction
負責對layer的修改的捕獲和提交。html
參考【重讀iOS】認識CALayergit
除了系統實現層面的東西,仍是通用意義上的動畫。動畫就是動起來的畫面,畫面不斷變換產生變化效果。並非真的有一個東西在動,一切都只是對大腦的欺騙。認識到這個,就知道動畫須要:一系列的畫面,這些畫面之間具備相關性。github
因此對於動畫系統而言,它須要:(1)知道變化規律,而後根據這個規律,(2)不斷的去重繪畫面。算法
有了這個認識,再來看最簡單的UIView
的動畫:spring
//建一個button
button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 40)];
button.backgroundColor = [UIColor orangeColor];
[self.view addSubview:button];
......
//一個簡單的移動動畫
[UIView animateWithDuration:3 animations:^{
button.frame = CGRectMake(0, 300, 100, 40);
}];
複製代碼
這是一個移動的動畫,移動是由於frame發生了改變。而後把這個修改放在UIView animateWithDuration:
的block裏。對於系統而言,它有了button開始的位置,block裏有告終束的位置,並且有了時間。bash
一個物體從一個點移動到另外一個點,並且時間已知,那麼就能夠求出在任何一箇中間時間,這個物體的位置。這就是變化規律。而不斷重繪這個就是屏幕的刷新了,這個是操做系統負責了,對於開發者而言,創造不一樣動畫就在於提供不一樣的變化規律。app
UIView的一些動畫方法只是提供了更方便的API,理解了CoreAnimation的動畫,UIView的這些方法都天然清楚了,直接看CoreAnimation吧。框架
這個動畫類的繼承圖,iOS9時又添加了CASpringAnimation
,繼承自CABasicAnimation
。ide
每個動畫類表明了某一種類型的動畫,表明着它們有着不一樣的變化規律。函數
這個是基類,因此它不會有特別有特點的屬性,而是一些通用性的東西。在屬性裏值得注意的是timingFunction
和delegate
。timingFunction
提供了時間的變化函數,能夠理解成時間流速變快或變慢。delegate
就兩個方法,通知你動畫開始了和結束了,沒什麼特別的。
這是一個協議,CAAnimation
實現了這個協議,裏面有一些跟時間相關的屬性挺有用的:
You do not create instances of CAPropertyAnimation: to animate the properties of a Core Animation layer, create instance of the concrete subclasses CABasicAnimation or CAKeyframeAnimation.
這個也仍是一個抽象類,跟UIGestureRecognizer
同樣直接構建對象用不了的。但它的屬性仍是值得解讀一下:
keyPath
並且有一個以keyPath
爲參數的構建方法,因此這個屬性是核心級別。回到動畫的定義上,除了須要變化規律外,還須要變化內容。巧婦難爲無米之炊,動畫是一種連續的變化,那就須要知道是什麼在變化。這裏選取內容的方式就是指定一個屬性,這個屬性是誰的屬性?CALayer
的,動畫是加載在layer上的,layer是動畫的載體。打開CALayer
的文檔,在屬性的註釋裏寫着Animatable
的就是能夠進行動畫的屬性,也就是能夠填入到這個keyPath裏的東西。 之因此是keyPath而不是key,是由於能夠像position.y
這樣使用點語法指定連續一連串的key。從CAPropertyAnimation繼承的動畫,也都是按照這種方式來指定變化內容的。
additive
和cumulative
須要例子纔好證明效果,到下面再說。valueFunction
這個屬性類爲CAValueFunction
,只能經過名稱來構建,甚至沒有數據輸入的地方,也是從這忽然看明白CAPropertyAnimation
構建對象是沒有意義的。由於沒有數據輸入,就沒有動畫,就無法實際應用,這個類只是爲了封裝的須要而建立的。總結一下,動畫須要3個基本要素:內容、時間和變化規律,不一樣的動畫都是在這3者上有差別。
這個類就增長了3個屬性:fromValue
toValue
byValue
。這3個屬性就正好是提供了輸入數據,肯定了開始和結束狀態。
到如今,內容(keyPath)有了,時間(duration和timingFunction)有了,開始和結束狀態有了。經過插值(Interpolates)就能夠獲得任意一個時間點的狀態,而後渲染繪製造成一系列關聯的圖像,造成動畫。
非空屬性 | 開始值 | 結束值 |
---|---|---|
fromValue toValue |
fromValue | toValue |
fromValue byValue |
fromValue | fromValue+byValue |
toValue byValue |
toValue -byValue | toValue |
fromValue | fromValue | currentValue |
toValue | currentValue | toValue |
byValue | currentValue | byValue+currentValue |
上面的表表示的是當3個屬性哪些是非空的時候,動畫是從哪一個值開始、到哪一個值結束。並且上面的狀況優先於下面的狀況。
button.frame = CGRectMake(200, 400, 100, 40);
CABasicAnimation *basicAnim = [CABasicAnimation animationWithKeyPath:@"position"];
//mediaTiming
basicAnim.duration = 1;
basicAnim.repeatCount = 3;
//CAAnimation
basicAnim.removedOnCompletion = NO;
basicAnim.delegate = self;
//property
basicAnim.additive = NO;
basicAnim.cumulative = YES;
//basic
basicAnim.fromValue = [NSValue valueWithCGPoint:CGPointMake(100, 60)];
basicAnim.toValue = [NSValue valueWithCGPoint:CGPointMake(100, 200)];
[button.layer addAnimation:basicAnim forKey:@"move"];
複製代碼
additive
爲true時,變化值總體加上layer的當前值,如button開始位置爲x爲200,fromValue的x爲100,開啓additive
則動畫開始時button的x爲200+100=300,不開啓則100.cumulative
這個指每次的值要加上上一次循環的的結束值。這個就須要repeatCount>1
的時候才能看出效果。好比這裏button第一次動畫結束後位置爲(100, 200),再次開始時位置不是(100, 60),而是加上以前的結束值,即(200,260)。對於不一樣類型的值疊加方式是不一樣的,如矩陣,並非直接單個元素相加,而是使用矩陣加法。
終於到了明星關鍵幀動畫。
關鍵幀動畫,幀指一副畫面,動畫就是一幀幀畫面連續變更而獲得的。而關鍵幀,是特殊的幀,舉個例子,一個物體按照矩形的路線運動,那麼提供4個角的座標就能夠了,其餘位置能夠經過4個角的位置算出來。而關鍵幀就是那些不可缺乏的關鍵的畫面,而其餘幀能夠經過這些關鍵幀推算出來。
因此關鍵幀動畫就是提供若干關鍵的數據,系統經過這些關鍵數據,推算出整個流程,而後完成動畫。
有了這個理解,再看CAKeyframeAnimation
的屬性裏的values
和keyTimes
就好理解了。
values
就是各個關鍵幀的數據,keyTimes
是各個關鍵幀的時間點,並且這兩組數據時一一對應的,第一個value和第一個keyTime都是第一幀畫面的,以此類推。
按照這種思路,其實整個動畫就被切割成n個小階段了,每一個節點有開始和結束數據和時間,就會發現這一小段其實就是一個CABasicAnimation
,而CABasicAnimation
也能夠當作是一個特殊的關鍵幀動畫,只有開始和結束兩個關鍵幀。
因此在使用上和CABasicAnimation
並無特別的地方,只是從傳from、to兩個數據,變成傳一組數據罷了。
屬性path
這個是一種特殊的動畫,若是要實現一個view按照某個路徑進行移動,就使用這個屬性,提供了路徑後,values
屬性會被忽略。路徑能夠經過貝塞爾曲線的類提供:
//內容
CAKeyframeAnimation *keyframeAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
//時間
keyframeAnim.duration = 5;
//變化規律
UIBezierPath *path = [[UIBezierPath alloc] init];
[path addArcWithCenter:CGPointMake(200, 300) radius:100 startAngle:0 endAngle:M_PI*2 clockwise:YES];
keyframeAnim.path = [path CGPath];
複製代碼
若是不提供這個path屬性,那就要咱們提供許多的點來完成動畫,哪怕是簡單的轉圈圈,點數據也超級多,越平滑的動畫就須要越多的點。這個屬性能夠說是爲了這種需求而提供的特殊福利。
屬性calculationMode
這個屬性影響着關鍵幀之間的數據如何進行推算,一個個來講:
kCAAnimationLinear
默認屬性,線性插值。kCAAnimationDiscrete
不進行插值,只顯示關鍵幀的畫面,看到的動畫就是跳躍的kCAAnimationPaced
,這個也是線性插值,但跟第一個的區別是它是總體考慮的。舉個例子,移動一個view,從A到B,再到C,假設A-B之間距離跟B-C之間距離同樣,可是前者的時間是10s,後者是20s,那麼動畫裏,後半段就會跑得慢。而Paced
類型,就忽略掉keyTimes屬性,達到全局勻速的效果,從新計算keyTimes。這個例子裏就變成A-B 15s,B-C也15s。kCAAnimationCubic
這個使用新的插值,算法是Catmull-Rom spline
,效果就是把轉折點變得圓滑。看一下這兩種路徑對比就立馬明白,第一個是線性插值。kCAAnimationCubicPaced
這個就是兩種效果疊加。
屬性rotationMode
這個是配合路徑使用的,在使用路徑動畫時纔有意義。當值爲kCAAnimationRotateAuto
是,會把layer旋轉,使得layer自身的x軸是跟路徑相切的,而且x軸方向跟運動方向一致,使用kCAAnimationRotateAutoReverse
也是相切,但x軸方向跟運動方向相反。
這個看似簡單,用起來卻彷佛有點摸不着頭腦。transition
過渡的意思,這個動畫用來完成layer的兩種狀態之間的過渡。
問題的核心就在這個兩種狀態,查看CATransition
的屬性,發現並無開始狀態、結束狀態之類的輸入。那這兩種狀態怎麼肯定?How does CATransition work?這個問題裏的回答很清楚,截取一段:
The way the CATransition performs this animation to to take a snapshot of the view before the layer properties are changed, and a snapshot of what the view will look like after the layer properties are changed
兩種狀態分別是:layer修改以前和以後。也就是把CATransition
的動畫加到layer上以後,這時會生成一個快照,這個開始狀態;而後你要立馬對layer進行修改,這時layer呈現出另外一種狀態,這是修改後,也就是動畫的結束狀態。這時系統獲得了兩張快照,在這兩張快照之間作過渡效果,就是這個動畫。
因此若是你添加動畫後不作修改,好像看不出什麼效果。
一個例子:
[CATransaction begin];
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];
container.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1];
[self.view addSubview:container];
UILabel *label1 = [[UILabel alloc] initWithFrame:container.bounds];
label1.backgroundColor = [UIColor redColor];
label1.text = @"1";
label1.font = [UIFont boldSystemFontOfSize:30];
label1.textAlignment = NSTextAlignmentCenter;
[container addSubview:label1];
UILabel *label2 = [[UILabel alloc] initWithFrame:container.bounds];
label2.backgroundColor = [UIColor orangeColor];
label2.text = @"2";
label2.font = [UIFont boldSystemFontOfSize:30];
label2.textAlignment = NSTextAlignmentCenter;
[container addSubview:label2];
[CATransaction commit];
CATransition *fade = [[CATransition alloc] init];
fade.duration = 2;
fade.type = kCATransitionPush;
fade.subtype = kCATransitionFromRight;
//位置1
[container.layer addAnimation:fade forKey:nil];
//位置2
[container insertSubview:label2 belowSubview:label1];
複製代碼
一個view上面添加了兩個子view,動畫加載父視圖上,添加動畫後修改子view的上下關係來修改layer的樣式。
爲何要使用[CATransaction begin]
和[CATransaction commit]
把添加子視圖的代碼包起來呢?
這本是一個bug,沒想到倒是一個對CATransaction
理解加深的好例子。緣由簡單說:
container
的layer數據時空的,那麼開始狀態就沒有,因此開始畫面是空白。//位置1
[CATransaction begin];
UIView *container = [[UIView alloc] initWithFrame:CGRectMake(150, 200, 100, 100)];
container.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1];
[self.view addSubview:container];
[CATransaction commit];
//位置5
UILabel *label1 = [[UILabel alloc] initWithFrame:container.bounds];
label1.backgroundColor = [UIColor redColor];
label1.text = @"1";
label1.font = [UIFont boldSystemFontOfSize:30];
label1.textAlignment = NSTextAlignmentCenter;
[container addSubview:label1];
UILabel *label2 = [[UILabel alloc] initWithFrame:container.bounds];
label2.backgroundColor = [UIColor orangeColor];
label2.text = @"2";
label2.font = [UIFont boldSystemFontOfSize:30];
label2.textAlignment = NSTextAlignmentCenter;
[container addSubview:label2];
//位置2
[CATransaction begin];
container.backgroundColor = [UIColor colorWithWhite:0 alpha:1];
[CATransaction commit];
CATransition *fade = [[CATransition alloc] init];
fade.duration = 2;
fade.type = kCATransitionPush;
fade.subtype = kCATransitionFromRight;
//位置3
[container.layer addAnimation:fade forKey:nil];
//位置4
[container insertSubview:label2 belowSubview:label1];
複製代碼
若是作一下簡單的修改:改爲位置1和位置2兩個事務,位置1時container
顏色是灰色,位置2時是黑色。中間label1和label2的處理代碼不加入顯式事務。
結果會怎麼樣?
動畫變成開始畫面是灰色的container
,結束狀態是label1的樣式。
仍是開始狀態的問題,有兩個問題:
中間有一段(位置5)沒有加入顯式事務,那麼它就開啓了隱式事務,它要等到下一次runloop循環才提交,反正是要等到這個方法執行結束。那麼這一段都沒有加入到container
的layer裏,因此不會是label2的樣式。
由於隱式事務開啓了,又尚未結束,因此位置2的事務變成了一個嵌套事務,而嵌套事務我只找到這麼一句話文檔位置:
Only after you commit the changes for the outermost transaction does Core Animation begin the associated animations.
很大的多是,嵌套時,內部的事務提交的東西是提給外層的事務,而後一層層提交,最後一層才把數據提交給CoreAnimation系統,系統這時纔會獲得數據刷新,纔會更新layer的畫面。
因此位置2的事務雖然提交了,可是它仍是等到隱式事務提交才能起做用。把位置5處代碼刪掉就能看出區別。
這個沒什麼可說的,讓多個動畫一塊兒執行,顯示出符合效果。值得注意的是:
Spring是彈簧的意思,這個動畫就是像彈簧同樣擺動的效果。
button.center = CGPointMake(0, 200);
CASpringAnimation *springAnim = [CASpringAnimation animationWithKeyPath:@"position"];
springAnim.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 200)];
springAnim.duration = 10;
springAnim.mass = 10;
springAnim.stiffness = 50;
springAnim.damping = 1;
springAnim.initialVelocity = 0;
springAnim.delegate = self;
[button.layer addAnimation:springAnim forKey:@"spring"];
複製代碼
這個類繼承自CABasicAnimation
,因此仍是須要keyPath、fromValue、toValue等數據。由於keyPath
存在,因此它不僅是用於物體的運動,還能夠是其餘的,好比顏色。CASpringAnimation
提供了像彈簧同樣的變化規律,而不僅是運動的動畫。
而後CASpringAnimation
自身的屬性用於計算彈簧的運動模式:
動畫時間不影響動畫的運行模式,這一點跟其餘的動畫不同,這裏時間到了,物體還在動就會直接掐掉、動畫中止。
CALayer
還有一系列的子類,每種layer還有它們本身特有的動畫。一樣,進文檔查看屬性的註釋,帶有Animatable
的是有動畫的,配合CABasicAnimation
和CAKeyframeAnimation
使用。
CATextLayer
有兩個動畫屬性,fontSize
和foregroundColor
。
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"fontSize"];
anim.duration = 5;
anim.fromValue = @(10);
anim.toValue = @(30);
CATextLayer *textLayer = [[CATextLayer alloc] init];
textLayer.foregroundColor = [UIColor blackColor].CGColor;
textLayer.string = @"一串字符串";
textLayer.frame = CGRectMake(0, 300, 300, 60);
[textLayer addAnimation:anim forKey:@"text"];
[self.view.layer addSublayer:textLayer];
複製代碼
CAShapeLayer
裏有許多動畫屬性,但最神奇的就是strokeStart
和strokeEnd
,特別是兩個組合使用的使用簡直刷新認知!!!
CAShapeLayer
的圖形是靠路徑提供的,而strokeStart
和strokeEnd
這兩個屬性就是用來設定繪製的開始和結束爲止。0表明path的開始位置,1表明path的結束爲止,好比strokeStart
設爲0.5,strokeEnd
設爲1,那麼layer就只繪製path的後半段。
經過修改這兩個屬性,就能夠達到只繪製path一部分的目的,而後它們還都支持動畫,就能夠創造出神奇的效果!
-(void)shaperLayerAnimations{
//圖形開始位置的動畫
CABasicAnimation *startAnim = [CABasicAnimation animationWithKeyPath:@"strokeStart"];
startAnim.duration = 5;
startAnim.fromValue = @(0);
startAnim.toValue = @(0.6);
//圖形結束位置的動畫
CABasicAnimation *endAnim = [CABasicAnimation animationWithKeyPath:@"strokeEnd"];
endAnim.duration = 5;
endAnim.fromValue = @(0.4);
endAnim.toValue = @(1);
//把兩個動畫合併,繪製的區域就會不斷變更
CAAnimationGroup *group = [[CAAnimationGroup alloc] init];
group.animations = @[startAnim, endAnim];
group.duration = 5;
group.autoreverses = YES;
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] init];
shapeLayer.frame = self.view.bounds;
//圖形是一大一小兩個圓相切嵌套
UIBezierPath *path = [[UIBezierPath alloc] init];
[path addArcWithCenter:CGPointMake(100, 300) radius:100 startAngle:0 endAngle:M_PI*2 clockwise:YES];
[path addArcWithCenter:CGPointMake(150, 300) radius:50 startAngle:0 endAngle:M_PI*2 clockwise:YES];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor whiteColor].CGColor;
[shapeLayer addAnimation:group forKey:@"runningLine"];
[self.view.layer addSublayer:shapeLayer];
}
複製代碼
iOS10有了UIViewPropertyAnimator
,能夠控制動畫的流程,核心是fractionComplete
這個參數,能夠指定動畫停留在某個位置。這裏用一個pan手勢來調整fractionComplete
,實現手指滑動時,動畫跟隨執行的效果。
這感受有點像,拖動進度條而後電影前進或後退,隨意控制進度。
UIViewPropertyAnimator *animator;
-(void)interactiveAnimations{
button.frame = CGRectMake(200, 100, 100, 100);
button.layer.cornerRadius = button.bounds.size.width/2;
button.layer.masksToBounds = YES;
animator = [[UIViewPropertyAnimator alloc] initWithDuration:5 curve:(UIViewAnimationCurveEaseOut) animations:^{
button.transform = CGAffineTransformMakeScale(0.1, 0.1);
}];
[animator startAnimation];
[animator pauseAnimation];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)];
[self.view addGestureRecognizer:pan];
}
float startFrac;
-(void)panAction:(UIPanGestureRecognizer *)pan{
if (pan.state == UIGestureRecognizerStateChanged) {
[animator pauseAnimation];
float delta = [pan translationInView:self.view].y / self.view.bounds.size.height;
animator.fractionComplete = startFrac+delta;
}else if (pan.state == UIGestureRecognizerStateBegan){
startFrac = animator.fractionComplete;
}else if (pan.state == UIGestureRecognizerStateEnded){
[animator startAnimation];
}
}
複製代碼
兩種,一個是navigation的push和pop,經過navigationController的delegate提供:
UIViewControllerAnimatedTransitioning
UIViewControllerInteractiveTransitioning
另外一種是VC的present和dismiss,經過VC自身的transitioningDelegate提供:
UIViewControllerAnimatedTransitioning
UIViewControllerInteractiveTransitioning
提供的數據時同樣的類型,因此這兩種其實邏輯上是同樣的。
先看提供動畫的UIViewControllerAnimatedTransitioning
,就兩個方法:
transitionDuration:
讓你提供動畫的時間animateTransition:
在這裏面執行動畫站在設計者的角度來看一下整個流程,這樣會幫助對這個框架的理解:
一切從push開始,nav開始push,它會去查看本身的delegate,有沒有實現提供轉場動畫的方法,沒有就使用默認的效果,結束。
有,那麼就能夠拿到實現UIViewControllerAnimatedTransitioning
的對象,而後從這個對象裏拿到動畫時間,用這個時間去同步處理其餘的操做,好比導航欄的動畫。 同時調用這個對象的animateTransition:
執行咱們提供的動畫。
這個過程瞭解了,就明白每一個類在這個過程裏的意義。由於這些名詞都太長,命名也很像,很容易混淆意義。
一個例子:
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return _duration;
}
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
if (self.type == TransitionTypePush) {
[transitionContext.containerView addSubview:toView];
float scale = 0.7f;
toView.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(toView.bounds.size.width*(1+1/scale)/2, 0), CGAffineTransformMakeScale(scale, scale));
[UIView animateWithDuration:_duration animations:^{
fromView.transform = CGAffineTransformMakeScale(scale, scale);
toView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
fromView.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:YES];
}];
}else if (self.type == TransitionTypePop){
[transitionContext.containerView insertSubview:toView belowSubview:fromView];
float scale = 0.7f;
toView.transform = CGAffineTransformMakeScale(scale, scale);
[UIView animateWithDuration:_duration animations:^{
fromView.transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(toView.bounds.size.width*(1+1/scale)/2, 0), CGAffineTransformMakeScale(scale, scale));
toView.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
[fromView removeFromSuperview];
[transitionContext completeTransition:YES];
}];
}
}
複製代碼
push時的效果是進來的view,即toView從右邊緣一邊進來一邊放大,直到鋪滿屏幕;退出的view,即fromView,逐漸縮小。合在一塊兒有一種滾筒的感受。pop時就是反操做。
除了動畫內容以外,值得注意的是:
toView
須要咱們本身加到containerView
上[transitionContext completeTransition:]
,這個標識這一次的VC切換結束了,不然後面的push、pop等都沒效果。