9. 圖層時間 圖層時間 時間和空間最大的區別在於,時間不能被覆用 -- 弗斯特梅里克 在上面兩章中,我們探討了可以用CAAnimation和它的子類實現的多種圖層動畫。動畫的發生是需要持續一段時間的,所以計時對整個概念來說至關重要。在這一章中,我們來看看CAMediaTiming,看看Core A ...
9. 圖層時間
圖層時間
時間和空間最大的區別在於,時間不能被覆用 -- 弗斯特梅里克
在上面兩章中,我們探討了可以用CAAnimation
和它的子類實現的多種圖層動畫。動畫的發生是需要持續一段時間的,所以計時對整個概念來說至關重要。在這一章中,我們來看看CAMediaTiming
,看看Core Animation是如何跟蹤時間的。
9.1 CAMediaTiming協議
CAMediaTiming`協議
CAMediaTiming
協議定義了在一段動畫內用來控制逝去時間的屬性的集合,CALayer
和CAAnimation
都實現了這個協議,所以時間可以被任意基於一個圖層或者一段動畫的類控制。
持續和重覆
我們在第八章“顯式動畫”中簡單提到過duration(CAMediaTiming
的屬性之一),duration
是一個CFTimeInterval
的類型(類似於NSTimeInterval
的一種雙精度浮點類型),對將要進行的動畫的一次迭代指定了時間。
這裡的一次迭代是什麼意思呢?CAMediaTiming
另外還有一個屬性叫做repeatCount
,代表動畫重覆的迭代次數。如果duration
是2,repeatCount
設為3.5(三個半迭代),那麼完整的動畫時長將是7秒。
一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:1012951431, 分享BAT,阿裡面試題、面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。
duration
和repeatCount
預設都是0。但這不意味著動畫時長為0秒,或者0次,這裡的0僅僅代表了“預設”,也就是0.25秒和1次,你可以用一個簡單的測試來嘗試為這兩個屬性賦多個值,如清單9.1,圖9.1展示了程式的結果。
清單9.1 測試duration
和repeatCount
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, weak) IBOutlet UITextField *durationField; @property (nonatomic, weak) IBOutlet UITextField *repeatField; @property (nonatomic, weak) IBOutlet UIButton *startButton; @property (nonatomic, strong) CALayer *shipLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add the ship self.shipLayer = [CALayer layer]; self.shipLayer.frame = CGRectMake(0, 0, 128, 128); self.shipLayer.position = CGPointMake(150, 150); self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage; [self.containerView.layer addSublayer:self.shipLayer]; } - (void)setControlsEnabled:(BOOL)enabled { for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) { control.enabled = enabled; control.alpha = enabled? 1.0f: 0.25f; } } - (IBAction)hideKeyboard { [self.durationField resignFirstResponder]; [self.repeatField resignFirstResponder]; } - (IBAction)start { CFTimeInterval duration = [self.durationField.text doubleValue]; float repeatCount = [self.repeatField.text floatValue]; //animate the ship rotation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation"; animation.duration = duration; animation.repeatCount = repeatCount; animation.byValue = @(M_PI * 2); animation.delegate = self; [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"]; //disable controls [self setControlsEnabled:NO]; } - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag { //reenable controls [self setControlsEnabled:YES]; } @end
圖9.2 擺動門的動畫
對門進行擺動的代碼見清單9.2。我們用了autoreverses
來使門在打開後自動關閉,在這裡我們把repeatDuration
設置為INFINITY
,於是動畫無限迴圈播放,設置repeatCount
為INFINITY
也有同樣的效果。註意repeatCount和repeatDuration
可能會相互衝突,所以你只要對其中一個指定非零值。對兩個屬性都設置非0值的行為沒有被定義。
清單9.2 使用autoreverses屬性實現門的搖擺
@interface ViewController () @property (nonatomic, weak) UIView *containerView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add the door CALayer *doorLayer = [CALayer layer]; doorLayer.frame = CGRectMake(0, 0, 128, 256); doorLayer.position = CGPointMake(150 - 64, 150); doorLayer.anchorPoint = CGPointMake(0, 0.5); doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage; [self.containerView.layer addSublayer:doorLayer]; //apply perspective transform CATransform3D perspective = CATransform3DIdentity; perspective.m34 = -1.0 / 500.0; self.containerView.layer.sublayerTransform = perspective; //apply swinging animation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation.y"; animation.toValue = @(-M_PI_2); animation.duration = 2.0; animation.repeatDuration = INFINITY; animation.autoreverses = YES; [doorLayer addAnimation:animation forKey:nil]; } @end
相對時間
每次討論到Core Animation,時間都是相對的,每個動畫都有它自己描述的時間,可以獨立地加速,延時或者偏移。
beginTime
指定了動畫開始之前的的延遲時間。這裡的延遲從動畫添加到可見圖層的那一刻開始測量,預設是0(就是說動畫會立刻執行)。
speed
是一個時間的倍數,預設1.0,減少它會減慢圖層/動畫的時間,增加它會加快速度。如果2.0的速度,那麼對於一個duration
為1的動畫,實際上在0.5秒的時候就已經完成了。
timeOffset
和beginTime
類似,但是和增加beginTime
導致的延遲動畫不同,增加timeOffset
只是讓動畫快進到某一點,例如,對於一個持續1秒的動畫來說,設置timeOffset
為0.5意味著動畫將從一半的地方開始。
和beginTime
不同的是,timeOffset
並不受speed的影響。所以如果你把speed
設為2.0,把timeOffset
設置為0.5,那麼你的動畫將從動畫最後結束的地方開始,因為1秒的動畫實際上被縮短到了0.5秒。然而即使使用了timeOffset
讓動畫從結束的地方開始,它仍然播放了一個完整的時長,這個動畫僅僅是迴圈了一圈,然後從頭開始播放。
可以用清單9.3的測試程式驗證一下,設置speed
和timeOffset
滑塊到隨意的值,然後點擊播放來觀察效果(見圖9.3)
清單9.3 測試timeOffset
和speed
屬性
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *containerView; @property (nonatomic, weak) IBOutlet UILabel *speedLabel; @property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel; @property (nonatomic, weak) IBOutlet UISlider *speedSlider; @property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider; @property (nonatomic, strong) UIBezierPath *bezierPath; @property (nonatomic, strong) CALayer *shipLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create a path self.bezierPath = [[UIBezierPath alloc] init]; [self.bezierPath moveToPoint:CGPointMake(0, 150)]; [self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)]; //draw the path using a CAShapeLayer CAShapeLayer *pathLayer = [CAShapeLayer layer]; pathLayer.path = self.bezierPath.CGPath; pathLayer.fillColor = [UIColor clearColor].CGColor; pathLayer.strokeColor = [UIColor redColor].CGColor; pathLayer.lineWidth = 3.0f; [self.containerView.layer addSublayer:pathLayer]; //add the ship self.shipLayer = [CALayer layer]; self.shipLayer.frame = CGRectMake(0, 0, 64, 64); self.shipLayer.position = CGPointMake(0, 150); self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage; [self.containerView.layer addSublayer:self.shipLayer]; //set initial values [self updateSliders]; } - (IBAction)updateSliders { CFTimeInterval timeOffset = self.timeOffsetSlider.value; self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset]; float speed = self.speedSlider.value; self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed]; } - (IBAction)play { //create the keyframe animation CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position"; animation.timeOffset = self.timeOffsetSlider.value; animation.speed = self.speedSlider.value; animation.duration = 1.0; animation.path = self.bezierPath.CGPath; animation.rotationMode = kCAAnimationRotateAuto; animation.removedOnCompletion = NO; [self.shipLayer addAnimation:animation forKey:@"slide"]; } @end
9.2 層級關係時間
層級關係時間
9.3 手動動畫
手動動畫
timeOffset
一個很有用的功能在於你可以它可以讓你手動控制動畫進程,通過設置speed
為0,可以禁用動畫的自動播放,然後來使用timeOffset
來來回顯示動畫序列。這可以使得運用手勢來手動控制動畫變得很簡單。
舉個簡單的例子:還是之前關門的動畫,修改代碼來用手勢控制動畫。我們給視圖添加一個UIPanGestureRecognizer
,然後用timeOffset
左右搖晃。
因為在動畫添加到圖層之後不能再做修改了,我們來通過調整layer
的timeOffset
達到同樣的效果(清單9.4)。
清單9.4 通過觸摸手勢手動控制動畫
@interface ViewController () @property (nonatomic, weak) UIView *containerView; @property (nonatomic, strong) CALayer *doorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //add the door self.doorLayer = [CALayer layer]; self.doorLayer.frame = CGRectMake(0, 0, 128, 256); self.doorLayer.position = CGPointMake(150 - 64, 150); self.doorLayer.anchorPoint = CGPointMake(0, 0.5); self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage; [self.containerView.layer addSublayer:self.doorLayer]; //apply perspective transform CATransform3D perspective = CATransform3DIdentity; perspective.m34 = -1.0 / 500.0; self.containerView.layer.sublayerTransform = perspective; //add pan gesture recognizer to handle swipes UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init]; [pan addTarget:self action:@selector(pan:)]; [self.view addGestureRecognizer:pan]; //pause all layer animations self.doorLayer.speed = 0.0; //apply swinging animation (which won't play because layer is paused) CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform.rotation.y"; animation.toValue = @(-M_PI_2); animation.duration = 1.0; [self.doorLayer addAnimation:animation forKey:nil]; } - (void)pan:(UIPanGestureRecognizer *)pan { //get horizontal component of pan gesture CGFloat x = [pan translationInView:self.view].x; //convert from points to animation duration //using a reasonable scale factor x /= 200.0f; //update timeOffset and clamp result CFTimeInterval timeOffset = self.doorLayer.timeOffset; timeOffset = MIN(0.999, MAX(0.0, timeOffset - x)); self.doorLayer.timeOffset = timeOffset; //reset pan gesture [pan setTranslation:CGPointZero inView:self.view]; } @end
這其實是個小詭計,也許相對於設置個動畫然後每次顯示一幀而言,用移動手勢來直接設置門的transform
會更簡單。
在這個例子中的確是這樣,但是對於比如說關鍵這這樣更加複雜的情況,或者有多個圖層的動畫組,相對於實時計算每個圖層的屬性而言,這就顯得方便的多了。
9.4 總結
總結
在這一章,我們瞭解了CAMediaTiming
協議,以及Core Animation用來操作時間控制動畫的機制。在下一章,我們將要接觸緩衝
,另一個用來使動畫更加真實的操作時間的技術。
10. 緩衝
緩衝
生活和藝術一樣,最美的永遠是曲線。 -- 愛德華布爾沃 - 利頓
在第九章“圖層時間”中,我們討論了動畫時間和CAMediaTiming
協議。現在我們來看一下另一個和時間相關的機制--所謂的緩衝。Core Animation使用緩衝來使動畫移動更平滑更自然,而不是看起來的那種機械和人工,在這一章我們將要研究如何對你的動畫控制和自定義緩衝曲線。
10.1 動畫速度
動畫速度
動畫實際上就是一段時間內的變化,這就暗示了變化一定是隨著某個特定的速率進行。速率由以下公式計算而來:
velocity = change / time
這裡的變化可以指的是一個物體移動的距離,時間指動畫持續的時長,用這樣的一個移動可以更加形象的描述(比如position
和bounds
屬性的動畫),但實際上它應用於任意可以做動畫的屬性(比如color
和opacity
)。
上面的等式假設了速度在整個動畫過程中都是恆定不變的(就如同第八章“顯式動畫”的情況),對於這種恆定速度的動畫我們稱之為“線性步調”,而且從技術的角度而言這也是實現動畫最簡單的方式,但也是完全不真實的一種效果。
考慮一個場景,一輛車行駛在一定距離內,它並不會一開始就以60mph的速度行駛,然後到達終點後突然變成0mph。一是因為需要無限大的加速度(即使是最好的車也不會在0秒內從0跑到60),另外不然的話會幹死所有乘客。在現實中,它會慢慢地加速到全速,然後當它接近終點的時候,它會慢慢地減速,直到最後停下來。
那麼對於一個掉落到地上的物體又會怎樣呢?它會首先停在空中,然後一直加速到落到地面,然後突然停止(然後由於積累的動能轉換伴隨著一聲巨響,砰!)。
現實生活中的任何一個物體都會在運動中加速或者減速。那麼我們如何在動畫中實現這種加速度呢?一種方法是使用物理引擎來對運動物體的摩擦和動量來建模,然而這會使得計算過於複雜。我們稱這種類型的方程為緩衝函數,幸運的是,Core Animation內嵌了一系列標準函數提供給我們使用。
CAMediaTimingFunction
那麼該如何使用緩衝方程式呢?首先需要設置CAAnimation的timingFunction屬性,是CAMediaTimingFunction
類的一個對象。如果想改變隱式動畫的計時函數,同樣也可以使用CATransaction
的+setAnimationTimingFunction:
方法。
這裡有一些方式來創建CAMediaTimingFunction
,最簡單的方式是調用+timingFunctionWithName:
的構造方法。這裡傳入如下幾個常量之一:
kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
kCAMediaTimingFunctionLinear
選項創建了一個線性的計時函數,同樣也是CAAnimation的timingFunction
屬性為空時候的預設函數。線性步調對於那些立即加速並且保持勻速到達終點的場景會有意義(例如射出槍膛的子彈),但是預設來說它看起來很奇怪,因為對大多數的動畫來說確實很少用到。
kCAMediaTimingFunctionEaseIn
常量創建了一個慢慢加速然後突然停止的方法。對於之前提到的自由落體的例子來說很適合,或者比如對準一個目標的導彈的發射。
kCAMediaTimingFunctionEaseOut
則恰恰相反,它以一個全速開始,然後慢慢減速停止。它有一個削弱的效果,應用的場景比如一扇門慢慢地關上,而不是砰地一聲。
kCAMediaTimingFunctionEaseInEaseOut
創建了一個慢慢加速然後再慢慢減速的過程。這是現實世界大多數物體移動的方式,也是大多數動畫來說最好的選擇。如果只可以用一種緩衝函數的話,那就必須是它了。那麼你會疑惑為什麼這不是預設的選擇,實際上當使用UIView的動畫方法時,他的確是預設的,但當創建CAAnimation
的時候,就需要手動設置它了。
最後還有一個kCAMediaTimingFunctionDefault
,它和kCAMediaTimingFunctionEaseInEaseOut
很類似,但是加速和減速的過程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut
的區別很難察覺,可能是蘋果覺得它對於隱式動畫來說更適合(然後對UIKit就改變了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut
作為預設效果),雖然它的名字說是預設的,但還是要記住當創建顯式的CAAnimation
它並不是預設選項(換句話說,預設的圖層行為動畫用kCAMediaTimingFunctionDefault
作為它們的計時方法)。
你可以使用一個簡單的測試工程來實驗一下(清單10.1),在運行之前改變緩衝函數的代碼,然後點擊任何地方來觀察圖層是如何通過指定的緩衝移動的。
清單10.1 緩衝函數的簡單測試
@interface ViewController () @property (nonatomic, strong) CALayer *colorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create a red layer self.colorLayer = [CALayer layer]; self.colorLayer.frame = CGRectMake(0, 0, 100, 100); self.colorLayer.position = CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0); self.colorLayer.backgroundColor = [UIColor redColor].CGColor; [self.view.layer addSublayer:self.colorLayer]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //configure the transaction [CATransaction begin]; [CATransaction setAnimationDuration:1.0]; [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; //set the position self.colorLayer.position = [[touches anyObject] locationInView:self.view]; //commit transaction [CATransaction commit]; } @end
UIView的動畫緩衝
UIKit的動畫也同樣支持這些緩衝方法的使用,儘管語法和常量有些不同,為了改變UIView動畫的緩衝選項,給options參數添加如下常量之一:
UIViewAnimationOptionCurveEaseInOut
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionCurveLinear
它們和CAMediaTimingFunction
緊密關聯,UIViewAnimationOptionCurveEaseInOut
是預設值(這裡沒有kCAMediaTimingFunctionDefault
相對應的值了)。
具體使用方法見清單10.2(註意到這裡不再使用UIView
額外添加的圖層,因為UIKit的動畫並不支持這類圖層)。
清單10.2 使用UIKit動畫的緩衝測試工程
@interface ViewController () @property (nonatomic, strong) UIView *colorView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create a red layer self.colorView = [[UIView alloc] init]; self.colorView.bounds = CGRectMake(0, 0, 100, 100); self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); self.colorView.backgroundColor = [UIColor redColor]; [self.view addSubview:self.colorView]; } - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { //perform the animation [UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{ //set the position self.colorView.center = [[touches anyObject] locationInView:self.view]; } completion:NULL]; } @end
緩衝和關鍵幀動畫
或許你會回想起第八章裡面顏色切換的關鍵幀動畫由於線性變換的原因(見清單8.5)看起來有些奇怪,使得顏色變換非常不自然。為了糾正這點,我們來用更加合適的緩衝方法,例如kCAMediaTimingFunctionEaseIn
,給圖層的顏色變化添加一點脈衝效果,讓它更像現實中的一個彩色燈泡。
我們不想給整個動畫過程應用這個效果,我們希望對每個動畫的過程重覆這樣的緩衝,於是每次顏色的變換都會有脈衝效果。
CAKeyframeAnimation
有一個NSArray
類型的timingFunctions
屬性,我們可以用它來對每次動畫的步驟指定不同的計時函數。但是指定函數的個數一定要等於keyframes數組的元素個數減一,因為它是描述每一幀之間動畫速度的函數。
在這個例子中,我們自始至終想使用同一個緩衝函數,但我們同樣需要一個函數的數組來告訴動畫不停地重覆每個步驟,而不是在整個動畫序列只做一次緩衝,我們簡單地使用包含多個相同函數拷貝的數組就可以了(見清單10.3)。
運行更新後的代碼,你會發現動畫看起來更加自然了。
清單10.3 對CAKeyframeAnimation
使用CAMediaTimingFunction
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @property (nonatomic, weak) IBOutlet CALayer *colorLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create sublayer self.colorLayer = [CALayer layer]; self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; //add it to our view [self.layerView.layer addSublayer:self.colorLayer]; } - (IBAction)changeColor { //create a keyframe animation CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"backgroundColor"; animation.duration = 2.0; animation.values = @[ (__bridge id)[UIColor blueColor].CGColor, (__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor greenColor].CGColor, (__bridge id)[UIColor blueColor].CGColor ]; //add timing function CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]; animation.timingFunctions = @[fn, fn, fn]; //apply animation to layer [self.colorLayer addAnimation:animation forKey:nil]; } @end
10.2 自定義緩衝函數
自定義緩衝函數
在第八章中,我們給時鐘項目添加了動畫。看起來很贊,但是如果有合適的緩衝函數就更好了。在顯示世界中,鐘錶指針轉動的時候,通常起步很慢,然後迅速啪地一聲,最後緩衝到終點。但是標準的緩衝函數在這裡每一個適合它,那該如何創建一個新的呢?
除了+functionWithName:
之外,CAMediaTimingFunction
同樣有另一個構造函數,一個有四個浮點參數的+functionWithControlPoints::::
(註意這裡奇怪的語法,並沒有包含具體每個參數的名稱,這在objective-C中是合法的,但是卻違反了蘋果對方法命名的指導方針,而且看起來是一個奇怪的設計)。
使用這個方法,我們可以創建一個自定義的緩衝函數,來匹配我們的時鐘動畫,為了理解如何使用這個方法,我們要瞭解一些CAMediaTimingFunction
是如何工作的。
三次貝塞爾曲線
CAMediaTimingFunction
函數的主要原則在於它把輸入的時間轉換成起點和終點之間成比例的改變。我們可以用一個簡單的圖標來解釋,橫軸代表時間,縱軸代表改變的量,於是線性的緩衝就是一條從起點開始的簡單的斜線(圖10.1)。
圖10.2 三次貝塞爾緩衝函數
實際上它是一個很奇怪的函數,先加速,然後減速,最後快到達終點的時候又加速,那麼標準的緩衝函數又該如何用圖像來表示呢?
CAMediaTimingFunction
有一個叫做-getControlPointAtIndex:values:
的方法,可以用來檢索曲線的點,這個方法的設計的確有點奇怪(或許也就只有蘋果能回答為什麼不簡單返回一個CGPoint
),但是使用它我們可以找到標準緩衝函數的點,然後用UIBezierPath
和CAShapeLayer
來把它畫出來。
曲線的起始和終點始終是{0, 0}和{1, 1},於是我們只需要檢索曲線的第二個和第三個點(控制點)。具體代碼見清單10.4。所有的標準緩衝函數的圖像見圖10.3。
清單10.4 使用UIBezierPath
繪製CAMediaTimingFunction
@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //create timing function CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]; //get control points CGPoint controlPoint1, controlPoint2; [function getControlPointAtIndex:1 values:(float *)&controlPoint1]; [function getControlPointAtIndex:2 values:(float *)&controlPoint2]; //create curve UIBezierPath *path = [[UIBezierPath alloc] init]; [path moveToPoint:CGPointZero]; [path addCurveToPoint:CGPointMake(1, 1) controlPoint1:controlPoint1 controlPoint2:controlPoint2]; //scale the path up to a reasonable size for display [path applyTransform:CGAffineTransformMakeScale(200, 200)]; //create shape layer 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]; //flip geometry so that 0,0 is in the bottom-left self.layerView.layer.geometryFlipped = YES; } @end
圖10.4 自定義適合時鐘的緩衝函數
清單10.5 添加了自定義緩衝函數的時鐘程式
- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated { //generate transform CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1); if (animated) { //create transform animation CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"transform"; animation.fromValue = [handView.layer.presentationLayer valueForKey:@"transform"]; animation.toValue = [NSValue valueWithCATransform3D:transform]; animation.duration = 0.5; animation.delegate = self; animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1]; //apply animation handView.layer.transform = transform; [handView.layer addAnimation:animation forKey:nil]; } else { //set transform directly handView.layer.transform = transform; } }
更加複雜的動畫曲線
考慮一個橡膠球掉落到堅硬的地面的場景,當開始下落的時候,它會持續加速知道落到地面,然後經過幾次反彈,最後停下來。如果用一張圖來說明,它會如圖10.5所示。
這可以起到作用,但效果並不是很好,到目前為止我們所完成的只是一個非常複雜的方式來使用線性緩衝複製CABasicAnimation
的行為。這種方式的好處在於我們可以更加精確地控制緩衝,這也意味著我們可以應用一個完全定製的緩衝函數。那麼該如何做呢?
緩衝背後的數學並不很簡單,但是幸運的是我們不需要一一實現它。羅伯特·彭納有一個網頁關於緩衝函數(http://www.robertpenner.com/easing ),包含了大多數普遍的緩衝函數的多種編程語言的實現的鏈接,包括C。這裡是一個緩衝進入緩衝退出函數的示例(實際上有很多不同的方式去實現它)。
float quadraticEaseInOut(float t) { return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1; }
對我們的彈性球來說,我們可以使用bounceEaseOut
函數:
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; }
如果修改清單10.7的代碼來引入bounceEaseOut
方法,我們的任務就是僅僅交換緩衝函數,現在就可以選擇任意的緩衝類型創建動畫了(見清單10.8)。
清單10.8 用關鍵幀實現自定義的緩衝函數
- (void)animate { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //set up animation parameters NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; CFTimeInterval duration = 1.0; //generate keyframes NSInteger numFrames = duration * 60; NSMutableArray *frames = [NSMutableArray array]; for (int i = 0; i < numFrames; i++) { float time = 1/(float)numFrames * i; //apply easing time = bounceEaseOut(time); //add keyframe [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]]; } //create keyframe animation CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position"; animation.duration = 1.0; animation.delegate = self; animation.values = frames; //apply animation [self.ballView.layer addAnimation:animation forKey:nil]; }
10.3 總結
在這一章中,我們瞭解了有關緩衝和CAMediaTimingFunction
類,它可以允許我們創建自定義的緩衝函數來完善我們的動畫,同樣瞭解瞭如何用CAKeyframeAnimation
來避開CAMediaTimingFunction
的限制,創建完全自定義的緩衝函數。
在下一章中,我們將要研究基於定時器的動畫--另一個給我們對動畫更多控制的選擇,並且實現對動畫的實時操縱。