聲明
該篇文章的內容參考自 iOS核心動畫高級技巧 一文,非常感謝其作者和中文版的作者,讓我能夠相對系統的學習 CoreAnimation 的知識,我受益匪淺,再次感謝。
如果有興趣的小夥伴可以訪問其網站,詳細的,完整的學習 CoreAnimation。
CAAnimation 篇
CAAnimation 是一個抽象動畫類。 遵循着 CAMediaTiming 和 CAAciotn 兩個協議。 要爲 Core Animation 圖層或 Scene Kit 對象設置動畫,請創建其子類 CABasicAnimation,CAKeyframeAnimation,CAAnimationGroup 或 CATransition 的實例。Core Animation 可以用在 Mac OS X 和 iOS 平臺。Core Animation 的動畫執行過程都是在後臺操作的,不會阻塞主線程。
隱式動畫
當你改變 CALayer 的一個可做動畫的屬性,它並不會立刻在屏幕上呈現出來,而是從先前的值平滑過渡到新值。典型的例子就是改變圖層的背景填充色。
示例:
假如我們現在有一個圖層,那我們在點擊屏幕時嘗試去改變此圖層的背景填充色。
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 生成隨機顏色
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
}
我們可以看到,但我們點擊屏幕以改變圖層的背景時,視圖從舊的背景逐漸地過度到了新值。在這過程中,我們沒有做其他額外的操作,這種自行完成的平滑過渡動畫就是隱式動畫。
事務
那麼這一過程時如何完成的呢?實際上動畫是由當前事務來完成的,事務是什麼?事務是 Core Animation 用來包含一系列屬性動畫集合的機制,你可以設置動畫的執行時間等,這些動畫的圖層屬性新值的設置都不會立刻發生變化,而是當事務提交時由 run loop 自動開始。
事務是通過 CATransaction 類來管理。該類沒有屬性或者實例方法,因此你不能創建它,但是你可以通過 +begin
和 commit
來將當前屬性設置分別進行入棧和出棧操作。
任何可以做動畫的圖層屬性都會添加到棧頂的事務,你可以通過 +setAnimationDuration:
方法來設置當前事務的動畫時間,如果不進行設置,默認的時間是0.25s。
我們現在使用事物來完成上一個例子中的動畫,並將動畫時間延長。
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 開始一個動畫事務
[CATransaction begin];
// 設置動畫的執行時間
[CATransaction setAnimationDuration:1.0];
// 生成顏色,作爲動畫的變化新值
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
// 提交動畫事務
[CATransaction commit];
}
我們可以看到圖層的動畫效果依舊沒變,但是漸變的時間明顯變長了很多。從代碼上看,我們僅僅將圖層需要改變的屬性加到 +begin
和 commit
之間,併爲此事務設置了一個時間。
如果你使用過 UIView 的動畫,那麼應該使用過 +beginAnimations:context:
和 +commitAnimations
,實際上這兩個都是對 CATransaction
的封裝,其所做動畫都是由 CATransaction
完成的。
完成回調
CATranscation
的 API 除了提供設置動畫時間 +setAnimationDuration:
還提供了動畫完成的回調方法:+ setCompletionBlock:
。你可以在該方法中接着完成一些事情。
修改一下代碼:
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 開始一個動畫事務
[CATransaction begin];
// 設置動畫的執行時間
[CATransaction setAnimationDuration:1.0];
// 生成顏色,作爲動畫的變化新值
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
// 提交動畫事務
[CATransaction commit];
// 動畫完成回調,可以寫在 commit 後面
[CATransaction setCompletionBlock:^{
self.colorLayer.affineTransform = CGAffineTransformRotate(self.colorLayer.affineTransform, M_PI_4);
}];
}
圖層動畫的過程
當我們給對 CALayer 的屬性設新值時,圖層經過以下幾個過程來檢測應該如何呈現新值。
- 圖層首先檢測它是否有委託者,並且是否實現了協議
CALayerDelegate
中的方法-actionForLayer:forKey:
,如果有,直接調用並返回結果。 - 如果沒有委託者,或者委託沒有實現上述方法,圖層會檢查屬性
actions
字典,試圖找到對應的屬性名。 - 如果依舊沒有,圖層還是檢查屬性
style
字典,再次嘗試搜索對應的屬性名。 - 最後,如果都未能找到,那麼圖層直接會調用默認的行爲
defaultActionForKey:
方法來展現對應屬性的新值。
那麼,既然我們知道了圖層的行爲過程,我們是否可以以此做些什麼?實際上,我們可以參與圖層的行爲過程來改變隱式動畫的行爲。
我們首先通過圖層的委託代理完成新的動畫過程。
@interface ViewController ()< CALayerDelegate >
@property (strong, nonatomic)CALayer* colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
// 設置圖層的委託代理
self.colorLayer.delegate = self;
}
// 完成圖層行爲協議
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event{
// 設置新的動畫
CATransition *transition = [CATransition animation];
transition.type = kCATransitionReveal;
transition.subtype = kCATransitionFromLeft;
return transition;
}
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 生成隨機顏色.
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
}
我們也可以通過 actions
字典來完成:
@interface ViewController ()< CALayerDelegate >
@property (strong, nonatomic)CALayer* colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
// 設置 actions 字典
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
transition.duration = 1.0; // 動畫時間設置稍長
self.colorLayer.actions = @{@"backgroundColor": transition};
}
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 生成隨機顏色
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
}
顯式動畫
和隱式動畫相對的,顯式動畫一般是開發者們主動去實現的動畫效果,完成圖層從舊狀態到新狀態到過渡切換。和隱式動畫不不同,顯式動畫需要開發者關心動畫從產生到消失的每一個細節,如變化的狀態、執行的時長、動畫的次數等等,相比系統提供的簡單的過渡動畫效果,顯式動畫可以完成圖層的各種各樣的酷炫效果。
屬性動畫
顧名思義,屬性動畫(CAPropertyAnimation)類的是針對圖層的一些可作動畫的屬性而言的,該類不能直接拿來使用,開發中通常使用其子類(這一點類似手勢),如:CABasicAnimation 經典動畫、CAKeyframeAnimation 關鍵幀動畫、CASpringAnimation 彈性動畫,基礎動畫的子類。
- CABasicAnimation
CABasicAnimation 動畫,需要我們爲其提供兩個狀態值,一個是初始狀態值,一個是終止狀態值。一般來說,初始值都是圖層最初的狀態,當然,你也可以指定從初始狀態到非終止狀態的之間的任意時刻。
示例:
我們接着上面的例子,將圖層的圓角值做一些改變。
@interface ViewController ()
@property (strong, nonatomic)CALayer* colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
}
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 修改圓角屬性
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"cornerRadius"];
animation.toValue = @(self.colorLayer.bounds.size.height/2.0);
animation.duration = 2;
animation.autoreverses = YES; // 執行逆動畫
[self.colorLayer addAnimation:animation forKey:@"cornerRadius_animation"];
}
注:animationWithKeyPath:
所帶的字符串表示需要修改的 layer 可動畫的屬性,不是隨便寫的字符串,一般常用的可動畫屬性如下:
key | 說明 | 使用樣例 |
---|---|---|
transform.scale | 縮放 | @(0.5) |
transform.scale.x | 寬的比例 | @(0.5) |
transform.scale.y | 高的比例 | @(0.5) |
opacity | 透明度 | @(0.5) |
cornerRadius | 圓角的設置 | @(50) |
transform.rotation.x | 圍繞x軸旋轉 | @(M_PI) |
transform.rotation.y | 圍繞y軸旋轉 | @(M_PI) |
transform.rotation.z | 圍繞z軸旋轉 | @(M_PI) |
strokeStart | 結合CAShapeLayer使用 | 賦值多變 |
strokeEnd | 結合CAShapeLayer使用 | 賦值都變 |
bounds | 大小,中心不變 | [NSValue valueWithCGRect:CGRectMake(0, 0, 100, 100)]; |
position | 位置(中心點的改變) | [NSValue valueWithCGPoint:CGPointMake(100, 100)]; |
contents | 內容, | 比如UIImageView的圖片 imageAnima.toValue = (id)[UIImage imageNamed:@”imageName”].CGImage; |
…… |
動畫開始和完成事件
和隱式動畫中的完成回調不同,CAAnimation 採用了委託模式,因此你如果需要處理動畫的開始和完成事件時,你需要完成 CAAnimationDelegate 的代理方法:
- (void)animationDidStart:(CAAnimation *)anim;
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;
其中 flag 標識了動畫是否是正常結束。另外,事件傳遞不是完成block塊,而是採用委託模式會帶來一個問題,就是你有多個動畫時,你需要判斷當前是那個圖層的動畫事件。
這裏提供兩個用來區別的方案。一種就是在添加動畫時,-addAnimation:forKey:
設置每個動畫對應不同的key值,然後通過 animationKeys
獲取到圖層上所有的動畫key,然後對每個圖層循環所有建,通過 -animationForKey:
找到結果。顯然這種是非常的麻煩的方式。好在 CAAnimation 實現了 KVC 協議,我們可以像使用字典一樣,隨意的存取屬性。
示例:
我們將圖層在完成動畫之後,進行背景色的更改。
@interface ViewController ()<CAAnimationDelegate>
@property (strong, nonatomic)CALayer* colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
}
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"cornerRadius"];
animation.toValue = @(self.colorLayer.bounds.size.height/2.0);
animation.duration = 2;
animation.autoreverses = YES; // 執行逆動畫
animation.delegate = self;
// 將視圖附加到動畫上
[animation setValue:self.colorLayer forKey:@"colorLayer"];
[self.colorLayer addAnimation:animation forKey:@"cornerRadius_animation"];
}
// 動畫結束事件
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
// 通過key值,取回附加的視圖
CALayer* layer = [anim valueForKey:@"colorLayer"];
layer.backgroundColor = UIColor.redColor.CGColor;
}
- CAKeyframeAnimation
相比於經典動畫關注於起始和終止的狀態值,關鍵幀動畫更注重整個動畫過程中多個關鍵點的狀態,因此關鍵幀動畫需要一連串的值來做動畫,你甚至可以說,經典動畫是關鍵動畫的一種。
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:@"backgroundColor"];
animation.duration = 4;
animation.values = @[
(__bridge id)UIColor.blueColor.CGColor,
(__bridge id)UIColor.redColor.CGColor,
(__bridge id)UIColor.yellowColor.CGColor,
(__bridge id)UIColor.greenColor.CGColor,
(__bridge id)UIColor.blueColor.CGColor
];
[self.colorLayer addAnimation:animation forKey:nil];
}
上述例子演示了給予關鍵幀動畫的關鍵位置的數組值,實際上,關鍵幀還可以是無數個位置,如果此時的動畫屬性是針對位置一類的,我們就可以將這些關鍵幀看作是路徑,這就演變出了另一種方式做動畫,即 path
。
下面通過移動圖層來演示這種方式的關鍵幀動畫:
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.duration = 4.0;
// 關鍵幀路徑
animation.path = self.path.CGPath;
[self.imageLayer addAnimation:animation forKey:nil];
}
我們的飛船可以沿着關鍵路徑進行移動,但是我們發現飛船的方向一直是橫向的,就如最初設置的方向,而不是指向曲線切線的方向。好在蘋果發現了這一點,並且給 CAKeyFrameAnimation 添加了一個 rotationMode 的屬性,設置它爲常量 kCAAnimationRotateAuto,圖層將會根據曲線的切線自動旋轉。
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CAKeyframeAnimation* animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
animation.duration = 4.0;
animation.path = self.path.CGPath;
animation.rotationMode = kCAAnimationRotateAuto;
[self.imageLayer addAnimation:animation forKey:nil];
}
- CASpringAnimation
CABasicAnimation 動畫的子類,可以實現彈性動畫。這個動畫是在 iOS9 之後纔出現的,UIView 有其對應的動畫塊。
CASpringAnimation 通過幾個物理相關屬性來計算出圖層執行的動畫效果。這些屬性如下:
mass
:質量,影響慣性、拉伸幅度
stiffness
:剛度係數,剛度係數越大,形變產生的力就越大,運動越快
damping
:阻尼係數,阻止彈簧伸縮的係數,阻尼係數越大,停止越快
initialVelocity
:初始速率
settlingDuration
:(只讀)結算時間,根據當前的動畫參數估算彈簧動畫到停止時的時間
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CASpringAnimation *animation = [CASpringAnimation animationWithKeyPath:@"position.y"];
animation.damping = 5; // 阻尼係數
animation.stiffness = 100; // 剛度係數
animation.mass = 1; // 質量
animation.initialVelocity = 0; // 初始速率
animation.duration = animation.settlingDuration; //結束時間
animation.fromValue = @(self.subLayer.position.y);
animation.toValue = @(self.subLayer.position.y+100);
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
[self.subLayer addAnimation:animation forKey:nil];
}
注:動畫並沒會改變圖層和視圖的 frame,因此在執行完動畫後,都默認被重置到最初的位置。如果你需要動畫執行完之後保持當前的位置狀態,可以設置 removedOnCompletion
爲 NO,並設置 fillMode
模式爲 kCAFillModeForwards
。
動畫組
之前提到的幾個屬性動畫,都僅僅是作用於單一屬性,但是如果我們需要幾個動畫一起作用到圖層上,該怎麼辦呢?蘋果爲我們提供了一個組合動畫 CAAnimationGroup
,他是另一個繼承 CAAnimation
的子類,和幾個屬性動畫的父類同級,它只有一個屬性 animations
數組,就是用來存放多個動畫的。
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 動畫1
CABasicAnimation* animation1 = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
animation1.toValue = @(0.5);
// 動畫2
CABasicAnimation* animation2 = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
animation2.toValue = @(M_PI*2);
// 動畫組
CAAnimationGroup* group = [CAAnimationGroup animation];
// 設置所有的動畫的執行時間
group.duration = 1.5;
group.removedOnCompletion = NO;
group.fillMode = kCAFillModeForwards;
// 將所有動畫都添加到組中
group.animations = @[animation1,animation2];
[self.subLayer addAnimation:group forKey:nil];
}
需要注意的是,動畫組的動畫時間取所有動畫最短時間,超出時間的部分會立刻被停止,因此使用動畫組的時候,最好是一些動畫時間統一的組合,比如上面例子中,動畫時間並非由某個動畫來決定,而是由動畫組來設置。
過渡動畫
屬性動畫只會對圖層的一些可動畫的屬性起到作用,當我們想要改變一個不能動畫的屬性(比如圖片),或者從層級關係中添加或者移除圖層(過場效果),屬性動畫將不起作用。因此,蘋果又提供了一個用來做過渡動畫的類 CATransition
,注意這個類和上面提到的事物 CATransaction
不是同一個東西。
CATransition
是 CAAnimation 的子類,它由兩個過渡類型來控制變換效果,一個是 type
:用來控制過渡效果,一個是 subtype
:用來控制過渡的方向。
type
的幾種類型:
kCATransitionFade // 默認,漸變消失
kCATransitionMoveIn // 從當前圖層上面劃入
kCATransitionPush // 當前圖層被推出,用新值替換
kCATransitionReveal // 從當前圖層上面劃出,效果和 kCATransitionMoveIn 相反
除了系統開放出來的四種類型,還有幾種私有API,可以通過字符串來設置:
cube //立方體翻滾效果
oglFlip //上下左右翻轉效果
suckEffect //收縮效果,如一塊布被抽走(不支持過渡方向)
rippleEffect //滴水效果(不支持過渡方向)
pageCurl //向上翻頁效果
pageUnCurl //向下翻頁效果
cameraIrisHollowOpen //相機鏡頭打開效果(不支持過渡方向)
cameraIrisHollowClose //相機鏡頭關上效果(不支持過渡方向)
subtype
的幾種類型:
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
示例:
我們來使用過渡動畫來切換幾張圖片
// 獲取隨機整數
#define randomFromAtoB(A,B) (int)(A+(arc4random()%(B-A+1)))
@interface ViewController ()
{
NSInteger currentIndex;
}
@property (strong, nonatomic) CALayer* subLayer;
@property (strong,nonatomic) NSArray *images; // 圖片數組
@property (strong,nonatomic) NSArray *animations; // 動畫類型數組
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.view.layer addSublayer:self.subLayer];
}
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CATransition *transition = [CATransition new];
// 設置動畫類型,注意對於蘋果官方沒公開的動畫類型只能使用字符串,並沒有對應的常量定義
transition.type = self.animations[randomFromAtoB(0, self.animations.count-1)];
// 設置子類型,方向
transition.subtype = @[kCATransitionFromRight,
kCATransitionFromLeft,
kCATransitionFromTop,
kCATransitionFromBottom][randomFromAtoB(0, 3)];
// 設置動畫時間
transition.duration = 1.0;
// 添加新的視圖
currentIndex = (currentIndex+1)%self.images.count;
NSString *imageName = self.images[currentIndex];
self.subLayer.contents = (__bridge id)[UIImage imageNamed:imageName].CGImage;
[self.subLayer addAnimation:transition forKey:@"KCATransitionAnimation"];
}
-(CALayer *)subLayer{
if (_subLayer==nil) {
_subLayer = [CALayer new];
_subLayer.frame = self.view.bounds;
_subLayer.backgroundColor = UIColor.blueColor.CGColor;
_subLayer.contents = (__bridge id)[UIImage imageNamed:@"0.jpg"].CGImage;
}
return _subLayer;
}
-(NSArray *)animations{
if (_animations == nil) {
_animations = @[@"fade", // 淡出效果
@"movein", // 新視圖移動到舊視圖
@"push", // 新視圖推出到舊視圖
@"reveal", // 移開舊視圖現實新視圖
@"cube", // 立方體翻轉效果
@"oglFlip", // 翻轉效果
@"suckEffect", // 吸收效果
@"rippleEffect", // 水滴效果
@"pageCurl", // 向上翻頁
@"pageUnCurl", // 向下翻頁
@"cameralIrisHollowOpen", // 攝像頭打開
@"cameraIrisHollowClose", // 攝像頭關閉
];
}
return _animations;
}
-(NSArray *)images{
if (_images==nil) {
_images = @[@"0.jpg",
@"1.jpg",
@"2.jpg",
@"3.jpg",
@"4.jpg",
@"5.jpg"];
}
return _images;
}
CATransition 並不作用於指定的圖層屬性,這就是說你可以在即使不能準確得知改變了什麼的情況下對圖層做動畫,例如,在不知道 UITableView 哪一行被添加或者刪除的情況下,直接就可以平滑地刷新它,又如在 UITabBarController 切換視圖時添加上過渡動畫,可以比如淡入淡出的效果,又或者在不知道 UIViewController 內部的視圖層級的情況下對兩個不同的實例做過渡動畫。
這些例子和我們之前所討論的情況完全不同,因爲它們不僅涉及到圖層的屬性,而且是整個圖層樹的改變–我們在這種動畫的過程中手動在層級關係中添加或者移除圖層。
自定義過渡動畫
過渡動畫做基礎的原則就是對原始的圖層外觀截圖,然後添加一段動畫,平滑過渡到圖層改變之後那個截圖的效果。如果我們對圖層截圖,就可以使用屬性動畫來代替 CATransition 或者是 UIKit 的過渡方法來實現動畫。
CALayer 有一個 -renderInContext:
方法,可以通過把它繪製到 Core Graphics 的上下文中捕獲當前內容的圖片,然後在另外的視圖中顯示出來。如果我們把這個截屏視圖置於原始視圖之上,就可以遮住真實視圖的所有變化,於是重新創建了一個簡單的過渡效果。
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
// 獲取當前屏幕的截圖
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
//insert snapshot view in front of this one
UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
coverView.frame = self.view.bounds;
// 將截圖覆蓋到當前視圖上
[self.view addSubview:coverView];
// 爲了演示過渡效果,我們修改一下當前視圖的背景色,以區分之前的視圖
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
// 執行過渡動畫
[UIView animateWithDuration:0.75 animations:^{
CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 1);
coverView.transform = transform;
} completion:^(BOOL finished) {
// 最後移除掉障眼法的圖層
[coverView removeFromSuperview];
}];
}
取消動畫
在我們使用-addAnimation:forKey:
方法中的key參數來在添加動畫之後檢索一個動畫,使用如下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
但並不支持在動畫運行過程中修改動畫,所以這個方法主要用來檢測動畫的屬性,或者判斷它是否被添加到當前圖層中。
爲了終止一個指定的動畫,你可以用如下方法把它從圖層移除掉:
- (void)removeAnimationForKey:(NSString *)key;
也可以根據需要移除所有動畫:
- (void)removeAllAnimations;
動畫一旦被移除,圖層的外觀就立刻更新到當前的模型圖層的值。一般說來,動畫在結束之後被自動移除,除非設置 removedOnCompletion 爲NO,如果你設置動畫在結束之後不被自動移除,那麼當它不需要的時候你要手動移除它;否則它會一直存在於內存中,直到圖層被銷燬。
時間相關
CAMediaTiming 協議
CAMediaTiming 協議定義了在一段動畫內用來控制逝去時間的屬性的集合, CALayer 和 CAAnimation 都實現了這個協議,所以時間可以被任意基於一個圖層或者一段動畫的類控制。
CAAnimation 中幾個常用的屬性:
duration
:
duration(CAMediaTiming的屬性之一),duration是一個 CFTimeInterval 的類型(類似於 NSTimeInterval 的一種雙精度浮點類型),對將要進行的動畫的一次迭代指定了時間。
repeatCount
:
代表動畫重複的迭代次數。
repeatDuration
:
它讓動畫重複一個指定的時間,而不是指定次數。
autoreverses
:
在每次間隔交替循環過程中自動回放。在設置此值爲 YES 時,duration 的一半時間會用來做自動回放。
相對時間的幾個屬性:
在 Core Animation 中,時間都是相對的,每個動畫都有它自己描述的時間,可以獨立地加速,延時或者偏移。
beginTime
:
指定了動畫開始之前的的延遲時間。這裏的延遲從動畫添加到可見圖層的那一刻開始測量,默認是0(就是說動畫會立刻執行)。
speed
:
是一個時間的倍數,默認1.0,減少它會減慢圖層/動畫的時間,增加它會加快速度。如果2.0的速度,那麼對於一個 duration 爲1的動畫,實際上在0.5秒的時候就已經完成了。
特別的,前面提到到 CALayer 也實現了 CAMediaTiming 協議,如果把圖層的 speed 的值設置爲0,它會暫停任何添加到圖層上的動畫,如果 speed 的值大於1.0則變現爲快進,如果設置成一個負值則變爲倒回的動畫。
如果設置主 window 圖層的 speed 爲0時,可以將整個應用程序的動畫暫停。同樣的,如果我們將其設置爲快進,就可以完成加速多有的視圖動畫來進行自動化測試。設置代碼如下:
self.window.layer.speed = 100;
timeOffset
:
和 beginTime 類似,但是和增加 beginTime 導致的延遲動畫不同,增加 timeOffset 只是讓動畫快進到某一點,例如,對於一個持續1秒的動畫來說,設置 timeOffset 爲0.5意味着動畫將從一半的地方開始。
timeOffset 一個很有用的功能在於你可以它可以讓你手動控制動畫進程,通過設置 speed 爲0,可以禁用動畫的自動播放,然後來使用 timeOffset 來來回顯示動畫序列。這可以使得運用手勢來手動控制複雜動畫或者多個圖層的動畫組變得很簡單。
fillMode
:
動畫結束後的填充模式。類型有如下幾種。
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved
這個屬性表示動畫結束之後,是保持動畫最開始的那一幀還是保持動畫結束之後的那一幀。默認情況是kCAFillModeRemoved
。
緩衝過渡
CAMediaTimingFunction
動畫時間決定了圖層變換的時長,而動畫的速度表示動畫執行的“速率”,通常是變化量和時間的比值。這裏的變化量可以是圖層移動的距離,縮放的大小,也可以是圖層的透明度、填充色等。實際上,任意的可以做動畫的屬性的變化差值都可以稱作變化量。
默認情況下,我們的動畫都是線性變化的,即速率是恆定不變的,就如前面的那些示例,但是有時候,我們並不希望動畫的速度一層不變,那麼該怎麼做呢?幸運的事,Core Animation 已經爲我們設計了一系列標準函數提供給我們使用。
timingFunction
:
CAAnimation 的 timingFunction 屬性,是 CAMediaTimingFunction 類的一個對象。(如果想改變隱式動畫的計時函數,同樣也可以使用CATransaction的+setAnimationTimingFunction:
方法)
我們可以通過下面的構造函數來創建緩衝對象:
+ (instancetype)functionWithName:(CAMediaTimingFunctionName)name;
傳入如下幾個常量之一:
kCAMediaTimingFunctionLinear // 線性
kCAMediaTimingFunctionEaseIn // 緩慢起步
kCAMediaTimingFunctionEaseOut // 緩慢停止
kCAMediaTimingFunctionEaseInEaseOut // 先慢起步後快最後慢停止
kCAMediaTimingFunctionDefault // 類似於上面
示例:
-(void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position.x"];
animation.duration = 2.0;
animation.toValue = @(self.subLayer.frame.origin.y+400);
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.subLayer addAnimation:animation forKey:nil];
}
上圖的緩衝模式分別爲:
kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
CAKeyframeAnimation 有一個NSArray類型的 timingFunctions 屬性,我們可以用它來對每次動畫的步驟指定不同的計時函數。但是指定函數的個數一定要等於 keyframes 數組的元素個數減一,因爲它是描述每一幀之間動畫速度的函數。
自定義緩衝函數
在上一節中,介紹了幾個系統爲我們定義好的緩衝函數,能適用於大部分的應用環境。我們注意到,除了 +functionWithName:
之外,CAMediaTimingFunction 同樣有另一個構造函數,一個有四個浮點參數的 +functionWithControlPoints::::
,使用這個方法,我們可以創建一個自定義的緩衝函數,來匹配我們的動畫。
CAMediaTimingFunction 函數的主要原則在於它把輸入的時間轉換成起點和終點之間成比例的改變。我們可以用一個簡單的圖標來解釋,橫軸代表時間,縱軸代表改變的量,於是線性的緩衝就是一條從起點開始的簡單的斜線。
這條曲線的斜率代表了速度,斜率的改變代表了加速度,原則上來說,任何加速的曲線都可以用這種圖像來表示,但是 CAMediaTimingFunction 使用了一個叫做三次貝塞爾曲線的函數,它只可以產出指定緩衝函數的子集。
三次貝塞爾緩衝函數表達出先加速,然後減速,最後快到達終點的時候又加速的情況,那麼標準的緩衝函數又該如何用圖像來表示呢?
CAMediaTimingFunction 有一個叫做-getControlPointAtIndex:values:
的方法,可以用來檢索曲線的點,使用它我們可以找到標準緩衝函數的點,然後用 UIBezierPath 和 CAShapeLayer 來把它畫出來。
曲線的起始和終點始終是{0, 0}和{1, 1},於是我們只需要檢索曲線的第二個和第三個點(控制點)。
- (void)viewDidLoad{
[super viewDidLoad];
CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
// 獲取到兩個控制點
CGPoint controlPoint1, controlPoint2;
[function getControlPointAtIndex:1 values:(float *)&controlPoint1];
[function getControlPointAtIndex:2 values:(float *)&controlPoint2];
// 創建曲線
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointZero];
[path addCurveToPoint:CGPointMake(1, 1)
controlPoint1:controlPoint1 controlPoint2:controlPoint2];
// 轉換點,讓其可見
[path applyTransform:CGAffineTransformMakeScale(200, 200)];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 4.0f;
shapeLayer.path = path.CGPath;
[self.layerView.layer addSublayer:shapeLayer];
self.layerView.layer.geometryFlipped = YES;
}
所有的標準緩衝函數的圖像如下: