@zyl06
2017-02-11T17:23:54.000000Z
字数 15208
阅读 1730
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.layer
convertTime: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.layer
convertTime: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 = [CAMediaTimingFunction
functionWithName:kCAMediaTimingFunctionEaseIn];
anim.timingFunction = [CAMediaTimingFunction
functionWithControlPoints: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.0
green:0.0
blue:0.0
alpha:1.0].CGColor;
self.actionLayerView.layer.backgroundColor = [UIColor colorWithRed:0.0
green:1.0
blue:0.0
alpha: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:red
green:green
blue:blue
alpha: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 = [CABasicAnimation
animationWithKeyPath:@"backgroundColor"];
anim.duration = 5;
self.actionLayerView.layer.actions = @{@"backgroundColor":anim};
}
属性修改代码同 示例11
NSTimer 和 CADisplayLink 都能开启计时器,并在每次计时器的回调中设置显示属性
- (void)viewDidLoad
{
[super viewDidLoad];
//adjust anchor points
self.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 timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(tick)
userInfo:nil
repeats:YES];
//set initial hand positions
[self tick];
}
- (void)tick
{
//convert time to hours, minutes and seconds
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
//calculate hour hand angle
CGFloat hourAngle = (components.hour / 12.0) * M_PI * 2.0;
//calculate minute hand angle
CGFloat minuteAngle = (components.minute / 60.0) * M_PI * 2.0;
//calculate second hand angle
CGFloat secondAngle = (components.second / 60.0) * M_PI * 2.0;
//rotate hands
self.hourHand.transform = CGAffineTransformMakeRotation(hourAngle);
self.minuteHand.transform = CGAffineTransformMakeRotation(minuteAngle);
self.secondHand.transform = CGAffineTransformMakeRotation(secondAngle);
}
- (void)animate
{
//reset ball to top of screen
self.ballView.center = CGPointMake(150, 32);
//configure the animation
self.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 timer
self.lastStep = CACurrentMediaTime();
self.timer = [CADisplayLink displayLinkWithTarget:self
selector:@selector(step:)];
//self.timer.frameInterval = 2;
[self.timer addToRunLoop:[NSRunLoop mainRunLoop]
forMode:NSDefaultRunLoopMode];
}
- (void)step:(CADisplayLink *)timer
{
//calculate time delta
CFTimeInterval 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.fromValue
toValue:self.toValue
time: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 animation
CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
animation1.keyPath = @"position";
animation1.path = bezierPath.CGPath;
animation1.rotationMode = kCAAnimationRotateAuto;
//create the color animation
CABasicAnimation *animation2 = [CABasicAnimation animation];
animation2.keyPath = @"backgroundColor";
animation2.toValue = (__bridge id)[UIColor redColor].CGColor;
注意:groupAnimation中的speed,duration的优先级低于animation1和animation2中的speed,duration
//create the position animation
CAKeyframeAnimation *animation1 = [CAKeyframeAnimation animation];
animation1.keyPath = @"position";
animation1.path = bezierPath.CGPath;
animation1.rotationMode = kCAAnimationRotateAuto;
//create the color animation
CABasicAnimation *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.layerView
duration:2
options:UIViewAnimationOptionTransitionCurlDown
animations:^{
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red
green:green
blue:blue
alpha:1.0].CGColor;
} completion:^(BOOL finished) {
// 在动画结束时,执行逻辑
}];
执行结果
说明
options
属性 : 其他可选值有 UIViewAnimationOptionTransitionNone (default), UIViewAnimationOptionTransitionFlipFromLeft, UIViewAnimationOptionTransitionFlipFromRight, UIViewAnimationOptionTransitionCurlUp, UIViewAnimationOptionTransitionCurlDown, UIViewAnimationOptionTransitionCrossDissolve, UIViewAnimationOptionTransitionFlipFromTop, UIViewAnimationOptionTransitionFlipFromBottom;
感兴趣的同学可以自行去尝试查看效果
- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
//add a custom action
CATransition *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 color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red
green:green
blue:blue
alpha: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 layer
self.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~