CALayer、Core Animation和UIView動畫(1)

概覽

在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺iOS動畫全貌。在這里你可以看到iOS中如何使用圖層精簡非交互式繪圖,如何通過核心動畫創建基礎動畫、關鍵幀動畫、動畫組、轉場動畫,如何通過UIView的裝飾方法對這些動畫操作進行簡化等。在今天的文章里您可以看到動畫操作在iOS中是如何簡單和高效,很多原來想做但是苦于沒有思路的動畫在iOS中將變得越發簡單:

1.CALayer
  • CALayer簡介
  • CALayer常用屬性
  • CALayer繪圖
2.Core Animation
  • 基礎動畫
  • 關鍵幀動畫
  • 動畫組
  • 轉場動畫
  • 逐幀動畫
3.UIView動畫封裝
  • 基礎動畫
  • 關鍵幀動畫
  • 轉場動畫

CALayer簡介

在介紹動畫操作之前我們必須先來了解一個動畫中常用的對象CALayer。CALayer包含在QuartzCore框架中,這是一個跨平臺的框架,既可以用在iOS中又可以用在Mac OS X中。在使用Core Animation開發動畫的本質就是將CALayer中的內容轉化為位圖從而供硬件操作,所以要熟練掌握動畫操作必須先來熟悉CALayer。當利用drawRect:方法繪圖的本質就是繪制到了UIView的layer(屬性)中。在Core Animation中我們操作更多的則不再是UIView而是直接面對CALayer。下圖描繪了CALayer和UIView的關系,在UIView中有一個layer屬性作為根圖層,根圖層上可以放其他子圖層,在UIView中所有能夠看到的內容都包含在layer中:

CALayer常用屬性

在iOS中CALayer的設計主要是了為了內容展示和動畫操作,CALayer本身并不包含在UIKit中,它不能響應事件。由于CALayer在設計之初就考慮它的動畫操作功能,CALayer很多屬性在修改時都能形成動畫效果,這種屬性稱為“隱式動畫屬性”。但是對于UIView的根圖層而言屬性的修改并不形成動畫效果,因為很多情況下根圖層更多的充當容器的做用,如果它的屬性變動形成動畫效果會直接影響子圖層。另外,UIView的根圖層創建工作完全由iOS負責完成,無法重新創建,但是可以往根圖層中添加子圖層或移除子圖層。

下表列出了CALayer常用的屬性:

5C22EFC5-3112-44B5-8922-1C84686DC94F.png
  • 隱式屬性動畫的本質是這些屬性的變動默認隱含了CABasicAnimation動畫實現,詳情大家可以參照Xcode幫助文檔中“Animatable Properties”一節。
  • 在CALayer中很少使用frame屬性,因為frame本身不支持動畫效果,通常使用bounds和position代替。
  • CALayer中透明度使用opacity表示而不是alpha;中心點使用position表示而不是center。
  • anchorPoint屬性是圖層的錨點,范圍在(01,01)表示在x、y軸的比例,這個點永遠可以同position(中心點)重合,當圖層中心點固定后,調整anchorPoint即可達到調整圖層顯示位置的作用(因為它永遠和position重合)

為了進一步說明anchorPoint的作用,假設有一個層大小100*100,現在中心點位置(50,50),由此可以得出frame(0,0,100,100)。上面說過anchorPoint默認為(0.5,0.5),同中心點position重合,此時使用圖形描述如圖1;當修改anchorPoint為(0,0),此時錨點處于圖層左上角,但是中心點poition并不會改變,因此圖層會向右下角移動,如圖2;然后修改anchorPoint為(1,1,),position還是保持位置不變,錨點處于圖層右下角,此時圖層如圖3。

下面通過一個簡單的例子演示一下上面幾個屬性,程序初始化階段我們定義一個正方形,但是圓角路徑調整為正方形邊長的一般,使其看起來是一個圓形,在點擊屏幕的時候修改圖層的屬性形成動畫效果(注意在程序中沒有直接修改UIView的layer屬性,因為根圖層無法形成動畫效果):

#import "KCMainViewController.h"
#define WIDTH 50

@interface KCMainViewController ()

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self drawMyLayer];
}

#pragma mark 繪制圖層
-(void)drawMyLayer{
    CGSize size=[UIScreen mainScreen].bounds.size;
    
    //獲得根圖層
    CALayer *layer=[[CALayer alloc]init];
    //設置背景顏色,由于QuartzCore是跨平臺框架,無法直接使用UIColor
    layer.backgroundColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0].CGColor;
    //設置中心點
    layer.position=CGPointMake(size.width/2, size.height/2);
    //設置大小
    layer.bounds=CGRectMake(0, 0, WIDTH,WIDTH);
    //設置圓角,當圓角半徑等于矩形的一半時看起來就是一個圓形
    layer.cornerRadius=WIDTH/2;
    //設置陰影
    layer.shadowColor=[UIColor grayColor].CGColor;
    layer.shadowOffset=CGSizeMake(2, 2);
    layer.shadowOpacity=.9;
    //設置邊框
//    layer.borderColor=[UIColor whiteColor].CGColor;
//    layer.borderWidth=1;

    //設置錨點
//    layer.anchorPoint=CGPointZero;

    [self.view.layer addSublayer:layer];

}

#pragma mark 點擊放大
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=[touches anyObject];
    CALayer *layer=self.view.layer.sublayers[0];
    CGFloat width=layer.bounds.size.width;
    if (width==WIDTH) {
        width=WIDTH*4;
    }else{
        width=WIDTH;
    }
    layer.bounds=CGRectMake(0, 0, width, width);
    layer.position=[touch locationInView:self.view];
    layer.cornerRadius=width/2;
}

@end

運行效果:

150628379878373.gif

CALayer繪圖

上一篇文章中重點討論了使用Quartz 2D繪圖,當時調用了UIView的drawRect:方法繪制圖形、圖像,這種方式本質還是在圖層中繪制,但是這里會著重介紹一下如何直接在圖層中繪圖。在圖層中繪圖的方式跟原來基本沒有區別,只是drawRect:方法是由UIKit組件進行調用,因此里面可以使用一些UIKit封裝的方法進行繪圖,而直接繪制到圖層的方法由于并非UIKit直接調用因此只能用原生的Core Graphics方法繪制。

圖層繪圖有兩種方法,不管使用哪種方法繪制完必須調用圖層的setNeedDisplay方法(注意是圖層的方法,不是UIView的方法,前面我們介紹過UIView也有此方法)

  • 通過圖層代理drawLayer: inContext:方法繪制
  • 通過自定義圖層drawInContext:方法繪制
使用代理方法繪圖

通過代理方法進行圖層繪圖只要指定圖層的代理,然后在代理對象中重寫-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx方法即可。需要注意這個方法雖然是代理方法但是不用手動實現CALayerDelegate,因為CALayer定義中給NSObject做了分類擴展,所有的NSObject都包含這個方法。另外設置完代理后必須要調用圖層的setNeedDisplay方法,否則繪制的內容無法顯示。

下面的代碼演示了在一個自定義圖層繪制一張圖像并將圖像設置成圓形,這種效果在很多應用中很常見,如最新版的手機QQ頭像就是這種效果:

#import "KCMainViewController.h"
#define PHOTO_HEIGHT 150

@interface KCMainViewController ()

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //自定義圖層
    CALayer *layer=[[CALayer alloc]init];
    layer.bounds=CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT);
    layer.position=CGPointMake(160, 200);
    layer.backgroundColor=[UIColor redColor].CGColor;
    layer.cornerRadius=PHOTO_HEIGHT/2;
    //注意僅僅設置圓角,對于圖形而言可以正常顯示,但是對于圖層中繪制的圖片無法正確顯示
    //如果想要正確顯示則必須設置masksToBounds=YES,剪切子圖層
    layer.masksToBounds=YES;
    //陰影效果無法和masksToBounds同時使用,因為masksToBounds的目的就是剪切外邊框,
    //而陰影效果剛好在外邊框
//    layer.shadowColor=[UIColor grayColor].CGColor;
//    layer.shadowOffset=CGSizeMake(2, 2);
//    layer.shadowOpacity=1;
    //設置邊框
    layer.borderColor=[UIColor whiteColor].CGColor;
    layer.borderWidth=2;
    
    //設置圖層代理
    layer.delegate=self;
    
    //添加圖層到根圖層
    [self.view.layer addSublayer:layer];
    
    //調用圖層setNeedDisplay,否則代理方法不會被調用
    [layer setNeedsDisplay];
}

#pragma mark 繪制圖形、圖像到圖層,注意參數中的ctx是圖層的圖形上下文,其中繪圖位置也是相對圖層而言的
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    //因為圖形上下文在每一時刻都有一個確定的狀態,該狀態概括了圖形上下文所有屬性的設置。為了便于操作這些狀態,圖形上下文提供了一個用來持有狀態的棧。調用CGContextSaveGState函數,上下文會將完整的當前狀態壓入棧頂;調用CGContextRestoreGState函數,上下文查找處在棧頂的狀態,并設置當前上下文狀態為棧頂狀態。
因此一般繪圖模式是:在繪圖之前調用CGContextSaveGState函數保存當前狀態,接著根據需要設置某些上下文狀態,然后繪圖,最后調用CGContextRestoreGState函數將當前狀態恢復到繪圖之前的狀態。要注意的是,CGContextSaveGState函數和CGContextRestoreGState函數必須成對出現,否則繪圖很可能出現意想不到的錯誤,這里有一個簡單的做法避免這種情況。代碼如下:
    CGContextSaveGState(ctx);

    //圖形上下文形變,解決圖片倒立的問題
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -PHOTO_HEIGHT);
    
    UIImage *image=[UIImage imageNamed:@"photo.png"];
    //注意這個位置是相對于圖層而言的不是屏幕
    CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);

//    CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100));
//    CGContextDrawPath(ctx, kCGPathFillStroke);
    
    CGContextRestoreGState(ctx);
}

@end

運行效果:

150628385812260.jpg

使用代理方法繪制圖形、圖像時在drawLayer:inContext:方法中可以通過事件參數獲得繪制的圖層和圖形上下文。在這個方法中繪圖時所有的位置都是相對于圖層而言的,圖形上下文指的也是當前圖層的圖形上下文。

需要注意的是上面代碼中繪制圖片圓形裁切效果時如果不設置masksToBounds是無法顯示圓形,但是對于其他圖形卻沒有這個限制。原因就是當繪制一張圖片到圖層上的時候會重新創建一個圖層添加到當前圖層,這樣一來如果設置了圓角之后雖然底圖層有圓角效果,但是子圖層還是矩形,只有設置了masksToBounds為YES讓子圖層按底圖層剪切才能顯示圓角效果。同樣的,有些朋友經常在網上提問說為什么使用UIImageView的layer設置圓角后圖片無法顯示圓角,只有設置masksToBounds才能出現效果,也是類似的問題。

擴展1--帶陰影效果的圓形圖片裁切

如果設置了masksToBounds=YES之后確實可以顯示圖片圓角效果,但遺憾的是設置了這個屬性之后就無法設置陰影效果。因為masksToBounds=YES就意味著外邊框不能顯示,而陰影恰恰作為外邊框繪制的,這樣兩個設置就產生了矛盾。要解決這個問題不妨換個思路:使用兩個大小一樣的圖層,下面的圖層負責繪制陰影,上面的圖層用來顯示圖片。

#import "KCMainViewController.h"
#define PHOTO_HEIGHT 150

@interface KCMainViewController ()

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    CGPoint position= CGPointMake(160, 200);
    CGRect bounds=CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT);
    CGFloat cornerRadius=PHOTO_HEIGHT/2;
    CGFloat borderWidth=2;
    
    //陰影圖層
    CALayer *layerShadow=[[CALayer alloc]init];
    layerShadow.bounds=bounds;
    layerShadow.position=position;
    layerShadow.cornerRadius=cornerRadius;
    layerShadow.shadowColor=[UIColor grayColor].CGColor;
    layerShadow.shadowOffset=CGSizeMake(2, 1);
    layerShadow.shadowOpacity=1;
    layerShadow.borderColor=[UIColor whiteColor].CGColor;
    layerShadow.borderWidth=borderWidth;
    [self.view.layer addSublayer:layerShadow];
    
    //容器圖層
    CALayer *layer=[[CALayer alloc]init];
    layer.bounds=bounds;
    layer.position=position;
    layer.backgroundColor=[UIColor redColor].CGColor;
    layer.cornerRadius=cornerRadius;
    layer.masksToBounds=YES;
    layer.borderColor=[UIColor whiteColor].CGColor;
    layer.borderWidth=borderWidth;
    
    //設置圖層代理
    layer.delegate=self;
    
    //添加圖層到根圖層
    [self.view.layer addSublayer:layer];
    
    //調用圖層setNeedDisplay,否則代理方法不會被調用
    [layer setNeedsDisplay];
}

#pragma mark 繪制圖形、圖像到圖層,注意參數中的ctx是圖層的圖形上下文,其中繪圖位置也是相對圖層而言的
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    //    NSLog(@"%@",layer);//這個圖層正是上面定義的圖層
    CGContextSaveGState(ctx);
    
    //圖形上下文形變,解決圖片倒立的問題
    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -PHOTO_HEIGHT);
    
    UIImage *image=[UIImage imageNamed:@"photo.jpg"];
    //注意這個位置是相對于圖層而言的不是屏幕
    CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);
    
    //    CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100));
    //    CGContextDrawPath(ctx, kCGPathFillStroke);
    
    CGContextRestoreGState(ctx);
}

@end

運行效果:

150628391437687.jpg
擴展2--圖層的形變

從上面代碼中大家不難發現使用Core Graphics繪制圖片時會倒立顯示,對圖層的圖形上下文進行了反轉。在前一篇文章中也采用了類似的方法去解決這個問題,但是在那篇文章中也提到過如果直接讓圖像沿著x軸旋轉180度同樣可以達到正確顯示的目的,只是當時的旋轉靠圖形上下文還無法繞x軸旋轉。今天學習了圖層之后,其實可以控制圖層直接旋轉而不用借助于圖形上下文的形變操作,而且這么操作起來會更加簡單和直觀。對于上面的程序,只需要設置圖層的transform屬性即可。需要注意的是transform是CATransform3D類型,形變可以在三個維度上進行,使用方法和前面介紹的二維形變是類似的,而且都有對應的形變設置方法(如:CATransform3DMakeTranslation()、CATransform3DMakeScale()、CATransform3DMakeRotation())。下面的代碼通過CATransform3DMakeRotation()方法在x軸旋轉180度解決倒立問題:

#import "KCMainViewController.h"
#define PHOTO_HEIGHT 150

@interface KCMainViewController ()

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    CGPoint position= CGPointMake(160, 200);
    CGRect bounds=CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT);
    CGFloat cornerRadius=PHOTO_HEIGHT/2;
    CGFloat borderWidth=2;

    //陰影圖層
    CALayer *layerShadow=[[CALayer alloc]init];
    layerShadow.bounds=bounds;
    layerShadow.position=position;
    layerShadow.cornerRadius=cornerRadius;
    layerShadow.shadowColor=[UIColor grayColor].CGColor;
    layerShadow.shadowOffset=CGSizeMake(2, 1);
    layerShadow.shadowOpacity=1;
    layerShadow.borderColor=[UIColor whiteColor].CGColor;
    layerShadow.borderWidth=borderWidth;
    [self.view.layer addSublayer:layerShadow];

    //容器圖層
    CALayer *layer=[[CALayer alloc]init];
    layer.bounds=bounds;
    layer.position=position;
    layer.backgroundColor=[UIColor redColor].CGColor;
    layer.cornerRadius=cornerRadius;
    layer.masksToBounds=YES;
    layer.borderColor=[UIColor whiteColor].CGColor;
    layer.borderWidth=borderWidth;
    
    //利用圖層形變解決圖像倒立問題
    layer.transform=CATransform3DMakeRotation(M_PI, 1, 0, 0);
    
    //設置圖層代理
    layer.delegate=self;

    //添加圖層到根圖層
    [self.view.layer addSublayer:layer];

    //調用圖層setNeedDisplay,否則代理方法不會被調用
    [layer setNeedsDisplay];
}

#pragma mark 繪制圖形、圖像到圖層,注意參數中的ctx時圖層的圖形上下文,其中繪圖位置也是相對圖層而言的
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    //    NSLog(@"%@",layer);//這個圖層正是上面定義的圖層
    UIImage *image=[UIImage imageNamed:@"photo.jpg"];
    //注意這個位置是相對于圖層而言的不是屏幕
    CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);
}

@end

事實上如果僅僅就顯示一張圖片在圖層中當然沒有必要那么麻煩,直接設置圖層contents就可以了,不牽涉到繪圖也就沒有倒立的問題了。

#import "KCMainViewController.h"
#define PHOTO_HEIGHT 150

@interface KCMainViewController ()

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    CGPoint position= CGPointMake(160, 200);
    CGRect bounds=CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT);
    CGFloat cornerRadius=PHOTO_HEIGHT/2;
    CGFloat borderWidth=2;

    //陰影圖層
    CALayer *layerShadow=[[CALayer alloc]init];
    layerShadow.bounds=bounds;
    layerShadow.position=position;
    layerShadow.cornerRadius=cornerRadius;
    layerShadow.shadowColor=[UIColor grayColor].CGColor;
    layerShadow.shadowOffset=CGSizeMake(2, 1);
    layerShadow.shadowOpacity=1;
    layerShadow.borderColor=[UIColor whiteColor].CGColor;
    layerShadow.borderWidth=borderWidth;
    [self.view.layer addSublayer:layerShadow];

    //容器圖層
    CALayer *layer=[[CALayer alloc]init];
    layer.bounds=bounds;
    layer.position=position;
    layer.backgroundColor=[UIColor redColor].CGColor;
    layer.cornerRadius=cornerRadius;
    layer.masksToBounds=YES;
    layer.borderColor=[UIColor whiteColor].CGColor;
    layer.borderWidth=borderWidth;
    //設置內容(注意這里一定要轉換為CGImage)
    UIImage *image=[UIImage imageNamed:@"photo.jpg"];
//    layer.contents=(id)image.CGImage;
    [layer setContents:(id)image.CGImage];

    //添加圖層到根圖層
    [self.view.layer addSublayer:layer];
}

@end

既然如此為什么還大費周章的說形變呢,因為形變對于動畫有特殊的意義。在動畫開發中形變往往不是直接設置transform,而是通過keyPath進行設置。這種方法設置形變的本質和前面沒有區別,只是利用了KVC可以動態修改其屬性值而已,但是這種方式在動畫中確實很常用的,因為它可以很方便的將幾種形變組合到一起使用。同樣是解決動畫旋轉問題,只要將前面的旋轉代碼改為下面的代碼即可:

[layer setValue:@M_PI forKeyPath:@"transform.rotation.x"];

當然,通過key path設置形變參數就需要了解有哪些key path可以設置,這里就不再一一列舉,大家可以參照Xcode幫助文檔中“CATransform3D Key Paths”一節,里面描述的很詳細。

使用自定義圖層繪圖

在自定義圖層中繪圖時只要自己編寫一個類繼承于CALayer然后在drawInContext:中繪圖即可。同前面在代理方法繪圖一樣,要顯示圖層中繪制的內容也要調用圖層的setNeedDisplay方法,否則drawInContext方法將不會調用。

前面的文章中曾經說過,在使用Quartz 2D在UIView中繪制圖形的本質也是繪制到圖層中,為了說明這個問題下面演示自定義圖層繪圖時沒有直接在視圖控制器中調用自定義圖層,而是在一個UIView將自定義圖層添加到UIView的根圖層中(例子中的UIView跟自定義圖層繪圖沒有直接關系)。從下面的代碼中可以看到:UIView在顯示時其根圖層會自動創建一個CGContextRef(CALayer本質使用的是位圖上下文),同時調用圖層代理(UIView創建圖層會自動設置圖層代理為其自身)的draw: inContext:方法并將圖形上下文作為參數傳遞給這個方法。而在UIView的draw:inContext:方法中會調用其drawRect:方法,在drawRect:方法中使用UIGraphicsGetCurrentContext()方法得到的上下文正是前面創建的上下文。

KCLayer.m

#import "KCLayer.h"

@implementation KCLayer

-(void)drawInContext:(CGContextRef)ctx{
    NSLog(@"3-drawInContext:");
    NSLog(@"CGContext:%@",ctx);
//    CGContextRotateCTM(ctx, M_PI_4);
    CGContextSetRGBFillColor(ctx, 135.0/255.0, 232.0/255.0, 84.0/255.0, 1);
    CGContextSetRGBStrokeColor(ctx, 135.0/255.0, 232.0/255.0, 84.0/255.0, 1);
//    CGContextFillRect(ctx, CGRectMake(0, 0, 100, 100));
//    CGContextFillEllipseInRect(ctx, CGRectMake(50, 50, 100, 100));
    CGContextMoveToPoint(ctx, 94.5, 33.5);

    //// Star Drawing
    CGContextAddLineToPoint(ctx,104.02, 47.39);
    CGContextAddLineToPoint(ctx,120.18, 52.16);
    CGContextAddLineToPoint(ctx,109.91, 65.51);
    CGContextAddLineToPoint(ctx,110.37, 82.34);
    CGContextAddLineToPoint(ctx,94.5, 76.7);
    CGContextAddLineToPoint(ctx,78.63, 82.34);
    CGContextAddLineToPoint(ctx,79.09, 65.51);
    CGContextAddLineToPoint(ctx,68.82, 52.16);
    CGContextAddLineToPoint(ctx,84.98, 47.39);
    CGContextClosePath(ctx);

    CGContextDrawPath(ctx, kCGPathFillStroke);
}

@end

KCView.m

#import "KCView.h"
#import "KCLayer.h"

@implementation KCView

-(instancetype)initWithFrame:(CGRect)frame{
    NSLog(@"initWithFrame:");
    if (self=[super initWithFrame:frame]) {
        KCLayer *layer=[[KCLayer alloc]init];
        layer.bounds=CGRectMake(0, 0, 185, 185);
        layer.position=CGPointMake(160,284);
        layer.backgroundColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0].CGColor;
        
        //顯示圖層
        [layer setNeedsDisplay];
        
        [self.layer addSublayer:layer];
    }
    return self;
}

-(void)drawRect:(CGRect)rect{
    NSLog(@"2-drawRect:");
    NSLog(@"CGContext:%@",UIGraphicsGetCurrentContext());//得到的當前圖形上下文正是drawLayer中傳遞的
    [super drawRect:rect];
    
}

-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx{
    NSLog(@"1-drawLayer:inContext:");
    NSLog(@"CGContext:%@",ctx);
    [super drawLayer:layer inContext:ctx];
    
}

@end

KCMainViewController.m

#import "KCMainViewController.h"
#import "KCView.h"

@interface KCMainViewController ()

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    KCView *view=[[KCView alloc]initWithFrame:[UIScreen mainScreen].bounds];
    view.backgroundColor=[UIColor colorWithRed:249.0/255.0 green:249.0/255.0 blue:249.0/255.0 alpha:1];
    
    
    [self.view addSubview:view];
}

@end

運行效果:

150628396905587.jpg

Core Animation

大家都知道在iOS中實現一個動畫相當簡單,只要調用UIView的塊代碼即可實現一個動畫效果,這在其他系統開發中基本不可能實現。下面通過一個簡單的UIView進行一個圖片放大動畫效果演示:

#import "KCMainViewController.h"

@interface KCMainViewController ()

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIImage *image=[UIImage imageNamed:@"open2.png"];
    UIImageView *imageView=[[UIImageView alloc]init];
    imageView.image=image;
    imageView.frame=CGRectMake(120, 140, 80, 80);
    [self.view addSubview:imageView];
    
    //兩秒后開始一個持續一分鐘的動畫
    [UIView animateWithDuration:1 delay:2 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
        imageView.frame=CGRectMake(80, 100, 160, 160);
    } completion:nil];
}

@end

使用上面UIView封裝的方法進行動畫設置固然十分方便,但是具體動畫如何實現我們是不清楚的,而且上面的代碼還有一些問題是無法解決的,例如:如何控制動畫的暫停?如何進行動畫的組合?。。。

這里就需要了解iOS的核心動畫Core Animation(包含在Quartz Core框架中)。在iOS中核心動畫分為幾類:基礎動畫、關鍵幀動畫、動畫組、轉場動畫。各個類的關系大致如下:

150628403626215.png
  • CAAnimation:核心動畫的基礎類,不能直接使用,負責動畫運行時間、速度的控制,本身實現了CAMediaTiming協議。

  • CAPropertyAnimation:屬性動畫的基類(通過屬性進行動畫設置,注意是可動畫屬性),不能直接使用。

  • CAAnimationGroup:動畫組,動畫組是一種組合模式設計,可以通過動畫組來進行所有動畫行為的統一控制,組中所有動畫效果可以并發執行。

  • CATransition:轉場動畫,主要通過濾鏡進行動畫效果設置。

  • CABasicAnimation:基礎動畫,通過屬性修改進行動畫參數控制,只有初始狀態和結束狀態。

  • CAKeyframeAnimation:關鍵幀動畫,同樣是通過屬性進行動畫參數控制,但是同基礎動畫不同的是它可以有多個狀態控制。

基礎動畫、關鍵幀動畫都屬于屬性動畫,就是通過修改屬性值產生動畫效果,開發人員只需要設置初始值和結束值,中間的過程動畫(又叫“補間動畫”)由系統自動計算產生。和基礎動畫不同的是關鍵幀動畫可以設置多個屬性值,每兩個屬性中間的補間動畫由系統自動完成,因此從這個角度而言基礎動畫又可以看成是有兩個關鍵幀的關鍵幀動畫。

基礎動畫

在開發過程中很多情況下通過基礎動畫就可以滿足開發需求,前面例子中使用的UIView代碼塊進行圖像放大縮小的演示動畫也是基礎動畫(在iOS7中UIView也對關鍵幀動畫進行了封裝),只是UIView裝飾方法隱藏了更多的細節。如果不使用UIView封裝的方法,動畫創建一般分為以下幾步:

1.初始化動畫并設置動畫屬性

2.設置動畫屬性初始值(可以省略)、結束值以及其他動畫屬性

3.給圖層添加動畫

下面以一個移動動畫為例進行演示,在這個例子中點擊屏幕哪個位置落花將飛向哪里。

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //設置背景(注意這個圖片其實在根圖層)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定義一個圖層
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
}



#pragma mark 移動動畫
-(void)translatonAnimation:(CGPoint)location{
    //1.創建動畫并指定動畫屬性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
    
    //2.設置動畫屬性初始值和結束值
//    basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不設置,默認為圖層初始狀態
    basicAnimation.toValue=[NSValue valueWithCGPoint:location];
    
    //設置其他動畫屬性
    basicAnimation.duration=5.0;//動畫時間5秒
    //basicAnimation.repeatCount=HUGE_VALF;//設置重復次數,HUGE_VALF可看做無窮大,起到循環動畫的效果
    //    basicAnimation.removedOnCompletion=NO;//運行一次是否移除動畫

    
    //3.添加動畫到圖層,注意key相當于給動畫進行命名,以后獲得該動畫時可以使用此名稱獲取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}

#pragma mark 點擊事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //創建并開始動畫
    [self translatonAnimation:location];
}

@end

運行效果:

150628410812829.gif

上面實現了一個基本動畫效果,但是這個動畫存在一個問題:動畫結束后動畫圖層回到了原來的位置,當然是用UIView封裝的方法是沒有這個問題的。如何解決這個問題呢?

前面說過圖層動畫的本質就是將圖層內部的內容轉化為位圖經硬件操作形成一種動畫效果,其實圖層本身并沒有任何的變化。上面的動畫中圖層并沒有因為動畫效果而改變它的位置(對于縮放動畫其大小也是不會改變的),所以動畫完成之后圖層還是在原來的顯示位置沒有任何變化,如果這個圖層在一個UIView中你會發現在UIView移動過程中你要觸發UIView的點擊事件也只能點擊原來的位置(即使它已經運動到了別的位置),因為它的位置從來沒有變過。當然解決這個問題方法比較多,這里不妨在動畫完成之后重新設置它的位置。

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //設置背景(注意這個圖片其實在根圖層)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定義一個圖層
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
}



