@zyl06
2017-02-11T09:23:54.000000Z
字数 15208
阅读 1995
iOS Animation
在游戏中,动画的主体基本上是各种精灵 (Spirit),如主人公、敌人、子弹等等。而在 iOS 系统中,各种动画的主体就是 UIView 和 CALayer
UIView : 是各种控件的基类,用于显示内容,也处理各种点击、手势操作。可以通过在 ViewController 的根 UIView 下嵌入各种 UIView 来形成一个场景树,也就是 APP 用户看到的一个页面。
CALayer : 和 UIView 的特征非常相似,是一个矩形方块,用于显示内容 (如图片、文本等);也可以相互组合形成一颗场景树;一个很大的区别就是,CALayer 并不接受用户的交互,同时 UIView 包含一个 CALayer 的属性。
CALayer在功能上是主要用于显示的,也提供了丰富的属性用于动画的执行,如背景颜色、3D 模型变换矩阵、位置、透明度等等。因此在CoreAnimation中,大部分的动画都是在CALayer上执行的。一个
CALayer分别拥有一个modelLayer和 一个presentationLayer属性,动画执行期间通过更新presentationLayer来显示动画效果,动画结束的时候通过modelLayer来显示动画的结果。
至于苹果官方为什么设计出 UIView 和 CALayer 2个看似有些类似的 class,可以参看 你给我解析清楚,都有了CALayer了,为什么还要UIView,该文讲述了CALayer和UIView各自的必要性以及苹果设计上考虑的周全性。
先直接来看一段示例代码
CABasicAnimation *anim = [CABasicAnimation animation];// 设置动画的类型为“位移动画”anim.keyPath = @"position";// 设置动画的执行时间为 10 秒anim.duration = 10;// 设置位移动画开始的起点绝对位置anim.fromValue = [NSValue valueWithCGPoint:pointSrc];// 设置位移动画结束的终点绝对位置anim.toValue = [NSValue valueWithCGPoint:pointDes];// 设置位移动画结束的终点相对起点的位置//anim.byValue = [NSValue valueWithCGPoint:CGPointMake(10, 60)];// 若为true,动画在结束的时候,会自动从CALayer上移除 anim 对象;// 若为false,动画结束的时候需要程序猿手动调用 `removeAnimationForKey` 移除//anim.removedOnCompletion = NO;// 动画执行的回调对象//anim.delegate = self;// 动画重复执行的次数,若设置为 `HUGE_VALF` 可以认为是在无限循环//anim.repeatCount = HUGE_VALF; //2;// 在设置的时间内,动画重复执行,不能和 `repeatCount` 一起使用//anim.repeatDuration = 25;// 设置结束的时候,自动执行逆动画//anim.autoreverses = YES;// 设置动画开始时间,通过在 `CALayer` 当前时间上添加值,显示延迟或者提前启动动画//anim.beginTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil] + 5;// 设置动画的偏移时间//anim.timeOffset = 5;// 设置动画的执行速度,默认为 1//anim.speed = speed;// 设置动画执行的开始和结束,是否将 `presentationLayer` 的对应动画属性设置 `modelLayer`// 可选值有kCAFillModeRemoved, kCAFillModeBackwards, kCAFillModeForwards, kCAFillModeBackwards (默认)//anim.fillMode = kCAFillModeBoth;// 设置动画执行的时间轴,通俗的讲就是设置动画执行的各个时间的速率//anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];//anim.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.2 :0.2 :0.8 :0.8];// 加动画对象添加给 `CALayer`,并开始执行动画[self.button.layer addAnimation:anim forKey:nil];
这段实例代码涵盖了
CABasicAnimation中可以设置的属性,注释中也给出了各个属性的意义
CGPoint point = self.sunImageView.layer.position;CGPoint pointSrc = CGPointMake(point.x + 20, point.y);CGPoint pointDes = CGPointMake(pointSrc.x + 120, pointSrc.y);CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position"];anim.duration = 5;anim.fromValue = [NSValue valueWithCGPoint:pointSrc];anim.toValue = [NSValue valueWithCGPoint:pointDes];

anim.repeatCount = HUGE_VALF;anim.autoreverses = true;

anim.beginTime=[self.button.layerconvertTime:CACurrentMediaTime() fromLayer:nil] - 4;

