Core Animation
大家都知道在iOS中实现一个动画相当简单,只要调用UIView的块代码即可实现一个动画效果,这在其他系统开发中基本不可能实现。下面通过一个简单的UIView进行一个图片放大动画效果演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
//
// KCMainViewController.m
// Animation
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#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中核心动画分为几类:基础动画、关键帧动画、动画组、转场动画。各个类的关系大致如下:
CAAnimation:核心动画的基础类,不能直接使用,负责动画运行时间、速度的控制,本身实现了CAMediaTiming协议。
CAPropertyAnimation:属性动画的基类(通过属性进行动画设置,注意是可动画属性),不能直接使用。
CAAnimationGroup:动画组,动画组是一种组合模式设计,可以通过动画组来进行所有动画行为的统一控制,组中所有动画效果可以并发执行。
CATransition:转场动画,主要通过滤镜进行动画效果设置。
CABasicAnimation:基础动画,通过属性修改进行动画参数控制,只有初始状态和结束状态。
CAKeyframeAnimation:关键帧动画,同样是通过属性进行动画参数控制,但是同基础动画不同的是它可以有多个状态控制。
基础动画、关键帧动画都属于属性动画,就是通过修改属性值产生动画效果,开发人员只需要设置初始值和结束值,中间的过程动画(又叫“补间动画”)由系统自动计算产生。和基础动画不同的是关键帧动画可以设置多个属性值,每两个属性中间的补间动画由系统自动完成,因此从这个角度而言基础动画又可以看成是有两个关键帧的关键帧动画。
基础动画
在开发过程中很多情况下通过基础动画就可以满足开发需求,前面例子中使用的UIView代码块进行图像放大缩小的演示动画也是基础动画(在iOS7中UIView也对关键帧动画进行了封装),只是UIView装饰方法隐藏了更多的细节。如果不使用UIView封装的方法,动画创建一般分为以下几步:
1.初始化动画并设置动画属性
2.设置动画属性初始值(可以省略)、结束值以及其他动画属性
3.给图层添加动画
下面以一个移动动画为例进行演示,在这个例子中点击屏幕哪个位置落花将飞向哪里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
//
// KCMainViewController.m
// Animation
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#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
|
运行效果
上面实现了一个基本动画效果,但是这个动画存在一个问题:动画结束后动画图层回到了原来的位置,当然是用UIView封装的方法是没有这个问题的。如何解决这个问题呢?
前面说过图层动画的本质就是将图层内部的内容转化为位图经硬件操作形成一种动画效果,其实图层本身并没有任何的变化。上面的动画中图层并没有因为动画效果而改变它的位置(对于缩放动画其大小也是不会改变的),所以动画完成之后图层还是在原来的显示位置没有任何变化,如果这个图层在一个UIView中你会发现在UIView移动过程中你要触发UIView的点击事件也只能点击原来的位置(即使它已经运动到了别的位置),因为它的位置从来没有变过。当然解决这个问题方法比较多,这里不妨在动画完成之后重新设置它的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
//
// KCMainViewController.m
// Animation
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#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,在事务内将隐式动画关闭,例如上面的代码可以改为:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#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设置图层旋转的内容了,在这里需要强调一下,图层的形变都是基于锚点进行的。例如旋转,旋转的中心点就是图层的锚点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
//
// KCMainViewController.m
// Animation
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#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。这个过程中真正起到暂停动画和恢复动画的其实是动画速度的调整,媒体时间偏移量以及恢复时的开始时间设置主要为了让动画更加连贯。
下面的代码演示了移动动画结束后旋转动画暂停,并且当再次点击动画时旋转恢复的过程(注意在移动过程中如果再次点击屏幕可以暂停移动和旋转动画,再次点击可以恢复两种动画。但是当移动结束后触发了移动动画的完成事件如果再次点击屏幕则只能恢复旋转动画,因为此时移动动画已经结束而不是暂停,无法再恢复)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
|
//
// KCMainViewController.m
// Animation
//
// Created by Kenshin Cui on 14-3-22.
// Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//
#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
|
运行效果
注意:
动画暂停针对的是图层而不是图层中的某个动画。
要做无限循环的动画,动画的removedOnCompletion属性必须设置为NO,否则运行一次动画就会销毁。