#pragma mark 移動動畫
-(void)translatonAnimation:(CGPoint)location{
    //1.創建動畫并指定動畫屬性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
    
    //2.設置動畫屬性初始值和結束值
//    basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不設置,默認為圖層初始狀態
    basicAnimation.toValue=[NSValue valueWithCGPoint:location];
    
    //設置其他動畫屬性
    basicAnimation.duration=5.0;//動畫時間5秒
    //basicAnimation.repeatCount=HUGE_VALF;//設置重復次數,HUGE_VALF可看做無窮大,起到循環動畫的效果
    //    basicAnimation.removedOnCompletion=NO;//運行一次是否移除動畫
    basicAnimation.delegate=self;
    //存儲當前位置在動畫結束后使用
    [basicAnimation setValue:[NSValue valueWithCGPoint:location] forKey:@"KCBasicAnimationLocation"];
    
    //3.添加動畫到圖層,注意key相當于給動畫進行命名,以后獲得該動畫時可以使用此名稱獲取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}

#pragma mark 點擊事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //創建并開始動畫
    [self translatonAnimation:location];
}

#pragma mark - 動畫代理方法
#pragma mark 動畫開始
-(void)animationDidStart:(CAAnimation *)anim{
    NSLog(@"animation(%@) start.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    NSLog(@"%@",[_layer animationForKey:@"KCBasicAnimation_Translation"]);//通過前面的設置的key獲得動畫
}

#pragma mark 動畫結束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    _layer.position=[[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
}

@end

上面通過給動畫設置一個代理去監聽動畫的開始和結束事件,在動畫開始前給動畫添加一個自定義屬性“KCBasicAnimationLocation”存儲動畫終點位置,然后在動畫結束后設置動畫的位置為終點位置。

如果運行上面的代碼大家可能會發現另外一個問題,那就是動畫運行完成后會重新從起始點運動到終點。這個問題產生的原因就是前面提到的,對于非根圖層,設置圖層的可動畫屬性(在動畫結束后重新設置了position,而position是可動畫屬性)會產生動畫效果。解決這個問題有兩種辦法:關閉圖層隱式動畫、設置動畫圖層為根圖層。顯然這里不能采取后者,因為根圖層當前已經作為動畫的背景。

要關閉隱式動畫需要用到動畫事務CATransaction,在事務內將隱式動畫關閉,例如上面的代碼可以改為:

#pragma mark 動畫結束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    //開啟事務
    [CATransaction begin];
    //禁用隱式動畫
    [CATransaction setDisableActions:YES];
    
    _layer.position=[[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
    
    //提交事務
    [CATransaction commit];
}
補充

上面通過在animationDidStop中重新設置動畫的位置主要為了說明隱式動畫關閉和動畫事件之間傳參的內容,有朋友發現這種方式有可能在動畫運行完之后出現從原點瞬間回到終點的過程,最早在調試的時候沒有發現這個問題,這里感謝這位朋友。其實解決這個問題并不難,首先必須設置fromValue,其次在動畫開始前設置動畫position為終點位置(當然也必須關閉隱式動畫)。但是這里主要還是出于學習的目的,真正開發的時候做平移動畫直接使用隱式動畫即可,沒有必要那么麻煩。

當然上面的動畫還顯得有些生硬,因為落花飄散的時候可能不僅僅是自由落體運動,本身由于空氣阻力、外界風力還會造成落花在空中的旋轉、搖擺等,這里不妨給圖層添加一個旋轉的動畫。對于圖層的旋轉前面已經演示過怎么通過key path設置圖層旋轉的內容了,在這里需要強調一下,圖層的形變都是基于錨點進行的。例如旋轉,旋轉的中心點就是圖層的錨點。

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //設置背景(注意這個圖片其實在根圖層)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定義一個圖層
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.anchorPoint=CGPointMake(0.5, 0.6);//設置錨點
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
}



#pragma mark 移動動畫
-(void)translatonAnimation:(CGPoint)location{
    //1.創建動畫并指定動畫屬性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
    
    //2.設置動畫屬性初始值、結束值
//    basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不設置,默認為圖層初始狀態
    basicAnimation.toValue=[NSValue valueWithCGPoint:location];
    
    //設置其他動畫屬性
    basicAnimation.duration=5.0;//動畫時間5秒
    //basicAnimation.repeatCount=HUGE_VALF;//設置重復次數,HUGE_VALF可看做無窮大,起到循環動畫的效果
    //    basicAnimation.removedOnCompletion=NO;//運行一次是否移除動畫
    basicAnimation.delegate=self;
    //存儲當前位置在動畫結束后使用
    [basicAnimation setValue:[NSValue valueWithCGPoint:location] forKey:@"KCBasicAnimationLocation"];
    
    //3.添加動畫到圖層,注意key相當于給動畫進行命名,以后獲得該圖層時可以使用此名稱獲取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}

#pragma mark 旋轉動畫
-(void)rotationAnimation{
    //1.創建動畫并指定動畫屬性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    
    //2.設置動畫屬性初始值、結束值
//    basicAnimation.fromValue=[NSNumber numberWithInt:M_PI_2];
    basicAnimation.toValue=[NSNumber numberWithFloat:M_PI_2*3];
    
    //設置其他動畫屬性
    basicAnimation.duration=6.0;
    basicAnimation.autoreverses=true;//旋轉后再旋轉到原來的位置

    
    //4.添加動畫到圖層,注意key相當于給動畫進行命名,以后獲得該動畫時可以使用此名稱獲取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Rotation"];
}

#pragma mark 點擊事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //創建并開始動畫
    [self translatonAnimation:location];
    
    [self rotationAnimation];
}

