显示动画
属性动画
通过- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key;
方法,我们可以为一个图层添加一个显示动画。但是,当这个图层并不是一个视图的图层属性的实例时,动画会发生两次,一个是由我们设置的显示动画引起的,另一个这是因为图层的隐式动画。为了避免这种情况,我们需要在配置显示动画的时候指定它的代理,并实现代理方法-animationDidStop:finished:
,在其中设置一个新的事务,并禁用图层行为。
在设置显示动画的过程中,还有一个问题就是在动画完成之后图层的状态又回到了动画之前的状态。这是因为在默认情况下,动画完成之后将彻底移除,不会在其超出时间后还修改呈现层的图层。一旦动画完成,呈现层的图层将回到模型层图层的值,而我们又没有修改图层属性的值,因此动画完成后图层显示的还是之前的状态。
这里有两种解决方案,一种是在设置动画后更新图层对应属性的值:
// Add explicit animation to a single layer
CGFloat red = rand() / (double)INT_MAX;
CGFloat green = rand() / (double)INT_MAX;
CGFloat blue = rand() / (double)INT_MAX;
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)[UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
animation.delegate = self;
[self.colorLayer addAnimation:animation forKey:nil];
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
另一种则是设置动画的 fillMode 属性为 kCAFillModeForward
以留在最终状态,并设置removedOnCompletion
为 NO 以防止它被自动移除:
// Add explicit animation to a single layer
CGFloat red = rand() / (double)INT_MAX;
CGFloat green = rand() / (double)INT_MAX;
CGFloat blue = rand() / (double)INT_MAX;
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"backgroundColor";
animation.toValue = (__bridge id)[UIColor colorWithRed:red green:green blue:blue alpha:1].CGColor;
animation.delegate = self;
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
[self.colorLayer addAnimation:animation forKey:nil];
需要注意的是:如果将已完成的动画保持在 layer 上时,会造成额外的开销,因为渲染器会去进行额外的绘画工作。
还有很重要的一点就是,当我们创建好一个动画并添加给一个layer时就立刻复制了一份,因此这个动画是可以重复添加个多个layer的。
关键帧动画
CAKeyframeAnimation
是另一种UIKit没有暴露出来但功能强大的类。和CABasicAnimation
类似,CAKeyframeAnimation
同样是CAPropertyAnimation
的一个子类,它依然作用于单一的一个属性,但是和CABasicAnimation
不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。
示例代码:
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position.x";
animation.values = @[ @0, @10, @-10, @10, @0 ];
animation.keyTimes = @[ @0, @(1 / 6.0), @(3 / 6.0), @(5 / 6.0), @1 ];
animation.duration = 0.4;
animation.additive = YES;
[form.layer addAnimation:animation forKey:@"shake"];
在使用CAKeyframeAnimation
做动画时需要注意的是CAKeyframeAnimation
并不能把当前layer的值作为第一帧动画,这就导致动画开始时会突然跳到第一帧的值,再再动画完成后恢复到原来的值,所以为了动画的平滑效果,我们需要开始和结束时的关键帧来匹配当前属性的值。
CAKeyframeAnimation
还有另外一种方式来指定动画,就是使用CGPath。path属性可以用一种直观的方式,使用Core Graphics函数定义运动序列来绘制动画。
示例代码:
- (void)viewDidLoad
{
[super viewDidLoad];
//create a path
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)];
//add the ship
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 64, 64);
shipLayer.position = CGPointMake(0, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//create the keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 4.0;
animation.path = bezierPath.CGPath;
animation.rotationMode = kCAAnimationRotateAuto;
[shipLayer addAnimation:animation forKey:nil];
}
虚拟属性
在layer中,有一种属性其实是不存在的,如:transform.rotation
,transform.rotation
和transform.rotation
等。因为CATransform3D
并不是一个对象,而是一个结构体,也没有符合KVC相关属性,它们实际上是CALayer用于处理动画变换的虚拟属性。
它们不能被直接使用。当对他们做动画时,Core Animation自动地根据通过CAValueFunction
来计算的值来更新transform
属性。
CAValueFunction
用于把我们赋给虚拟的transform.rotation简单浮点值转换成真正的用于摆放图层的CATransform3D矩阵值。我们可以通过设置CAPropertyAnimation
的valueFunction
属性来改变,这将会覆盖默认的函数。
示例代码:
- (void)viewDidLoad
{
[super viewDidLoad];
//add the ship
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 128, 128);
shipLayer.position = CGPointMake(150, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 2.0;
animation.byValue = @(M_PI * 2);
[shipLayer addAnimation:animation forKey:nil];
}
动画组
CABasicAnimation
和CAKeyframeAnimation
仅仅作用于单独的属性,而CAAnimationGroup
可以把这些动画组合在一起。CAAnimationGroup
是另一个继承于CAAnimation
的子类,它添加了一个animations
数组的属性,用来组合别的动画。
示例代码:
- (void)viewDidLoad
{
[super viewDidLoad];
//create a path
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:CGPointMake(0, 150)];
[bezierPath addCurveToPoint:CGPointMake(300, 150)
controlPoint1:CGPointMake(75, 0)
controlPoint2:CGPointMake(225, 300)];
//draw the path using a CAShapeLayer
CAShapeLayer *pathLayer = [CAShapeLayer layer];
pathLayer.path = bezierPath.CGPath;
pathLayer.fillColor = [UIColor clearColor].CGColor;
pathLayer.strokeColor = [UIColor redColor].CGColor;
pathLayer.lineWidth = 3.0f;
[self.containerView.layer addSublayer:pathLayer];
//add a colored layer
CALayer *colorLayer = [CALayer layer];
colorLayer.frame = CGRectMake(0, 0, 64, 64);
colorLayer.position = CGPointMake(0, 150);
colorLayer.backgroundColor = [UIColor greenColor].CGColor;
[self.containerView.layer addSublayer:colorLayer];
//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;
//create group animation
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.animations = @[animation1, animation2];
groupAnimation.duration = 4.0;
//add the animation to the color layer
[colorLayer addAnimation:groupAnimation forKey:nil];
}
过渡
属性动画只对layer的可动画属性起作用,如果要改变一个不可动画属性(如图片、文本),或者从层级关系中添加或移除图层,属性动画将不再起作用,这时,我们可以使用过渡来实现动画效果。
为了创建一个过渡动画,我们将使用CATransition,同样是另一个CAAnimation的子类,和别的子类不同,CATransition有一个type和subtype来标识变换效果。type属性是一个NSString类型,用来设置过渡动画效果,可以被设置成如下类型:
- kCATransitionFade
- kCATransitionMoveIn
- kCATransitionPush
- kCATransitionReveal
通过subtype我们可以控制动画的方向,它提供了如下四种类型:
- kCATransitionFromRight
- kCATransitionFromLeft
- kCATransitionFromTop
- kCATransitionFromBottom
示例代码:
- (IBAction)switchImage
{
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to imageview backing layer
[self.imageView.layer addAnimation:transition forKey:nil];
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];
}
隐式过渡
对于视图关联的图层,或者是其他隐式动画的行为,过渡的特性依然是被禁用的,但是对于我们自己创建的图层,这意味着对图层contents图片做的改动都会自动附上淡入淡出的动画。
对图层树的动画
CATransition
并不作用于指定的图层属性,这就是说我们可以在即使不能准确得知改变了什么的情况下对图层做动画,例如,在不知道UITableView哪一行被添加或者删除的情况下,直接就可以平滑地刷新它,或者在不知道UIViewController内部的视图层级的情况下对两个不同的实例做过渡动画。
在这里,我们只要将过渡动画添加到被影响图层的superlayer
,就实现了对整个图层树添加过渡动画。
对UITabBarController
添加过渡动画,示例代码:
#import "AppDelegate.h"
#import "FirstViewController.h"
#import "SecondViewController.h"
#import
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]];
UIViewController *viewController1 = [[FirstViewController alloc] init];
UIViewController *viewController2 = [[SecondViewController alloc] init];
self.tabBarController = [[UITabBarController alloc] init];
self.tabBarController.viewControllers = @[viewController1, viewController2];
self.tabBarController.delegate = self;
self.window.rootViewController = self.tabBarController;
[self.window makeKeyAndVisible];
return YES;
}
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to tab bar controller's view
[self.tabBarController.view.layer addAnimation:transition forKey:nil];
}
@end
在动画过程中取消动画
移除指定动画:
- (void)removeAnimationForKey:(NSString *)key;
移除所有动画:
- (void)removeAllAnimations;
图层时间
CAMediaTiming协议
CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。
持续和重复
duration
:设置动画迭代一次的时间
repeatCount
:设置动画迭代次数
动画时长 = duration
* repeatCount
repeatDuration
:设置动画重复时长(当时此属性值与duration的值一致时动画不重复)
autoreverses
:完成一次动画迭代后,自动后退到图层原始状态
相对时间
beginTime
:设置动画开始延迟时间
speed
:设置动画播放倍数,默认为1.0
timeOffset
:设置动画播放位置(0~1)
fillMode
fillMode
用来设置动画开始前和结束后的这段时间动画属性的值,它是一个NSString类型,可以接受如下四种常量:
- kCAFillModeForwards
- kCAFillModeBackwards
- kCAFillModeBoth
- kCAFillModeRemoved
注意,如果要图层保持动画结束时的状态,除了要设置fillMode
,还需要将removeOnCompletion
设置为NO
。另外,将已完成的动画保持在layer上,会造成额外的开销,因为渲染器需要进行额外的绘制工作。所以,最好是在为layer
添加动画的时候设置一个key,这样就可以在不需要动画的时候将它从layer
上移除。
层级关系时间
在图层树中每个图层都有一个特定的坐标系统来定义该图层和它的父图层之间的关系,动画时间同样也是如此。每个图层和动画都有一套基于它父视图的层级关系概念上的时间。改变一个layer的时间将会影响该图层和它的子图层的动画,但不会对它的父图层产生影响。这点同样适用于由CAAnimationGroup
组成的动画。
调整CALayer
或CAAnimationGroup
的duration
,repeatCount
和repeatDuration
属性不会对子图层的动画产生影响,而调整beginTime
, timeOffset
和speed
属相将会对子视图的动画也产生影响。
全局时间和局部时间
在Core Animation当中,有着全局时间和局部时间的概念。全局时间以设备时间为标准,通过如下方法可以获得:
CFTimeInterval time = CACurrentMediaTime();
局部时间是每个layer的时间体系,当beginTime
,timeOffset
或speed
属性发生改变时,局部时间可能会和全局时间不一致。通过以下方法可以在转换局部时间:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
暂停,倒放和快进
当将一个动画的speed
属性设为0,动画将暂停,但是这不会对正在动画中的layer产生影响,因为当我们将一个动画添加到一个layer上时,是将动画copy然后赋值给layer。我们可以通过-animationForKey:
获取到正在动画的layer真正的动画,但是,即使获取到了正确的动画,我们依然不能通过直接修改动画的speed
属性来让动画暂停。
当然,我们也可以在动画被移除钱获取presentation layer的属性值然后赋值给model layer。但是这样做有一个缺点就是,在之后想要再继续动画将会很复杂。
暂停动画正确地姿势是利用层级关系时间。也就是调整动画所属layer的speed
属性。
如果要调整整个app的动画速度,我们只需要如此这般:
self.window.layer.speed = 100;
手动动画
通过layer的timeOffset
属性和UIPanGestureRecognizer
手势,我们可以很容易地实现一个手动控制的动画。
缓冲
动画速度
CAMediaTimingFunction
通过设置CAAnimation
的timingFunction
属性,我们可以改变动画的缓冲函数:
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
如果要改变隐式动画的缓冲函数,我们可以使用CATransaction
的+setAnimationTimingFunction:
方法:
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
创建CAMediaTimingFunction
最简单的方法是调用+timingFunctionWithName:
的构造方法。这里传入如下几个常量之一:
- kCAMediaTimingFunctionLinear
- kCAMediaTimingFunctionEaseIn
- kCAMediaTimingFunctionEaseOut
- kCAMediaTimingFunctionEaseInEaseOut
- kCAMediaTimingFunctionDefault
UIView的动画缓冲
UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变UIView动画的缓冲选项,给options参数添加如下常量之一:
- UIViewAnimationOptionCurveEaseInOut
- UIViewAnimationOptionCurveEaseIn
- UIViewAnimationOptionCurveEaseOut
- UIViewAnimationOptionCurveLinear
示例代码:
[UIView animateWithDuration:5
delay:1
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
view.layer.backgroundColor = [UIColor redColor].CGColor;
}
completion:NULL];
缓冲和关键帧动画
CAKeyframeAnimation
有一个NSArray类型的timingFunctions
属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。