// anim.beginTime=[self.button.layer convertTime:CACurrentMediaTime() fromLayer:nil] - 4;anim.timeOffset = 4;

anim.speed = 2;

speed 和 beginTime 一起使用
anim.speed = 2;anim.beginTime=[self.button.layerconvertTime:CACurrentMediaTime() fromLayer:nil] - 1.5;

speed 和 timeOffset 一起使用
anim.speed = 2;anim.timeOffset = 1.5;

anim.fillMode = kCAFillModeRemoved;anim.beginTime=[self.button.layer convertTime:CACurrentMediaTime() fromLayer:nil] + 1.5;

anim.fillMode = kCAFillModeBackwards;anim.beginTime=[self.button.layer convertTime:CACurrentMediaTime() fromLayer:nil] + 1.5;

anim.timingFunction = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn];

anim.timingFunction = [CAMediaTimingFunctionfunctionWithControlPoints:0.5 :0.1 :0.5 :0.9];

说明:
toValue 和 byValue 不应该同时设置;
repeatCount 指动画完整执行的次数,repeatDuration 指定在一段时间内动画能重复执行,如 duration 为 5,repeatCount 为 2,则等价于 repeatDuration 为 10;
repeatCount 和 repeatDuration 不应该同时设置;
设置 beginTime 不为0,会延长或者缩短动画执行时间,但设置 timeOffset 会偏移的动画执行,但并不影响总的动画执行时间;
speed 参数能加快或者减慢动画执行速度,会影响 duration 的表现值;如 示例 5 中,duration=5; speed = 2; 则动画真正的执行时间为 2.5 秒;
见示例 6, speed 和 beginTime 一起使用,beginTime 设置提前 1.5 秒,这里的 1.5 秒是相对动画动画真正的执行时间为 2.5 秒而言,而不是 duration 参数指定的 5 秒;
见示例 7, speed 和 timeOffset 一起使用,timeOffset 设置提前 1.5 秒,这里的 1.5 秒是相对 duration 参数指定的 5 秒,而不是动画真正的执行时间为 2.5 秒而言;
见示例 8, fillMode 的默认值为 kCAFillModeRemoved,在动画开始之前等待时间内,动画主体在原位置等待;当值为 kCAFillModeBackwards,动画主体在起点位置等待;当值为 kCAFillModeForwards,动画主体在终点位置等待;kCAFillModeBoth 动画主体分别在起点和终点位置等待
见示例 9, timingFunction 的默认值为 kCAMediaTimingFunctionLinear,为动画匀速执行;其他可选值有:kCAMediaTimingFunctionEaseIn (先慢后快), kCAMediaTimingFunctionEaseOut (先快后慢), kCAMediaTimingFunctionEaseInEaseOut (先慢后快再慢), kCAMediaTimingFunctionDefault (先慢后快); 也可以使用设置 3 次 4 控制点贝塞尔曲线的中间 2 个控制点位置设置
[CATransaction begin];[CATransaction setAnimationDuration:2.0];[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]];// 动画执行结束时调用[CATransaction setCompletionBlock:^{[CATransaction begin];[CATransaction setAnimationDuration:5.0];// 动画结束的时候,修改属性 2[CATransaction commit];}];// 修改属性 1[CATransaction commit];
self.actionLayerView.layer.backgroundColor = [UIColor colorWithRed:1.0green:0.0blue:0.0alpha:1.0].CGColor;
self.actionLayerView.layer.backgroundColor = [UIColor colorWithRed:0.0green:1.0blue:0.0alpha:1.0].CGColor;

CGFloat red = arc4random() / (CGFloat)INT_MAX;CGFloat green = arc4random() / (CGFloat)INT_MAX;CGFloat blue = arc4random() / (CGFloat)INT_MAX;self.actionLayerView.layer.backgroundColor = [UIColor colorWithRed:redgreen:greenblue:bluealpha:1.0].CGColor;