#pragma mark - 動畫代理方法
#pragma mark 動畫開始
-(void)animationDidStart:(CAAnimation *)anim{
    NSLog(@"animation(%@) start.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    NSLog(@"%@",[_layer animationForKey:@"KCBasicAnimation_Translation"]);//通過前面的設置的key獲得動畫
}

#pragma mark 動畫結束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    //開啟事務
    [CATransaction begin];
    //禁用隱式動畫
    [CATransaction setDisableActions:YES];
    
    _layer.position=[[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
    
    //提交事務
    [CATransaction commit];
}

@end

上面代碼中結合兩種動畫操作,需要注意的是只給移動動畫設置了代理,在旋轉動畫中并沒有設置代理,否則代理方法會執行兩遍。由于旋轉動畫會無限循環執行(上面設置了重復次數無窮大),并且兩個動畫的執行時間沒有必然的關系,這樣一來移動停止后可能還在旋轉,為了讓移動動畫停止后旋轉動畫停止就需要使用到動畫的暫停和恢復方法。

核心動畫的運行有一個媒體時間的概念,假設將一個旋轉動畫設置旋轉一周用時60秒的話,那么當動畫旋轉90度后媒體時間就是15秒。如果此時要將動畫暫停只需要讓媒體時間偏移量設置為15秒即可,并把動畫運行速度設置為0使其停止運動。類似的,如果又過了60秒后需要恢復動畫(此時媒體時間為75秒),這時只要將動畫開始開始時間設置為當前媒體時間75秒減去暫停時的時間(也就是之前定格動畫時的偏移量)15秒(開始時間=75-15=60秒),那么動畫就會重新計算60秒后的狀態再開始運行,與此同時將偏移量重新設置為0并且把運行速度設置1。這個過程中真正起到暫停動畫和恢復動畫的其實是動畫速度的調整,媒體時間偏移量以及恢復時的開始時間設置主要為了讓動畫更加連貫。

下面的代碼演示了移動動畫結束后旋轉動畫暫停,并且當再次點擊動畫時旋轉恢復的過程(注意在移動過程中如果再次點擊屏幕可以暫停移動和旋轉動畫,再次點擊可以恢復兩種動畫。但是當移動結束后觸發了移動動畫的完成事件如果再次點擊屏幕則只能恢復旋轉動畫,因為此時移動動畫已經結束而不是暫停,無法再恢復)。

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //設置背景(注意這個圖片其實在根圖層)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定義一個圖層
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.anchorPoint=CGPointMake(0.5, 0.6);//設置錨點
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
}



#pragma mark 移動動畫
-(void)translatonAnimation:(CGPoint)location{
    //1.創建動畫并指定動畫屬性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
    
    //2.設置動畫屬性初始值、結束值
//    basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不設置,默認為圖層初始狀態
    basicAnimation.toValue=[NSValue valueWithCGPoint:location];
    
    //設置其他動畫屬性
    basicAnimation.duration=5.0;//動畫時間5秒
//    basicAnimation.repeatCount=HUGE_VALF;//設置重復次數,HUGE_VALF可看做無窮大,起到循環動畫的效果
    basicAnimation.removedOnCompletion=NO;//運行一次是否移除動畫
    basicAnimation.delegate=self;
    //存儲當前位置在動畫結束后使用
    [basicAnimation setValue:[NSValue valueWithCGPoint:location] forKey:@"KCBasicAnimationLocation"];
    
    //3.添加動畫到圖層,注意key相當于給動畫進行命名,以后獲得該圖層時可以使用此名稱獲取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}

#pragma mark 旋轉動畫
-(void)rotationAnimation{
    //1.創建動畫并指定動畫屬性
    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    
    //2.設置動畫屬性初始值、結束值
//    basicAnimation.fromValue=[NSNumber numberWithInt:M_PI_2];
    basicAnimation.toValue=[NSNumber numberWithFloat:M_PI_2*3];
    
    //設置其他動畫屬性
    basicAnimation.duration=6.0;
    basicAnimation.autoreverses=true;//旋轉后在旋轉到原來的位置
    basicAnimation.repeatCount=HUGE_VALF;//設置無限循環
    basicAnimation.removedOnCompletion=NO;
//    basicAnimation.delegate=self;

    
    //4.添加動畫到圖層,注意key相當于給動畫進行命名,以后獲得該動畫時可以使用此名稱獲取
    [_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Rotation"];
}

#pragma mark 點擊事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    UITouch *touch=touches.anyObject;
    CGPoint location= [touch locationInView:self.view];
    //判斷是否已經常見過動畫,如果已經創建則不再創建動畫
    CAAnimation *animation= [_layer animationForKey:@"KCBasicAnimation_Translation"];
    if(animation){
        if (_layer.speed==0) {
            [self animationResume];
        }else{
            [self animationPause];
        }
    }else{
        //創建并開始動畫
        [self translatonAnimation:location];
        
        [self rotationAnimation];
    }
}

#pragma mark 動畫暫停
-(void)animationPause{
    //取得指定圖層動畫的媒體時間,后面參數用于指定子圖層,這里不需要
    CFTimeInterval interval=[_layer convertTime:CACurrentMediaTime() fromLayer:nil];
    //設置時間偏移量,保證暫停時停留在旋轉的位置
    [_layer setTimeOffset:interval];
    //速度設置為0,暫停動畫
    _layer.speed=0;
}

#pragma mark 動畫恢復
-(void)animationResume{
    //獲得暫停的時間
    CFTimeInterval beginTime= CACurrentMediaTime()- _layer.timeOffset;
    //設置偏移量
    _layer.timeOffset=0;
    //設置開始時間
    _layer.beginTime=beginTime;
    //設置動畫速度,開始運動
    _layer.speed=1.0;
}

#pragma mark - 動畫代理方法
#pragma mark 動畫開始
-(void)animationDidStart:(CAAnimation *)anim{
    NSLog(@"animation(%@) start.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    NSLog(@"%@",[_layer animationForKey:@"KCBasicAnimation_Translation"]);//通過前面的設置的key獲得動畫
}

#pragma mark 動畫結束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
    
    //開啟事務
    [CATransaction begin];
    //禁用隱式動畫
    [CATransaction setDisableActions:YES];
    
    _layer.position=[[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
    
    //提交事務
    [CATransaction commit];
    
    //暫停動畫
    [self animationPause];

}

@end

運行效果:

150628422217913.gif

注意:

  • 動畫暫停針對的是圖層而不是圖層中的某個動畫。
  • 要做無限循環的動畫,動畫的removedOnCompletion屬性必須設置為NO,否則運行一次動畫就會銷毀。

關鍵幀動畫

熟悉flash開發的朋友對于關鍵幀動畫應該不陌生,這種動畫方式在flash開發中經常用到。關鍵幀動畫就是在動畫控制過程中開發者指定主要的動畫狀態,至于各個狀態間動畫如何進行則由系統自動運算補充(每兩個關鍵幀之間系統形成的動畫稱為“補間動畫”),這種動畫的好處就是開發者不用逐個控制每個動畫幀,而只要關心幾個關鍵幀的狀態即可。

關鍵幀動畫開發分為兩種形式:一種是通過設置不同的屬性值進行關鍵幀控制,另一種是通過繪制路徑進行關鍵幀控制。后者優先級高于前者,如果設置了路徑則屬性值就不再起作用。

對于前面的落花動畫效果而言其實落花的過程并不自然,很顯然實際生活中它不可能沿著直線下落,這里我們不妨通過關鍵幀動畫的values屬性控制它在下落過程中的屬性。假設下落過程如圖:

在這里需要設置四個關鍵幀(如圖中四個關鍵點),具體代碼如下(動畫創建過程同基本動畫基本完全一致):

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //設置背景(注意這個圖片其實在根圖層)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];

    //自定義一個圖層
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
    
    //創建動畫
    [self translationAnimation];
}

#pragma mark 關鍵幀動畫
-(void)translationAnimation{
    //1.創建關鍵幀動畫并設置動畫屬性
    CAKeyframeAnimation *keyframeAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
    
    //2.設置關鍵幀,這里有四個關鍵幀
    NSValue *key1=[NSValue valueWithCGPoint:_layer.position];//對于關鍵幀動畫初始值不能省略
    NSValue *key2=[NSValue valueWithCGPoint:CGPointMake(80, 220)];
    NSValue *key3=[NSValue valueWithCGPoint:CGPointMake(45, 300)];
    NSValue *key4=[NSValue valueWithCGPoint:CGPointMake(55, 400)];
    NSArray *values=@[key1,key2,key3,key4];
    keyframeAnimation.values=values;
    //設置其他屬性
    keyframeAnimation.duration=8.0;
    keyframeAnimation.beginTime=CACurrentMediaTime()+2;//設置延遲2秒執行
    
    
    //3.添加動畫到圖層,添加動畫后就會執行動畫
    [_layer addAnimation:keyframeAnimation forKey:@"KCKeyframeAnimation_Position"];
}

@end

運行效果(注意運行結束沒有設置圖層位置為動畫運動結束位置):

150628438318855.gif

上面的方式固然比前面使用基礎動畫效果要好一些,但其實還是存在問題,那就是落花飛落的路徑是直線的,當然這個直線是根據程序中設置的四個關鍵幀自動形成的,那么如何讓它沿著曲線飄落呢?這就是第二種類型的關鍵幀動畫,通過描繪路徑進行關鍵幀動畫控制。假設讓落花沿著下面的曲線路徑飄落:

當然,這是一條貝塞爾曲線,學習了前篇文章之后相信對于這類曲線應該并不陌生,下面是具體實現代碼:

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //設置背景(注意這個圖片其實在根圖層)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定義一個圖層
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
    
    //創建動畫
    [self translationAnimation];
}

#pragma mark 關鍵幀動畫
-(void)translationAnimation{
    //1.創建關鍵幀動畫并設置動畫屬性
    CAKeyframeAnimation *keyframeAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
    
    //2.設置路徑
    //繪制貝塞爾曲線
    CGPathRef path=CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, _layer.position.x, _layer.position.y);//移動到起始點
    CGPathAddCurveToPoint(path, NULL, 160, 280, -30, 300, 55, 400);//繪制二次貝塞爾曲線

    keyframeAnimation.path=path;//設置path屬性
    CGPathRelease(path);//釋放路徑對象
    //設置其他屬性
    keyframeAnimation.duration=8.0;
    keyframeAnimation.beginTime=CACurrentMediaTime()+5;//設置延遲2秒執行
    
    
    //3.添加動畫到圖層,添加動畫后就會執行動畫
    [_layer addAnimation:keyframeAnimation forKey:@"KCKeyframeAnimation_Position"];
}

@end

運行效果(注意運行結束沒有設置圖層位置為動畫運動結束位置):

150628450189925.gif

看起來動畫不會那么生硬了,但是這里需要注意,對于路徑類型的關鍵幀動畫系統是從描繪路徑的位置開始路徑,直到路徑結束。如果上面的路徑不是貝塞爾曲線而是矩形路徑那么它會從矩形的左上角開始運行,順時針一周回到左上角;如果指定的路徑是一個橢圓,那么動畫運行的路徑是從橢圓右側開始(0度)順時針一周回到右側。

補充--其他屬性

在關鍵幀動畫中還有一些動畫屬性初學者往往比較容易混淆,這里專門針對這些屬性做一下介紹。

keyTimes:各個關鍵幀的時間控制。前面使用values設置了四個關鍵幀,默認情況下每兩幀之間的間隔為:8/(4-1)秒。如果想要控制動畫從第一幀到第二針占用時間4秒,從第二幀到第三幀時間為2秒,而從第三幀到第四幀時間2秒的話,就可以通過keyTimes進行設置。keyTimes中存儲的是時間占用比例點,此時可以設置keyTimes的值為0.0,0.5,0.75,1.0(當然必須轉換為NSNumber),也就是說1到2幀運行到總時間的50%,2到3幀運行到總時間的75%,3到4幀運行到8秒結束。

caculationMode:動畫計算模式。還拿上面keyValues動畫舉例,之所以1到2幀能形成連貫性動畫而不是直接從第1幀經過8/3秒到第2幀是因為動畫模式是連續的(值為kCAAnimationLinear,這是計算模式的默認值);而如果指定了動畫模式為kCAAnimationDiscrete離散的那么你會看到動畫從第1幀經過8/3秒直接到第2幀,中間沒有任何過渡。其他動畫模式還有:kCAAnimationPaced(均勻執行,會忽略keyTimes)、kCAAnimationCubic(平滑執行,對于位置變動關鍵幀動畫運行軌跡更平滑)、kCAAnimationCubicPaced(平滑均勻執行)。

下圖描繪出了幾種動畫模式的關系(橫坐標是運行時間,縱坐標是動畫屬性[例如位置、透明度等]):

動畫組

實際開發中一個物體的運動往往是復合運動,單一屬性的運動情況比較少,但恰恰屬性動畫每次進行動畫設置時一次只能設置一個屬性進行動畫控制(不管是基礎動畫還是關鍵幀動畫都是如此),這樣一來要做一個復合運動的動畫就必須創建多個屬性動畫進行組合。對于一兩種動畫的組合或許處理起來還比較容易,但是對于更多動畫的組合控制往往會變得很麻煩,動畫組的產生就是基于這樣一種情況而產生的。動畫組是一系列動畫的組合,凡是添加到動畫組中的動畫都受控于動畫組,這樣一來各類動畫公共的行為就可以統一進行控制而不必單獨設置,而且放到動畫組中的各個動畫可以并發執行,共同構建出復雜的動畫效果。

動畫組使用起來并不復雜,首先單獨創建單個動畫(可以是基礎動畫也可以是關鍵幀動畫),然后將基礎動畫添加到動畫組,最后將動畫組添加到圖層即可。

前面關鍵幀動畫部分,路徑動畫看起來效果雖然很流暢,但是落花本身的旋轉運動沒有了,這里不妨將基礎動畫部分的旋轉動畫和路徑關鍵幀動畫進行組合使得整個動畫看起來更加的和諧、順暢。

#import "KCMainViewController.h"

@interface KCMainViewController (){
    CALayer *_layer;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //設置背景(注意這個圖片其實在根圖層)
    UIImage *backgroundImage=[UIImage imageNamed:@"background.jpg"];
    self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
    
    //自定義一個圖層
    _layer=[[CALayer alloc]init];
    _layer.bounds=CGRectMake(0, 0, 10, 20);
    _layer.position=CGPointMake(50, 150);
    _layer.contents=(id)[UIImage imageNamed:@"petal.png"].CGImage;
    [self.view.layer addSublayer:_layer];
    
    //創建動畫
    [self groupAnimation];
}

#pragma mark 基礎旋轉動畫
-(CABasicAnimation *)rotationAnimation{

    CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];

    CGFloat toValue=M_PI_2*3;
    basicAnimation.toValue=[NSNumber numberWithFloat:M_PI_2*3];

//    basicAnimation.duration=6.0;
    basicAnimation.autoreverses=true;
    basicAnimation.repeatCount=HUGE_VALF;
    basicAnimation.removedOnCompletion=NO;
    
    [basicAnimation setValue:[NSNumber numberWithFloat:toValue] forKey:@"KCBasicAnimationProperty_ToValue"];
    
    return basicAnimation;
}

