iOS核心動畫高級技巧-5

9. 圖層時間

圖層時間

時間和空間最大的區別在於,時間不能被複用 -- 弗斯特梅里克面試

在上面兩章中,咱們探討了能夠用CAAnimation和它的子類實現的多種圖層動畫。動畫的發生是須要持續一段時間的,因此計時對整個概念來講相當重要。在這一章中,咱們來看看CAMediaTiming,看看Core Animation是如何跟蹤時間的。編程

9.1 CAMediaTiming協議

CAMediaTiming`協議

CAMediaTiming協議定義了在一段動畫內用來控制逝去時間的屬性的集合,CALayerCAAnimation都實現了這個協議,因此時間能夠被任意基於一個圖層或者一段動畫的類控制。數組

持續和重複

咱們在第八章「顯式動畫」中簡單提到過duration(CAMediaTiming的屬性之一),duration是一個CFTimeInterval的類型(相似於NSTimeInterval的一種雙精度浮點類型),對將要進行的動畫的一次迭代指定了時間。app

這裏的一次迭代是什麼意思呢?CAMediaTiming另外還有一個屬性叫作repeatCount,表明動畫重複的迭代次數。若是duration是2,repeatCount設爲3.5(三個半迭代),那麼完整的動畫時長將是7秒。編程語言

一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個個人iOS交流羣:1012951431, 分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!但願幫助開發者少走彎路。ide

durationrepeatCount默認都是0。但這不意味着動畫時長爲0秒,或者0次,這裏的0僅僅表明了「默認」,也就是0.25秒和1次,你能夠用一個簡單的測試來嘗試爲這兩個屬性賦多個值,如清單9.1,圖9.1展現了程序的結果。函數

清單9.1 測試durationrepeatCount學習

@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,因而動畫無限循環播放,設置repeatCountINFINITY也有一樣的效果。注意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秒的時候就已經完成了。

timeOffsetbeginTime相似,可是和增長beginTime致使的延遲動畫不一樣,增長timeOffset只是讓動畫快進到某一點,例如,對於一個持續1秒的動畫來講,設置timeOffset爲0.5意味着動畫將從一半的地方開始。

beginTime不一樣的是,timeOffset並不受speed的影響。因此若是你把speed設爲2.0,把timeOffset設置爲0.5,那麼你的動畫將從動畫最後結束的地方開始,由於1秒的動畫實際上被縮短到了0.5秒。然而即便使用了timeOffset讓動畫從結束的地方開始,它仍然播放了一個完整的時長,這個動畫僅僅是循環了一圈,而後從頭開始播放。

能夠用清單9.3的測試程序驗證一下,設置speedtimeOffset滑塊到隨意的值,而後點擊播放來觀察效果(見圖9.3)

清單9.3 測試timeOffsetspeed屬性

@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左右搖晃。

由於在動畫添加到圖層以後不能再作修改了,咱們來經過調整layertimeOffset達到一樣的效果(清單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

 

這裏的變化能夠指的是一個物體移動的距離,時間指動畫持續的時長,用這樣的一個移動能夠更加形象的描述(好比positionbounds屬性的動畫),但實際上它應用於任意能夠作動畫的屬性(好比coloropacity)。

上面的等式假設了速度在整個動畫過程當中都是恆定不變的(就如同第八章「顯式動畫」的狀況),對於這種恆定速度的動畫咱們稱之爲「線性步調」,並且從技術的角度而言這也是實現動畫最簡單的方式,但也是徹底不真實的一種效果。

考慮一個場景,一輛車行駛在必定距離內,它並不會一開始就以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),可是使用它咱們能夠找到標準緩衝函數的點,而後用UIBezierPathCAShapeLayer來把它畫出來。

曲線的起始和終點始終是{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的限制,建立徹底自定義的緩衝函數。

在下一章中,咱們將要研究基於定時器的動畫--另外一個給咱們對動畫更多控制的選擇,而且實現對動畫的實時操縱。

相關文章
相關標籤/搜索