16 (OC)* UIAnimation和CoreAnimation
目录
- 一 Core Animation
- 二 核心动画
- 2.1 基础动画
- 2.2 关键帧动画
- 2.3 动画组
- 2.4 转场动画
- 2.5 逐帧动画
- 三 UIView动画封装
- 3.1 基础动画
- 3.2 弹簧动画
- 3.3 关键帧动画
- 3.4 转场动画
一 Core Animation
大家都知道在iOS中实现一个动画相当简单,只要调用UIView的块代码即可实现一个动画效果,这在其他系统开发中基本不可能实现。下面通过一个简单的UIView进行一个图片放大动画效果演示:
- (void)addAnimation {
UIImage *image=[UIImage imageNamed:@"girl"];
UIImageView *imageView=[[UIImageView alloc]init];
imageView.image=image;
imageView.frame=CGRectMake(120, 140, 80, 80);
imageView.center = self.view.center;
[self.view addSubview:imageView];
//两秒后开始一个持续一分钟的动画
[UIView animateWithDuration:1 delay:2 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
imageView.frame=CGRectMake(80, 100, 160, 160);
} completion:nil];
}
使用上面UIView封装的方法进行动画设置固然十分方便,但是具体动画如何实现我们是不清楚的,而且上面的代码还有一些问题是无法解决的,例如:如何控制动画的暂停?如何进行动画的组合?
这里就需要了解iOS的核心动画Core Animation(包含在Quartz Core框架中)
二 核心动画
在iOS中核心动画分为几类:
- 基础动画
- 关键帧动画
- 动画组
- 转场动画
- 逐帧动画
各个类的关系大致如下:
CAAnimation
:核心动画的基础类,不能直接使用,负责动画运行时间、速度的控制,本身实现了CAMediaTiming协议。CAPropertyAnimation
:属性动画的基类(通过属性进行动画设置,注意是可动画属性),不能直接使用。CAAnimationGroup
:动画组,动画组是一种组合模式设计,可以通过组合动画组来进行所有动画行为的统一控制,组中的所有动画效果可以并发执行。CATransition
:转场动画,通过滤镜进行动画效果设置。CABasicAnimation
:基础动画,通过属性修改进行动画参数控制,只有初始状态和结束状态。CAKeyframeAnimation
:关键帧动画,同样是通过属性进行动画参数控制,但是同基础动画不同的是它可以有多个状态控制。
基础动画、关键帧动画都属于属性动画,就是通过修改属性值产生动画效果,开发人员只需要设置初始值和结束值,中间的过程动画(又叫“补间动画”)由系统自动计算产生。和基础动画不同的是关键帧动画可以设置多个属性值,每两个属性中间的补间动画由系统自动完成,因此从这个角度而言基础动画又可以看成是有两个关键帧的关键帧动画。
2.1 基础动画
在开发过程中很多情况下通过基础动画就可以满足开发需求,如果不使用UIView封装的方法,动画创建一般分为以下几步:
- 1.初始化动画并设置动画属性
- 2.设置动画属性初始值(可以省略)、结束值以及其他动画属性
- 3.给图层添加动画
下面以一个移动动画为例进行演示,在这个例子中点击屏幕哪个位置落花将飞向哪里
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"基础动画";
[self addSubLayer];
}
// 添加一个图层
- (void)addSubLayer {
//设置背景(注意这个图片其实在根图层)
UIImage *backgroundImage = [UIImage imageNamed:@"women"];
self.view.backgroundColor=[UIColor colorWithPatternImage:backgroundImage];
// 自定义一个图层
_layer=[[CALayer alloc]init];
_layer.bounds=CGRectMake(0, 0, 35, 35);
_layer.position=CGPointMake(50, 150);
_layer.contents=(id)[UIImage imageNamed:@"girl"].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 = 2.0;//动画时间5秒
// basicAnimation.repeatCount=HUGE_VALF;//设置重复次数,HUGE_VALF可看做无穷大,起到循环动画的效果
basicAnimation.removedOnCompletion = NO;//运行一次是否移除动画
//3.添加动画到图层,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
[_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
}
// 指哪飞哪
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = touches.anyObject;
CGPoint point = [touch locationInView:self.view];
[self translatonAnimation:point];
}
前面说过图层动画的本质就是将图层内部的内容转化为位图经硬件操作形成一种动画效果,其实图层本身
并没有任何的变化。上面的动画中图层并没有因为动画效果而改变它的位置(对于缩放动画其大小也是不会改变的),所以动画完成之后图层还是在原来的显示位置
没有任何变化,如果这个图层在一个UIView中你会发现在UIView移动过程中你要触发UIView的点击事件也只能点击原来的位置(即使它已经运动到了别的位置),因为它的位置从来没有变过。当然解决这个问题方法比较多,这里不妨在动画完成之后重新设置它的位置。
如过给动画设置一个代理去监听动画的开始和结束事件,在动画开始前给动画添加一个自定义属性KCBasicAnimationLocation
存储动画终点位置,然后在动画结束后设置动画的位置为终点位置。
如果运行上面的代码大家可能会发现另外一个问题,那就是动画运行完成后会重新从起始点运动到终点。这个问题产生的原因就是前面提到的,对于非根图层,设置图层的可动画属性(在动画结束后重新设置了position,而position是可动画属性)会产生动画效果。解决这个问题有两种办法:关闭图层隐式动画、设置动画图层为根图层。显然这里不能采取后者,因为根图层当前已经作为动画的背景。
法一
// 设置代理
basicAnimation.delegate = self;
//存储当前位置在动画结束后使用
[basicAnimation setValue:[NSValue valueWithCGPoint:location] forKey:@"KCBasicAnimationLocation"];
#pragma mark - 动画代理方法
// 动画开始
-(void)animationDidStart:(CAAnimation *)anim{
NSLog(@"animation(%@) start.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
NSLog(@"%@",[_layer animationForKey:@"KCBasicAnimation_Translation"]);//通过前面的设置的key获得动画
}
// 动画结束
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
NSLog(@"animation(%@) stop.\r_layer.frame=%@",anim,NSStringFromCGRect(_layer.frame));
_layer.position = [[anim valueForKey:@"KCBasicAnimationLocation"] CGPointValue];
}
发现动画完毕后,马上回到起点然后直接跳转到终点。
法二
要关闭隐式动画需要用到动画事务CATransaction,在事务内将隐式动画关闭。
// 动画结束
-(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为终点位置(当然也必须关闭隐式动画)。但是这里主要还是出于学习的目的,真正开发的时候做平移动画直接使用隐式动画即可,没有必要那么麻烦。
- 图层的形变都是基于锚点进行的。例如旋转,旋转的中心点就是图层的锚点。
添加旋转动画
#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 = 2.0;
basicAnimation.autoreverses = true;//旋转后再旋转到原来的位置
//4.添加动画到图层,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
[_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Rotation"];
}
上面代码中结合两种动画操作,需要注意的是只给移动动画设置了代理,在旋转动画中并没有设置代理,否则代理方法会执行两遍。由于旋转动画会无限循环执行(上面设置了重复次数无穷大),并且两个动画的执行时间没有必然的关系,这样一来移动停止后可能还在旋转,为了让移动动画停止后旋转动画,停止就需要使用到动画的暂停和恢复方法。
下面的代码演示了移动动画结束后旋转动画暂停,并且当再次点击动画时旋转恢复的过程(注意在移动过程中如果再次点击屏幕可以暂停移动和旋转动画,再次点击可以恢复两种动画。但是当移动结束后触发了移动动画的完成事件如果再次点击屏幕则只能恢复旋转动画,因为此时移动动画已经结束而不是暂停,无法再恢复)。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch *touch = touches.anyObject;
CGPoint position = [touch locationInView:self.view];
CAAnimation *animation = [_layer animationForKey:@"ZXBasicAnimation_rotation"];
//创建并开始动画
if (animation) {
if (_layer.speed == 0) {
[self animationResume];
}else{
[self animationPause];
}
} else {
[self translatonAnimation:position];
[self rotationAnimation];
}
}
#pragma mark - 暂停 | 继续
- (void)animationPause {
//取得指定图层动画的媒体时间,后面参数用于指定子图层,这里不需要
CFTimeInterval interval = [_layer convertTime:CACurrentMediaTime() fromLayer:nil];
//设置时间偏移量,保证暂停时停留在旋转的位置
_layer.timeOffset = interval;
//速度设置为零,暂停动画
_layer.speed = 0;
}
- (void)animationResume {
//获取暂停的时间
CFTimeInterval beginTime = CACurrentMediaTime() - _layer.timeOffset;
//设置偏移量
_layer.timeOffset = 0;
//设置开始时间
_layer.beginTime = beginTime;
//设置动画速度,开始运动
_layer.speed = 1.0;
}
注意
: 动画暂停针对的是图层而不是图层中的某个动画。
要做无限循环的动画,动画的removedOnCompletion属性必须设置为NO,否则运行一次动画就会销毁。
2.2 关键帧动画
熟悉flash开发的朋友对于关键帧动画应该不陌生,这种动画方式在flash开发中经常用到。关键帧动画就是在动画控制过程中开发者指定主要的动画状态,至于各个状态间动画如何进行则由系统自动运算补充(每两个关键帧之间系统形成的动画称为“补间动画”),这种动画的好处就是开发者不用逐个控制每个动画帧,而只要关心几个关键帧的状态即可。
关键帧动画开发分为两种形式:
- 一种是通过设置不同的属性值进行关键帧控制,
- 另一种是通过绘制路径进行关键帧控制。
后者优先级高于前者,如果设置了路径则属性值就不再起作用。
在这里需要设置四个关键帧(如图中四个关键点),具体代码如下(动画创建过程同基本动画基本完全一致):
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"关键帧动画";
//设置背景(注意这个图片其实在根图层)
UIImage* backgroundImage = [UIImage imageNamed:@"snow"];
self.view.backgroundColor = [UIColor colorWithPatternImage:backgroundImage];
//自定义一个图层
self.layer = [[CALayer alloc]init];
self.layer.bounds= CGRectMake(0, 0, 10, 20);
self.layer.position = CGPointMake(50, 150);
self.layer.contents = (id)[UIImage imageNamed:@"snowflake"].CGImage;
[self.view.layer addSublayer:self.layer];
//创建动画
[self translationAnimation_values];
}
- (void)translationAnimation_values {
//1.创建关键帧动画并设置动画属性
CAKeyframeAnimation* keyFrameAnimation =[CAKeyframeAnimation animationWithKeyPath:@"position"];
//2.设置关键帧,这里有四个关键帧
NSValue* key1 = [NSValue valueWithCGPoint:self.layer.position];//对于关键帧动画初始值不能省略
NSValue* key2 = [NSValue valueWithCGPoint:CGPointMake(80, 220)];
NSValue* key3 = [NSValue valueWithCGPoint:CGPointMake(45, 320)];
NSValue* key4 = [NSValue valueWithCGPoint:CGPointMake(75, 420)];
//设置其他属性
keyFrameAnimation.values = @[key1,key2,key3,key4];
keyFrameAnimation.duration = 7;
//keyFrameAnimation.beginTime = CACurrentMediaTime() + 2;//设置延迟2秒执行
keyFrameAnimation.keyTimes = @[@(2/7.0),@(5.5/7),@(6.25/7),@1.0];
//3.添加动画到图层,添加动画后就会行动画
[self.layer addAnimation:keyFrameAnimation forKey:@"myAnimation"];
}
上面的方式固然比前面使用基础动画效果要好一些,但其实还是存在问题,那就是落花飞落的路径是直线的,当然这个直线是根据程序中设置的四个关键帧自动形成的,那么如何让它沿着曲线飘落呢?这就是第二种类型的关键帧动画,通过描绘路径进行关键帧动画控制。假设让落花沿着下面的曲线路径飘落:
- 这是一条贝塞尔曲线
- (void)translationAnimation_path {
//1.创建关键帧动画并设置动画属性
CAKeyframeAnimation* keyFrameAnimation =[CAKeyframeAnimation animationWithKeyPath:@"position"];
//2.设置路径
//贝塞尔曲线
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, self.layer.position.x, self.layer.position.y);
CGPathAddCurveToPoint(path, NULL, 160, 280, -30, 300, 55, 400);//绘制二次贝塞尔曲线
keyFrameAnimation.path = path;
CGPathRelease(path);
keyFrameAnimation.duration = 5.0;
// keyFrameAnimation.beginTime = CACurrentMediaTime() + 2;//设置延迟2秒执行
//3.添加动画到图层,添加动画后就会行动画
[self.layer addAnimation:keyFrameAnimation forKey:@"myAnimation"];
}
但是这里需要注意,对于路径类型的关键帧动画系统是从描绘路径的位置开始路径,直到路径结束。如果上面的路径不是贝塞尔曲线而是矩形路径那么它会从矩形的左上角开始运行,顺时针一周回到左上角;如果指定的路径是一个椭圆,那么动画运行的路径是从椭圆右侧开始(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(平滑均匀执行)。
2.3 动画组
实际开发中一个物体的运动往往是复合运动,单一属性的运动情况比较少,但恰恰属性动画每次进行动画设置时一次只能设置一个属性进行动画控制(不管是基础动画还是关键帧动画都是如此),这样一来要做一个复合运动的动画就必须创建多个属性动画进行组合。对于一两种动画的组合或许处理起来还比较容易,但是对于更多动画的组合控制往往会变得很麻烦,动画组的产生就是基于这样一种情况而产生的。动画组是一系列动画的组合,凡是添加到动画组中的动画都受控于动画组,这样一来各类动画公共的行为就可以统一进行控制而不必单独设置,而且放到动画组中的各个动画可以并发执行,共同构建出复杂的动画效果。
前面关键帧动画部分,路径动画看起来效果虽然很流畅,但是落花本身的旋转运动没有了,这里不妨将基础动画部分的旋转动画和路径关键帧动画进行组合使得整个动画看起来更加的和谐、顺畅。
#pragma mark - 添加基础旋转动画
- (CABasicAnimation*)rotationAnimation {
CABasicAnimation* basicAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
CGFloat toValue = M_PI_2*3;
basicAnimation.toValue = [NSNumber numberWithFloat:toValue];
basicAnimation.autoreverses = YES;
basicAnimation.repeatCount = HUGE_VAL;
basicAnimation.removedOnCompletion = NO;
[basicAnimation setValue:[NSNumber numberWithFloat:toValue] forKey:@"ZXBaiscAnimationProperty_toValue"];
return basicAnimation;
}
#pragma mark - 添加关键帧移动动画
- (CAKeyframeAnimation*)translationAnimation {
CAKeyframeAnimation* keyframeAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"];
CGPoint endPoint = CGPointMake(55, 400);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, self.layer.position.x, self.layer.position.y);
CGPathAddCurveToPoint(path, NULL, 160, 280, -30, 300, endPoint.x, endPoint.y);
keyframeAnim.path = path;
CGPathRelease(path);
[keyframeAnim setValue:[NSValue valueWithCGPoint:endPoint] forKey:@"ZXKeyFrameAnimationProperty_endPosition"];
return keyframeAnim;
}
#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;//延迟5秒执行
//3.给图层添加动画
[self.layer addAnimation:animationGroup forKey:nil];
}
#pragma mark - CAAnimationDelegate
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
CAAnimationGroup* animationGroup = (CAAnimationGroup*)anim;
CABasicAnimation* basicAnimation = (CABasicAnimation*)animationGroup.animations[0];
CAKeyframeAnimation* keyFrameAnimation = (CAKeyframeAnimation*)animationGroup.animations[1];
CGFloat toValue = [[basicAnimation valueForKey:@"ZXBaiscAnimationProperty_toValue"] floatValue];
CGPoint endPoint = [[keyFrameAnimation valueForKey:@"ZXKeyFrameAnimationProperty_endPosition"] CGPointValue];
[CATransaction begin];
[CATransaction setDisableActions:YES];
//设置动画的最终状态
self.layer.position = endPoint;
self.layer.transform = CATransform3DMakeRotation(toValue, 0, 0, 1);
[CATransaction commit];
}
2.4 转场动画
转场动画就是从一个场景以动画的形式过渡到另一个场景。转场动画的使用一般分为以下几个步骤:
- 创建转场动画
- 设置转场动类型、子类型(可选)及其他属性
- 设置转场后的新视图并添加动画到图层
这里使用转场动画利用一个UIImageView实现一个漂亮的无限循环图片浏览器。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"转场动画";
self.myImageView = [[UIImageView alloc]initWithFrame:self.view.bounds];
self.myImageView.image = [UIImage imageNamed:@"0"];
self.myImageView.contentMode = UIViewContentModeScaleAspectFit;
[self.view addSubview:_myImageView];
//添加手势
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 transitionAnimtion:YES];
}
#pragma mark - 向右滑动浏览上张图片
- (void)rightSwipe:(UISwipeGestureRecognizer*)gesture {
[self transitionAnimtion:NO];
}
#pragma mark - 添加转场动画
- (void)transitionAnimtion:(BOOL)flag {
//1.创建转场动画对象
CATransition* transition = [[CATransition alloc]init];
//2.设置动画类型,注意对于苹果官方没有公开的动画类型只能使用字符串,并没有对应的常亮使用
transition.type = @"cube";
//设置子类型
if (flag) {
transition.subtype = kCATransitionFromRight;
} else {
transition.subtype = kCATransitionFromLeft;
}
//设置动画时长
transition.duration = 1.0f;
//3.设置转场动画后,给新视图添加转场动画
self.myImageView.image = [self getImage:flag];
[self.myImageView.layer addAnimation:transition forKey:@"abc"];
}
- (UIImage*)getImage:(BOOL)flag {
if (flag) {
self.currentIndex = ++self.currentIndex % kImageCount;
} else {
self.currentIndex = --self.currentIndex % kImageCount;
}
return [UIImage imageNamed:[NSString stringWithFormat:@"%ld",self.currentIndex]];
}
代码十分简单,但是效果和性能却很惊人。当然演示代码有限,其他动画类型大家可以自己实现,效果都很绚丽。
2.5逐帧动画
前面介绍了核心动画中大部分动画类型,但是做过动画处理的朋友都知道,在动画制作中还有一种动画类型“逐帧动画”。说到逐帧动画相信很多朋友第一个想到的就是UIImageView,通过设置UIImageView的animationImages属性,然后调用它的startAnimating方法去播放这组图片。
当然这种方法在某些场景下是可以达到逐帧的动画效果,但是它也存在着很大的性能问题,并且这种方法一旦设置完图片中间的过程就无法控制了。当然,也许有朋友会想到利用iOS的定时器NSTimer定时更新图片来达到逐帧动画的效果。这种方式确实可以解决UIImageView一次性加载大量图片的问题,而且让播放过程可控,唯一的缺点就是定时器方法调用有时可能会因为当前系统执行某种比较占用时间的任务造成动画连续性出现问题。
虽然在核心动画没有直接提供逐帧动画类型,但是却提供了用于完成逐帧动画的相关对象CADisplayLink。CADisplayLink是一个计时器,但是同NSTimer不同的是,CADisplayLink的刷新周期同屏幕完全一致。例如在iOS中屏幕刷新周期是60次/秒,CADisplayLink刷新周期同屏幕刷新一致也是60次/秒,这样一来使用它完成的逐帧动画(又称为“时钟动画”)完全感觉不到动画的停滞情况。
iOS程序在运行后就进入一个消息循环中(这个消息循环称为“主运行循环”),整个程序相当于进入一个死循环中,始终等待用户输入。将CADisplayLink加入到主运行循环队列后,它的时钟周期就和主运行循环保持一致,而主运行循环周期就是屏幕刷新周期。在CADisplayLink加入到主运行循环队列后就会循环调用目标方法,在这个方法中更新视图内容就可以完成逐帧动画。
当然这里不得不强调的是逐帧动画性能势必较低,但是对于一些事物的运动又不得不选择使用逐帧动画,例如人的运动,这是一个高度复杂的运动,基本动画、关键帧动画是不可能解决的。所大家一定要注意在循环方法中尽可能的降低算法复杂度,同时保证循环过程中内存峰值尽可能低。下面以一个鱼的运动为例为大家演示一下逐帧动画。
@interface CADisplaylinkAnimationViewController ()
@property (nonatomic, strong) CALayer *fishLayer;
@property (nonatomic, assign) NSInteger index;
@property (nonatomic, strong) NSMutableArray *images;
@end
@implementation CADisplaylinkAnimationViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.title = @"逐帧动画";
//创建图像显示图层
self.fishLayer = [[CALayer alloc]init];
self.fishLayer.bounds = CGRectMake(0, 0, 250, 250);
self.fishLayer.position = CGPointMake(160, 150);
self.fishLayer.contents = (id)[UIImage imageNamed:@"timg_0001"].CGImage;
self.fishLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:self.fishLayer];
//由于鱼的图片在循环中会不断创建,而几张图片相对较小
//与其在循环中不断创建UIImage不如直接将所有图片缓存起来
self.images = [NSMutableArray array];
for (int i = 0; i < 8; i++) {
NSString* imageName = [NSString stringWithFormat:@"timg_000%i.png",i];
UIImage* image = [UIImage imageNamed:imageName];
[self.images addObject:image];
}
//定义时钟对象
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(stepFish)];
//将时钟对象加入到主运行循环RunLoop中
[displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
UIImageView* animImageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 400, 250, 250 )];
animImageView.animationImages = self.images;
animImageView.animationDuration = 0.8;
[animImageView startAnimating];
[self.view addSubview:animImageView];
}
#pragma mark 每次屏幕刷新就会执行一次此方法(每秒接近60次)
- (void)stepFish {
//定义一个变量记录执行的次数
static int s = 0;
//每秒行6次
if (++s % 6 == 0) {
UIImage* image = self.images[self.index];
self.fishLayer.contents = (id)image.CGImage;
self.index = (self.index + 1) % 8;
}
}
@end
3 UIView动画封装
有了前面核心动画的知识,相信大家开发出一般的动画效果应该不在话下。在核心动画开篇也给大家说过,其实UIView本身对于基本动画和关键帧动画、转场动画都有相应的封装,在对动画细节没有特殊要求的情况下使用起来也要简单的多。可以说在日常开发中90%以上的情况使用UIView的动画封装方法都可以搞定,因此在熟悉了核心动画的原理之后还是有必要给大家简单介绍一下UIView中各类动画使用方法的。由于前面核心动画内容已经进行过详细介绍,学习UIView的封装方法根本是小菜一碟,这里对于一些细节就不再赘述了。
3.1 基础动画
基础动画部分和前面的基础动画演示相对应,演示点击屏幕雪花飘落到鼠标点击位置的过程。注意根据前面介绍的隐式动画知识其实非根图层直接设置终点位置不需要使用UIView的动画方法也可以实现动画效果,因此这里雪花不再放到图层中而是放到了一个UIImageView中。下面的代码演示了通过block和静态方法实现动画控制的过程:
@implementation AnimationFromUIViewController {
UIImageView* _imageView;
UIImageView* _ball;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//创建图像显示空间
_imageView = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"snow"]];
_imageView.center = CGPointMake(50, 150);
[self.view addSubview:_imageView];
_ball = [[UIImageView alloc]initWithImage:[UIImage imageNamed:@"snowflake"]];
_ball.center = self.view.center;
[self.view addSubview:_ball];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
UITouch * touch = [touches anyObject];
CGPoint location = [touch locationInView:self.view];
[self startBasicAnimate:location];
[self startSpringAnimate:location];
}
- (void)startSpringAnimate:(CGPoint)location {
//创建阻尼动画
//damping:阻尼,范围0-1,阻尼越接近于0,弹性效果越明显
//velocity:弹性复位的速度
[UIView animateWithDuration:1.2 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:1.0 options:UIViewAnimationOptionCurveLinear animations:^{
_ball.center = location;
} completion:nil];
}
- (void)startBasicAnimate:(CGPoint)location {
//方法1;block方法
/*
开始动画,UIView的动画方法执行完后动画会停留在重点位置,而不需要进行任何特殊处理
duration:执行时间
delay:延迟时间
option:动画设置,列如自动恢复,匀速运动等
completion:动画完成回调方法
*/
// [UIView animateWithDuration:1.5 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
// _imageView.center = location;
// } completion:^(BOOL finished) {
// NSLog(@"animate is end");
// }];
//方法2:静态方法
//开始动画
[UIView beginAnimations:@"ZXBasicAnimation" context:nil];
[UIView setAnimationDuration:1.5];
//[UIView setAnimationDelay:1.0];//设置延迟
//[UIView setAnimationRepeatAutoreverses:NO];//是否回复
//[UIView setAnimationRepeatCount:10];//重复次数
//[UIView setAnimationStartDate:(NSDate *)];//设置动画开始运行的时间
//[UIView setAnimationDelegate:self];//设置代理
//[UIView setAnimationWillStartSelector:(SEL)];//设置动画开始运动的执行方法
//[UIView setAnimationDidStopSelector:(SEL)];//设置动画运行结束后的执行方法
_imageView.center = location;
//开始动画
[UIView commitAnimations];
}
@end
3.2 弹簧动画效果
由于在iOS开发中弹性动画使用很普遍,所以在iOS7苹果官方直接提供了一个方法用于弹性动画开发,下面简单的演示一下:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//创建图像显示控件
_imageView=[[UIImageView alloc]initWithImage:[UIImage imageNamed:@"ball"]];
_imageView.size = CGSizeMake(100, 100);
_imageView.center=CGPointMake(160, 50);
[self.view addSubview:_imageView];
}
#pragma mark 点击事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
UITouch *touch=touches.anyObject;
CGPoint location= [touch locationInView:self.view];
/*创建弹性动画
damping:阻尼,范围0-1,阻尼越接近于0,弹性效果越明显
velocity:弹性复位的速度
*/
[UIView animateWithDuration:5.0 delay:0 usingSpringWithDamping:0.1 initialSpringVelocity:1.0 options:UIViewAnimationOptionCurveLinear animations:^{
_imageView.center=location; //CGPointMake(160, 284);
} completion:nil];
}
3.3 关键帧动画
从iOS7开始UIView动画中封装了关键帧动画,下面就来看一下如何使用UIView封装方法进行关键帧动画控制,这里实现前面关键帧动画部分对于球的控制。
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//关键帧动画,options
[UIView animateKeyframesWithDuration:5.0 delay:0 options:UIViewKeyframeAnimationOptionCalculationModeLinear animations:^{
//第二关键帧(准确的说第一个关键帧是开始位置):从0秒开始持续50%的时间,也就是5*0.5 = 2.5秒
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
_imageView.center = CGPointMake(80, 220);
}];
//第三帧,从0.5*5.0秒开始,持续时间:5.0*0.25 = 1.25秒
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.25 animations:^{
_imageView.center = CGPointMake(40, 300);
}];
//第四帧,从0.75*5.0秒开始,持续时间:5.0*0.25秒
[UIView addKeyframeWithRelativeStartTime:0.75f relativeDuration:0.25 animations:^{
_imageView.center = CGPointMake(67, 400);
}];
} completion:^(BOOL finished) {
NSLog(@"animation ended");
}];
/*
options 的补充
UIViewKeyframeAnimationOptionCalculationModeLinear:连续运算模式。
UIViewKeyframeAnimationOptionCalculationModeDiscrete :离散运算模式。
UIViewKeyframeAnimationOptionCalculationModePaced:均匀执行运算模式。
UIViewKeyframeAnimationOptionCalculationModeCubic:平滑运算模式。
UIViewKeyframeAnimationOptionCalculationModeCubicPaced:平滑均匀运算模式。
*/
}
注意
:前面说过关键帧动画有两种形式,上面演示的是属性值关键帧动画,路径关键帧动画目前UIView还不支持
3.4 转场动画
从iOS4.0开始,UIView直接封装了转场动画,使用起来同样很简单。
@implementation TransitionUIAnimationViewController {
UIImageView *_imageView;
int _currentIndex;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
//定义图片控件
_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{
UIViewAnimationOptions option;
if (isNext) {
option=UIViewAnimationOptionCurveLinear|UIViewAnimationOptionTransitionFlipFromRight;
} else {
option=UIViewAnimationOptionCurveLinear|UIViewAnimationOptionTransitionFlipFromLeft;
}
[UIView transitionWithView:_imageView duration:1.0 options:option animations:^{
_imageView.image=[self getImage:isNext];
} completion:nil];
}
#pragma mark 取得当前图片
-(UIImage *)getImage:(BOOL)isNext{
if (isNext) {
_currentIndex = ++_currentIndex % IMAGE_COUNT;
} else {
_currentIndex = --_currentIndex % IMAGE_COUNT;
}
NSString *imageName=[NSString stringWithFormat:@"%i.jpg",_currentIndex];
return [UIImage imageNamed:imageName];
}
@end
上面的转场动画演示中,其实仅仅有一个视图UIImageView做转场动画,每次转场通过切换UIImageView的内容而已。如果有两个完全不同的视图,并且每个视图布局都很复杂,此时要在这两个视图之间进行转场可以使用+ (void)transitionFromView:(UIView )fromView toView:(UIView )toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(4_0)方法进行两个视图间的转场,需要注意的是默认情况下转出的视图会从父视图移除,转入后重新添加,可以通过UIViewAnimationOptionShowHideTransitionViews参数设置,设置此参数后转出的视图会隐藏(不会移除)转入后再显示。
注意
:转场动画设置参数完全同基本动画参数设置;同直接使用转场动画不同的是使用UIView的装饰方法进行转场动画其动画效果较少,因为这里无法直接使用私有API。
本文参考zmmzxxx的CALayer与iOS动画 讲解及使用,非常感谢。
- 如有错误,欢迎指正,多多点赞,打赏更佳,您的支持是我写作的动力。