#pragma mark 關鍵幀移動動畫
-(CAKeyframeAnimation *)translationAnimation{
    CAKeyframeAnimation *keyframeAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
    
    CGPoint endPoint= CGPointMake(55, 400);
    CGPathRef path=CGPathCreateMutable();
    CGPathMoveToPoint(path, NULL, _layer.position.x, _layer.position.y);
    CGPathAddCurveToPoint(path, NULL, 160, 280, -30, 300, endPoint.x, endPoint.y);
    
    keyframeAnimation.path=path;
    CGPathRelease(path);

    [keyframeAnimation setValue:[NSValue valueWithCGPoint:endPoint] forKey:@"KCKeyframeAnimationProperty_EndPosition"];
    
    return keyframeAnimation;
}

#pragma mark 創建動畫組
-(void)groupAnimation{
    //1.創建動畫組
    CAAnimationGroup *animationGroup=[CAAnimationGroup animation];
    
    //2.設置組中的動畫和其他屬性
    CABasicAnimation *basicAnimation=[self rotationAnimation];
    CAKeyframeAnimation *keyframeAnimation=[self translationAnimation];
    animationGroup.animations=@[basicAnimation,keyframeAnimation];
    
    animationGroup.delegate=self;
    animationGroup.duration=10.0;//設置動畫時間,如果動畫組中動畫已經設置過動畫屬性則不再生效
    animationGroup.beginTime=CACurrentMediaTime()+5;//延遲五秒執行
    
    //3.給圖層添加動畫
    [_layer addAnimation:animationGroup forKey:nil];
}