说明:
UIView 内置 CALayer 进行设置背景颜色,其他并没有设置,但也能看到一个较快的动画效果,这个就是隐式动画。我们定义 CALayer 中的显示属性发生改变时所执行的动画为 actions,而系统中获取 action 的顺序如下:
当 CALayer 设置了 delegate 属性,并且 delegate 中实现了 CALayerDelegate 中的 -actionForLayer:forKey,则通过调用该方法得到 action 值。
若并没有设置 delegate 属性或者 CALayerDelegate 中并没有定义 -actionForLayer:forKey,则检查 CALayer 的 actions 字典属性,获取 action 值。
若 actions 并未定义,则检查 CALayer 的 style 属性。
若 style 属性并未定义,则 通过 + (id)defaultValueForKey:(NSString *)key 获取 action 值。
由上可知,上面代码的执行正是通过 + (id)defaultValueForKey:(NSString *)key 获取的 action 值。
- (void)viewDidLoad{[super viewDidLoad];CABasicAnimation *anim = [CABasicAnimationanimationWithKeyPath:@"backgroundColor"];anim.duration = 5;self.actionLayerView.layer.actions = @{@"backgroundColor":anim};}
属性修改代码同 示例11

NSTimer 和 CADisplayLink 都能开启计时器,并在每次计时器的回调中设置显示属性
- (void)viewDidLoad{[super viewDidLoad];//adjust anchor pointsself.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);//start timerself.timer = [NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:@selector(tick)userInfo:nilrepeats:YES];//set initial hand positions[self tick];}- (void)tick{//convert time to hours, minutes and secondsNSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];//calculate hour hand angleCGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;//calculate minute hand angleCGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;//calculate second hand angleCGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;//rotate handsself.hourHand.transform = CGAffineTransformMakeRotation(hourAngle);self.minuteHand.transform = CGAffineTransformMakeRotation(minuteAngle);self.secondHand.transform = CGAffineTransformMakeRotation(secondAngle);}

- (void)animate{//reset ball to top of screenself.ballView.center = CGPointMake(150, 32);//configure the animationself.duration = 1.0;self.timeOffset = 0.0;self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];//stop the timer if it's already running[self.timer invalidate];//start the timerself.lastStep = CACurrentMediaTime();self.timer = [CADisplayLink displayLinkWithTarget:selfselector:@selector(step:)];//self.timer.frameInterval = 2;[self.timer addToRunLoop:[NSRunLoop mainRunLoop]forMode:NSDefaultRunLoopMode];}- (void)step:(CADisplayLink *)timer{//calculate time deltaCFTimeInterval thisStep = CACurrentMediaTime();CFTimeInterval stepDuration = thisStep - self.lastStep;self.lastStep = thisStep;// 计算时间偏移self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration);// 单位化time值float time = self.timeOffset / self.duration;// 重新计算time,球到起点的距离和行程的比值time = bounceEaseOut(time);// 通过差值计算新的位置id position = [self interpolateFromValue:self.fromValuetoValue:self.toValuetime:time];// 设置球的新位置self.ballView.center = [position CGPointValue];// 当动画时间到的时候,停止计时器if (self.timeOffset >= self.duration){[self.timer invalidate];self.timer = nil;}}

可以发现,使用 NSTimer 和 CADisplayLink 都能实现相同的效果,并且编写的代码差别并不大。那么,二者之间有哪些差别呢?
NSTimer 初始化器接受调用方法逻辑之间的间隔作为它的其中一个参数,预设一秒执行 30 次; CADisplayLink 是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器 ( 60 /秒)。
NSTimer 设置 timeInterval - 时间间隔; CADisplayLink通过 frameInterval 来设置几帧调用一次函数。
NSTimer 一旦初始化它就开始运行; CADisplayLink 需要将显示链接添加到一个运行循环中。
NSTimer 的精度低,NSTimer 的触发时间到的时候,runloop 如果在阻塞状态,触发时间就会推迟到下一个 runloop 周期。并且 NSTimer 新增了 tolerance 属性,让用户可以设置可以容忍的触发的时间的延迟范围; CADisplayLink 的精度高, CADisplayLink 在正常情况下会在每次刷新结束都被调用。于是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。
NSTimer 和 CADisplayLink 都能 add 进 run loop。都能设置优先级。
NSTimer 使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用; CADisplayLink 的使用范围相对单一些,适合做 UI 的不停重绘,比如自定义动画引擎或者视频播放的渲染。
另外使用 CADisplayLink 添加至 run loop 的优先级有:
NSDefaultRunLoopMode — 标准优先级
NSRunLoopCommonModes — 优先级高于NSDefaultRunLoopMode
UITrackingRunLoopMode — 用在UIScrollView和其他控件的动画
可以控制动画的每一帧内容,动画过程可以比较灵活,也可以在动画执行过程中可以交互控制动画
需要设计函数计算每一帧内容,动画过程中各个逻辑都需要用户编写
因为
core animation中并没有专门的类来定义串行动画 (至少我前面看的时候,还没发现),所以就根据自己的粗浅理解,如何来实现串行动画
在animationDidStop:(CAAnimation*)anim finished:(BOOL)flag中触发下一个动画
在[UIView animateWithDuration:<#(NSTimeInterval)#>animations:<#^(void)animations#>completion:<#^(BOOL finished)completion#>]中的completion函数中触发下一个动画
CAAnimationGroup
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];groupAnimation.animations = @[animation1, animation2];[colorLayer addAnimation:groupAnimation forKey:nil];
其中 animation1 和 animation2 分别为位移动画和背景颜色动画
//create the position animationCAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];animation1.keyPath = @"position";animation1.path = bezierPath.CGPath;animation1.rotationMode = kCAAnimationRotateAuto;//create the color animationCABasicAnimation *animation2 = [CABasicAnimation animation];animation2.keyPath = @"backgroundColor";animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
注意:groupAnimation中的speed,duration的优先级低于animation1和animation2中的speed,duration

//create the position animationCAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];animation1.keyPath = @"position";animation1.path = bezierPath.CGPath;animation1.rotationMode = kCAAnimationRotateAuto;//create the color animationCABasicAnimation *animation2 = [CABasicAnimation animation];animation2.keyPath = @"backgroundColor";animation2.toValue = (__bridge id)[UIColor redColor].CGColor;[colorLayer addAnimation:animation1 forKey:nil];[colorLayer addAnimation:animation2 forKey:nil];
animation1和animation2中的speed或duration并不相互干涉

在 [UIView animateWithDuration:<#(NSTimeInterval)#>animations:<#^(void)animations#>completion:<#^(BOOL finished)completion#>]中的animations函数里面添加多个的目标属性
执行结果

在计时器的响应函数中不断改变ImageView的
image属性
-(void)timerHandler:(NSTimer *)timer{if (playIndex>[self.frames count]-1) {playIndex=0;}self.image=[self.frames objectAtIndex:playIndex];playIndex++;}
执行结果

catImageView.animationImages = [NSArray arrayWithObjects:[UIImage imageNamed:@"cat_stand_0.png"],[UIImage imageNamed:@"cat_stand_2.png"],nil];[catImageView setAnimationDuration:1.0f];[catImageView setAnimationRepeatCount:HUGE_VALF];[catImageView startAnimating];
执行结果

// 新建左移进入的 Transition 动画对象CATransition *transition = [CATransition animation];transition.type = kCATransitionMoveIn;transition.subtype = kCATransitionFromLeft;// 为 ImageView 的内置 CALayer 添加 Transition 动画[self.imageView.layer addAnimation:transition forKey:nil];// 修改图片UIImage *currentImage = self.imageView.image;NSUInteger index = [self.images indexOfObject:currentImage];index = (index + 1) % [self.images count];self.imageView.image = self.images[index];
执行结果

[UIView transitionWithView:self.layerViewduration:2options:UIViewAnimationOptionTransitionCurlDownanimations:^{CGFloat red = arc4random() / (CGFloat)INT_MAX;CGFloat green = arc4random() / (CGFloat)INT_MAX;CGFloat blue = arc4random() / (CGFloat)INT_MAX;self.colorLayer.backgroundColor = [UIColor colorWithRed:redgreen:greenblue:bluealpha:1.0].CGColor;} completion:^(BOOL finished) {// 在动画结束时,执行逻辑}];
执行结果

说明
options 属性 : 其他可选值有 UIViewAnimationOptionTransitionNone (default), UIViewAnimationOptionTransitionFlipFromLeft, UIViewAnimationOptionTransitionFlipFromRight, UIViewAnimationOptionTransitionCurlUp, UIViewAnimationOptionTransitionCurlDown, UIViewAnimationOptionTransitionCrossDissolve, UIViewAnimationOptionTransitionFlipFromTop, UIViewAnimationOptionTransitionFlipFromBottom;
感兴趣的同学可以自行去尝试查看效果
- (void)viewDidLoad{[super viewDidLoad];//create sublayerself.colorLayer = [CALayer layer];self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;//add a custom actionCATransition *transition = [CATransition animation];transition.type = kCATransitionPush;transition.subtype = kCATransitionFromLeft;transition.duration = 1;//transition.repeatCount = 2;//transition.repeatDuration = 2;//transition.beginTime//transition.timeOffset//transition.timingFunction//transition.autoreverses = YES;//transition.removedOnCompletion = NO; //invalid//transition.fillMode = kCAFillModeBoth; //invalid//transition.speed = 2;self.colorLayer.actions = @{@"backgroundColor": transition};//add it to our view[self.layerView.layer addSublayer:self.colorLayer];}- (IBAction)changeColor{//randomize the layer background colorCGFloat red = arc4random() / (CGFloat)INT_MAX;CGFloat green = arc4random() / (CGFloat)INT_MAX;CGFloat blue = arc4random() / (CGFloat)INT_MAX;self.colorLayer.backgroundColor = [UIColor colorWithRed:redgreen:greenblue:bluealpha:1.0].CGColor;}
执行结果
type = kCATransitionPush

type = kCATransitionFade

type = kCATransitionMoveIn

type = kCATransitionReveal

其他属性前面在属性动画中已经讲述过,这边就不再赘述
在 Core Animation 中通过 CAEmitterLayer 和 CAEmitterCell 来实现粒子动画
其中 CAEmitterLayer 是 CALayer 的子类,是一个拥有高性能的例子系统的显示容器。通过定义不同类型的 CAEmitterCell 实例,并添加至 CAEmitterLayer 来显示如火焰、雪花等等的粒子效果
- (void) viewDidLoad{[super viewDidLoad];CGRect viewBounds = self.view.layer.bounds;// 创建 emitter layerself.fireEmitter = [CAEmitterLayer layer];// 设置粒子发射器的位置self.fireEmitter.emitterPosition = CGPointMake(viewBounds.size.width/2.0, viewBounds.size.height - 60);// 设置粒子发射器的尺寸self.fireEmitter.emitterSize = CGSizeMake(45, 0);// 设置粒子从粒子发射器的形状外围生成self.fireEmitter.emitterMode = kCAEmitterLayerOutline;// 设置粒子发射器的形状为直线self.fireEmitter.emitterShape = kCAEmitterLayerLine;// 使用 `kCAEmitterLayerAdditive` 参数,使粒子重叠部分增加亮度,创建火焰中心“亮”的效果self.fireEmitter.renderMode = kCAEmitterLayerAdditive;// 创建粒子发射单元CAEmitterCell* fire = [CAEmitterCell emitterCell];[fire setName:@"fire"];// 设置粒子创建的速度fire.birthRate = 450;// 设置粒子的发射方向 (经度值)fire.emissionLongitude = M_PI;// 设置粒子发射方向的(纬度值)//fire.emissionLatitude = M_PI;// 设置粒子发射的初始速度fire.velocity = -80;// 设置粒子发射的初始速度范围fire.velocityRange = 30;// 设置粒子发射的方向角度范围fire.emissionRange = 1.1;// 设置粒子发射后的 y 方向的加速度,火苗越向上越快fire.yAcceleration = -200;// 设置粒子发射后的尺寸变化速度,火苗越来越小fire.scaleSpeed = 0.3;// 设置粒子发射后的生命周期处置fire.lifetime = 0.9;// 设置粒子发射后的生命周期范围fire.lifetimeRange = 0.35;fire.color = [[UIColor colorWithRed:0.8 green:0.4 blue:0.2 alpha:0.1] CGColor];// 设置粒子的内容图片fire.contents = (id) [[UIImage imageNamed:@"DazFire"] CGImage];// 将粒子发射器添加至 `CAEmitterLayer`self.fireEmitter.emitterCells = [NSArray arrayWithObject:fire];// `CAEmitterLayer` 添加至场景树中[self.view.layer addSublayer:self.fireEmitter];}
执行结果

大概简单的总结了下 Core Animation 中定义的常用动画,当然还是有些属性和细节等还未介绍,感兴趣的 iOS 同学可以深入去了解下;
除此之外,还有其他的,如使用二维物理引擎实现的物理动画,使用 CAEAGLLayer 显示的OpenGL三位动画等,这里并没有介绍,以后可能会介绍吧O(∩_∩)O~