我能夠指導你,可是你必須按照我說的作。 -- 駭客帝國git
在第10章「緩衝」中,咱們研究了CAMediaTimingFunction
,它是一個經過控制動畫緩衝來模擬物理效果例如加速或者減速來加強現實感的東西,那麼若是想更加真實地模擬物理交互或者實時根據用戶輸入修改動畫改怎麼辦呢?在這一章中,咱們將繼續探索一種可以容許咱們精確地控制一幀一幀展現的基於定時器的動畫。github
動畫看起來是用來顯示一段連續的運動過程,但實際上當在固定位置上展現像素的時候並不能作到這一點。通常來講這種顯示都沒法作到連續的移動,能作的僅僅是足夠快地展現一系列靜態圖片,只是看起來像是作了運動。數組
咱們以前提到過iOS按照每秒60次刷新屏幕,而後CAAnimation
計算出須要展現的新的幀,而後在每次屏幕更新的時候同步繪製上去,CAAnimation
最機智的地方在於每次刷新須要展現的時候去計算插值和緩衝。網絡
在第10章中,咱們解決了如何自定義緩衝函數,而後根據須要展現的幀的數組來告訴CAKeyframeAnimation
的實例如何去繪製。全部的Core Animation實際上都是按照必定的序列來顯示這些幀,那麼咱們能夠本身作到這些麼?app
NSTimer
實際上,咱們在第三章「圖層幾何學」中已經作過相似的東西,就是時鐘那個例子,咱們用了NSTimer
來對鐘錶的指針作定時動畫,一秒鐘更新一次,可是若是咱們把頻率調整成一秒鐘更新60次的話,原理是徹底相同的。ide
咱們來試着用NSTimer
來修改第十章中彈性球的例子。因爲如今咱們在定時器啓動以後連續計算動畫幀,咱們須要在類中添加一些額外的屬性來存儲動畫的fromValue
,toValue
,duration
和當前的timeOffset
(見清單11.1)。函數
清單11.1 使用NSTimer
實現彈性球動畫oop
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) UIImageView *ballView; @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, assign) NSTimeInterval duration; @property (nonatomic, assign) NSTimeInterval timeOffset; @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //add ball image view UIImage *ballImage = [UIImage imageNamed:@"Ball.png"]; self.ballView = [[UIImageView alloc] initWithImage:ballImage]; [self.containerView addSubview:self.ballView]; //animate [self animate]; }- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ //replay animation on tap [self animate]; }float interpolate(float from, float to, float time) { return (to - from) * time + from; }- (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time { if ([fromValue isKindOfClass:[NSValue class]]) { //get type const char *type = [(NSValue *)fromValue objCType]; if (strcmp(type, @encode(CGPoint)) == 0) { CGPoint from = [fromValue CGPointValue]; CGPoint to = [toValue CGPointValue]; CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time)); return [NSValue valueWithCGPoint:result]; } } //provide safe default implementation return (time < 0.5)? fromValue: toValue; }float bounceEaseOut(float t) { if (t < 4/11.0) { return (121 * t * t)/16.0; } else if (t < 8/11.0) { return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; } else if (t < 9/10.0) { return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; } return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; }- (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0 target:self selector:@selector(step:) userInfo:nil repeats:YES]; }- (void)step:(NSTimer *)step { //update time offset self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; //move ball view to new position self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; self.timer = nil; } }@end
很贊,並且和基於關鍵幀例子的代碼同樣不少,可是若是想一次性在屏幕上對不少東西作動畫,很明顯就會有不少問題。優化
NSTimer
並非最佳方案,爲了理解這點,咱們須要確切地知道NSTimer
是如何工做的。iOS上的每一個線程都管理了一個NSRunloop
,字面上看就是經過一個循環來完成一些任務列表。可是對主線程,這些任務包含以下幾項:動畫
處理觸摸事件
發送和接受網絡數據包
執行使用gcd的代碼
處理計時器行爲
屏幕重繪
當你設置一個NSTimer
,他會被插入到當前任務列表中,而後直到指定時間過去以後纔會被執行。可是什麼時候啓動定時器並無一個時間上限,並且它只會在列表中上一個任務完成以後開始執行。這一般會致使有幾毫秒的延遲,可是若是上一個任務過了好久才完成就會致使延遲很長一段時間。
屏幕重繪的頻率是一秒鐘六十次,可是和定時器行爲同樣,若是列表中上一個執行了很長時間,它也會延遲。這些延遲都是一個隨機值,因而就不能保證定時器精準地一秒鐘執行六十次。有時候發生在屏幕重繪以後,這就會使得更新屏幕會有個延遲,看起來就是動畫卡殼了。有時候定時器會在屏幕更新的時候執行兩次,因而動畫看起來就跳動了。
咱們能夠經過一些途徑來優化:
咱們能夠用CADisplayLink
讓更新頻率嚴格控制在每次屏幕刷新以後。
基於真實幀的持續時間而不是假設的更新頻率來作動畫。
調整動畫計時器的run loop
模式,這樣就不會被別的事件干擾。
CADisplayLink
CADisplayLink 是CoreAnimation提供的另外一個相似於 NSTimer 的類,它老是在屏幕完成一次更新以前啓動,它的接口設計的和 NSTimer 很相似,因此它實際上就是一個內置實現的替代,可是和 timeInterval 以秒爲單位不一樣, CADisplayLink 有一個整型的 frameInterval 屬性,指定了間隔多少幀以後才執行。默認值是1,意味着每次屏幕更新以前都會執行一次。可是若是動畫的代碼執行起來超過了六十分之一秒,你能夠指定frameInterval
爲2,就是說動畫每隔一幀執行一次(一秒鐘30幀)或者3,也就是一秒鐘20次,等等。
用 CADisplayLink 而不是 NSTimer ,會保證幀率足夠連續,使得動畫看起來更加平滑,但即便 CADisplayLink 也不能保證每一幀都按計劃執行,一些失去控制的離散的任務或者事件(例如資源緊張的後臺程序)可能會致使動畫偶爾地丟幀。當使用NSTimer
的時候,一旦有機會計時器就會開啓,可是 CADisplayLink 卻不同:若是它丟失了幀,就會直接忽略它們,而後在下一次更新的時候接着運行。
不管是使用NSTimer
仍是 CADisplayLink ,咱們仍然須要處理一幀的時間超出了預期的六十分之一秒。因爲咱們不可以計算出一幀真實的持續時間,因此須要手動測量。咱們能夠在每幀開始刷新的時候用 CACurrentMediaTime() 記錄當前時間,而後和上一幀記錄的時間去比較。
經過比較這些時間,咱們就能夠獲得真實的每幀持續的時間,而後代替硬編碼的六十分之一秒。咱們來更新一下上個例子(見清單11.2)。
清單11.2 經過測量沒幀持續的時間來使得動畫更加平滑
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, strong) UIImageView *ballView; @property (nonatomic, strong) CADisplayLink *timer; @property (nonatomic, assign) CFTimeInterval duration; @property (nonatomic, assign) CFTimeInterval timeOffset; @property (nonatomic, assign) CFTimeInterval lastStep; @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue;@end@implementation ViewController ...- (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; }- (void)step:(CADisplayLink *)timer { //calculate time delta CFTimeInterval thisStep = CACurrentMediaTime(); CFTimeInterval stepDuration = thisStep - self.lastStep; self.lastStep = thisStep; //update time offset self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; //move ball view to new position self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; self.timer = nil; } }@end