#pragma mark - 代理方法
#pragma mark 動畫完成
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    CAAnimationGroup *animationGroup=(CAAnimationGroup *)anim;
    CABasicAnimation *basicAnimation=animationGroup.animations[0];
    CAKeyframeAnimation *keyframeAnimation=animationGroup.animations[1];
    CGFloat toValue=[[basicAnimation valueForKey:@"KCBasicAnimationProperty_ToValue"] floatValue];
    CGPoint endPoint=[[keyframeAnimation valueForKey:@"KCKeyframeAnimationProperty_EndPosition"] CGPointValue];
    
    [CATransaction begin];
    [CATransaction setDisableActions:YES];
    
    //設置動畫最終狀態
    _layer.position=endPoint;
    _layer.transform=CATransform3DMakeRotation(toValue, 0, 0, 1);
    
    [CATransaction commit];
}

@end

運行效果:

150628465964410.gif

轉場動畫

轉場動畫就是從一個場景以動畫的形式過渡到另一個場景。轉場動畫的使用一般分為以下幾個步驟:

1.創建轉場動畫

2.設置轉場類型、子類型(可選)及其他屬性

3.設置轉場后的新視圖并添加動畫到圖層

下表列出了常用的轉場類型(注意私有API是蘋果官方沒有公開的動畫類型,但是目前通過仍然可以使用):

308E9EE8-DE59-4C26-9DCB-FB48F764DEE6.png

另外對于支持方向設置的動畫類型還包含子類型:

305C4347-E2CC-4031-9A23-035D1F5BFBE9.png

在前面的文章“IOS開發系列--無限循環的圖片瀏覽器”中為了使用UIScrollView做無限循環圖片瀏覽器花費了不少時間在性能優化上面,這里使用轉場動畫利用一個UIImageView實現一個漂亮的無限循環圖片瀏覽器。

#import "KCMainViewController.h"
#define IMAGE_COUNT 5

@interface KCMainViewController (){
    UIImageView *_imageView;
    int _currentIndex;
}

@end

@implementation KCMainViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //定義圖片控件
    _imageView=[[UIImageView alloc]init];
    _imageView.frame=[UIScreen mainScreen].applicationFrame;
    _imageView.contentMode=UIViewContentModeScaleAspectFit;
    _imageView.image=[UIImage imageNamed:@"0.jpg"];//默認圖片
    [self.view addSubview:_imageView];
    //添加手勢
    UISwipeGestureRecognizer *leftSwipeGesture=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(leftSwipe:)];
    leftSwipeGesture.direction=UISwipeGestureRecognizerDirectionLeft;
    [self.view addGestureRecognizer:leftSwipeGesture];
    
    UISwipeGestureRecognizer *rightSwipeGesture=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:@selector(rightSwipe:)];
    rightSwipeGesture.direction=UISwipeGestureRecognizerDirectionRight;
    [self.view addGestureRecognizer:rightSwipeGesture];
}

#pragma mark 向左滑動瀏覽下一張圖片
-(void)leftSwipe:(UISwipeGestureRecognizer *)gesture{
    [self transitionAnimation:YES];
}

#pragma mark 向右滑動瀏覽上一張圖片
-(void)rightSwipe:(UISwipeGestureRecognizer *)gesture{
    [self transitionAnimation:NO];
}


#pragma mark 轉場動畫
-(void)transitionAnimation:(BOOL)isNext{
    //1.創建轉場動畫對象
    CATransition *transition=[[CATransition alloc]init];
    
    //2.設置動畫類型,注意對于蘋果官方沒公開的動畫類型只能使用字符串,并沒有對應的常量定義
    transition.type=@"cube";
    
    //設置子類型
    if (isNext) {
        transition.subtype=kCATransitionFromRight;
    }else{
        transition.subtype=kCATransitionFromLeft;
    }
    //設置動畫時常
    transition.duration=1.0f;
    
    //3.設置轉場后的新視圖添加轉場動畫
    _imageView.image=[self getImage:isNext];
    [_imageView.layer addAnimation:transition forKey:@"KCTransitionAnimation"];
}

#pragma mark 取得當前圖片
-(UIImage *)getImage:(BOOL)isNext{
    if (isNext) {
        _currentIndex=(_currentIndex+1)%IMAGE_COUNT;
    }else{
        _currentIndex=(_currentIndex-1+IMAGE_COUNT)%IMAGE_COUNT;
    }
    NSString *imageName=[NSString stringWithFormat:@"%i.jpg",_currentIndex];
    return [UIImage imageNamed:imageName];
}

@end

運行效果:

150628587684058.gif

代碼十分簡單,但是效果和性能卻很驚人。當然演示代碼有限,其他動畫類型大家可以自己實現,效果都很絢麗。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,615評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,606評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,044評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,826評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,227評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,447評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,992評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,807評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,001評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,243評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,667評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,930評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,709評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,996評論 2 374

推薦閱讀更多精彩內容

  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺ios動畫全貌。在這里你可以看...
    每天刷兩次牙閱讀 8,543評論 6 30
  • 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺iOS動畫全貌。在這里你可以看...
    F麥子閱讀 5,131評論 5 13
  • 書寫的很好,翻譯的也棒!感謝譯者,感謝感謝! iOS-Core-Animation-Advanced-Techni...
    錢噓噓閱讀 2,313評論 0 6
  • Core Animation編程指南 關于Core Animation Core Animation自身并不是一個...
    錢噓噓閱讀 1,191評論 0 12
  • 概覽 在iOS中隨處都可以看到絢麗的動畫效果,實現這些動畫的過程并不復雜,今天將帶大家一窺iOS動畫全貌。在這里你...
    Yiart閱讀 3,850評論 3 34