【iOS小结】Core Animation(中)─── 动画
前面我们介绍了Core Animation框架中的图层,接下来我们来学习动画部分,动画是Core Animation一个显著的特性。要注意的是,Core Animation的动画执行过程都是在后台操作的,不会阻塞主线程。且Core Animation是直接作用在CALayer上的,并非UIView。
Core Animation对CALayer所有的可动画属性做动画。动画不需要我们在Core Animation手动打开,除非明确关闭,否则它一直存在。这也是存在隐式动画的原因(当然也有显式动画)。
我们先从隐式动画来了解动画的本质,再进一步学习显式动画。
一. 隐式动画
隐式动画是因为我们没有指定动画类型,仅仅改变一个属性,然后Core Animation决定如何并且何时去执行动画,实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。
事务
事务是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性不会马上改变,而是当事务提交的时候开始用一个动画过渡到新值。
事务是通过CATransaction类管理的,你可以把CATransaction当成一个事务栈(管理类)。CATransaction没有属性和实例方法,也不能被主动创建,只能用+began和+commit分别来入栈和出栈。
任何可以做动画的图层属性都会被添加到栈顶的事务,事务默认的动画时间是0.25秒(你也可以重新设置,不过建议起一个新的事务,以免影响该事务的其他动画)。
在某次RunLoop中设置了一个可动画图层属性,如果当前没有设置事务,Core Animation会自动创建一个事务,并在当前线程的下一个RunLoop中commit这个事务。这个也是常说的隐形事务(没有主动去创建),当然显示事务就是通过明确的调用begin,commit来提交动画。
实际上,我们常用的UIView的-animateWithDuration:animations:也是一个原理,CATransaction的+began和+commit会在-animateWithDuration:animations:内部自动调用,这样block中所有属性的改变都会被事务所包含。另外CATranscation也提供了一个完成块的接口,类似于UIView的-animateWithDuration:animations:completion:。完成块是在该个事务提交并出栈后才被执行的(即上一个任务完成),然后添加进默认的事务(时间0.25s)。
- (void)createCATransaction{
//开始一个新事务
[CATransaction begin];
//设置该事务时间为2s
[CATransaction setAnimationDuration:2.0];
//添加事务完成后执行的Block(该Block不在该事务执行)
[CATransaction setCompletionBlock:^{
//旋转90度--------任务2(耗时0.25s)
CGAffineTransform transform = self.colorLayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
self.colorLayer.affineTransform = transform;
}];
//随机切换颜色--------任务1执行(耗时2s)再执行任务2(耗时0.25s)
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;
//提交事务
[CATransaction commit];
}
图层行为
当我们修改UIView关联的图层上的可动画属性时,会发现没有平滑过渡的动画。因为UIView把它关联的图层的隐式动画特性关闭了。
我们先来了解一些隐式动画在视图上是怎么实现的。我们把改变属性时CALayer自动应用的动画称为行为,当CALayer的属性被改变时,它会调用-actionForKey:方法,传递属性名称,获取动画。
具体的步骤如下:
① 图层首先检测它是否有委托,并且是否实现CALayerDelegate的- actionForLayer:forKey。如果有,直接调用并返回结果。
② 如果没有委托,或委托没有实现- actionForLayer:forKey方法,图层会接着检查属性名称对应行为映射的actions字典。
③ 如果actions字典没有包含对应的属性,图层接着在stytle字典搜索属性名。
④ 如果stytle字典也找不到对应的行为,图层将直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
而UIView就是通过返回 - actionForLayer:forKey为 nil 来禁用隐式动画。当然在动画块中(事务中),会根据属性返回行为( 行为通常是被Core Animation隐式调用的显式动画对象,比如CABasicAnimation),然后CALayer拿这个结果去对先前和当前的值做动画。
//传入属性,获取行为
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
当然,禁用隐式动画,也可以通过设置CATransacition,用来对里面所有属性关闭开启隐式动画。
[CATransaction begin];
[CATransacition setDisableActions:YES];
...
[CATransaction commit];
总结一下:
- UIView关联的图层禁用了隐式动画,对这种图层做动画主要有三种方式:
① 使用UIView的动画函数(而不是依赖CATransaction)
② 继承UIView,并覆盖 - actionForLayer:forKey。
③ 直接创建一个显式动画。 - 对于单独的图层,我们可以通过实现图层的- actionForLayer:forKey委托方法,或者提供actions字典来控制隐式动画。
呈现过程
当改变图层的属性时,属性值是马上更新的,但屏幕上没有马上改变。这是因为设置的属性没有直接调整图层的外观,它只是定义图层动画结束之后要展示的外观。
当设置CALayer的属性,实际上是在定义当前事务结束之后图层如何显示的模型。这边可以看做一个MVC,Core Animation扮演控制器的角色,负责根据图层行为和事务设置去不断更新视图这些属性在屏幕上的变化,CALayer是存储视图如何显示和动画的模型,用户界面就是MVC中的view。在苹果开发文档中,图层树通常都是值的图层树模型。
在iOS中,屏幕每秒重绘60次,如果动画时长大于1/60秒,Core Animation就要设置一个中间值(显示值)来对屏幕上的图层重新组织。每个图层属性的显示值被存储在呈现图层中,这个呈现图层是独立的,实际上是模型图层(原来的图层)的复制。你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值。对应的还有呈现树,呈现树由图层树中所有图层的呈现图层形成。有点需要注意的是,呈现图层仅当图层首次被提交的时候创建,之前的话是nil。
图层调用-presentationLayer返回呈现图层,呈现图层调用-modelLayer返回原本的图层(模型图层)。
大多数情况下,我们不需要访问呈现图层,只需要和模型图层进行交换。但以下情况呈现图层却很重要:
- 实现基于定时器的动画。准确知道某一时刻图层显示在什么位置对正确摆放图层很有用。
- 想让做动画的图层响应用户输入。这是要对呈现图层使用-hitTest:,因为这代表了用户看到的图层位置。
@interface ViewController ()
@property (nonatomic, strong) CALayer *colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a red layer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2,
self.view.bounds.size.hei self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self.view];
//check if we've tapped the moving layer
if ([self.colorLayer.presentationLayer hitTest:point]) {
//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].
} else {
//otherwise (slowly) move the layer to new position
[CATransaction begin];
[CATransaction setAnimationDuration:4.0];
self.colorLayer.position = point;
[CATransaction commit];
}
}
@end
二. 显式动画
显式和隐式的区别,无非就是隐式的系统帮我们做了,而显式的需要我们自己去调用,隐式的行为也是调用显式动画对象(比如CABasicAnimation)。
要想执行动画,就必须初始化一个CAAnimation对象。CAAnimation是所有动画类的父类,但是它不能直接使用,应该使用它的子类。
CAAnimation对象.png
CAAnimation有三个子类:属性动画(该类不能直接用,需要用它的子类),动画组,过渡动画。我们从这几个子类一一介绍起。
① 属性动画(CAPropertyAnimation)
CAPropertyAnimation是CAAnimation的子类,但是不能直接使用,要想创建动画对象,应该使用它的两个子类:基础动画(CABasicAnimation)和关键帧动画(CAKeyframeAnimation)。
它有个NSString类型的keyPath属性,你可以指定CALayer的某个属性名为keyPath,并且对CALayer的这个属性的值进行修改,达到相应的动画效果。比如,指定@"position"为keyPath,就会修改CALayer的position属性的值,以达到平移的动画效果。也可以设置子属性或者虚拟属性(比如“transform.rotation”)。主要根据情况来,比如使用“transform.rotation”就不会和“transform.position”或者“transform.scale”冲突,相对于“transform”只需要给toValue赋一个简单的数值。
基础动画
使用CABasicAnimation可以实现一些基本的动画效果,它可以让CALayer的某个属性从某个值渐变到另一个值。不过执行动画后keyPath的值会迅速回到原始值,需要我们手动去设置。一种是使用fillMode。
- (void)createCABasicAnimation{
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
CABasicAnimation *baseicAnimation = [CABasicAnimation animation];
baseicAnimation.keyPath = @"backgroundColor";
baseicAnimation.toValue = (__bridge id _Nullable)(color.CGColor);
//保持动画执行后的状态
baseicAnimation.removedOnCompletion = NO;
baseicAnimation.fillMode = kCAFillModeForwards;
[self.colorLayer addAnimation:baseicAnimation forKey:nil];
}
一种是在-animationDidStop:finished:方法中设置该值(遵守CAAnimationDelegate协议),这种方法相对麻烦多,多个属性情况下需要判断。
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag {
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue;
[CATransaction commit];
}
关键帧动画
相较于CABasicAnimation简单的功能,CAKeyframeAnimation虽然也是作用于单一元素,但它不限制于设置一个其实和结束的值,而是可以根据一连串随意的值来做动画。它主要有两种运用:一种是设置values:
- (void)createCAKeyframeAnimation{
CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animation];
keyAnimation.duration = 3;
keyAnimation.keyPath = @"backgroundColor";
//需要说明的是,动画开始时它会突然变到第一个颜色,因为它把第一个颜色当成第一帧,动画结束时又会突然恢复原来的值。
//所以为了动画的平滑特性,关键帧的值要设置好。
keyAnimation.values = @[(__bridge id)[UIColor blueColor].CGColor,
(__bridge id)[UIColor yellowColor].CGColor,
(__bridge id)[UIColor greenColor].CGColor,
(__bridge id)[UIColor blueColor].CGColor
];
[self.colorLayer addAnimation:keyAnimation forKey:nil];
}
另一种是设置path(比如view根据path做平移动画):
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 4.0;
animation.path = bezierPath.CGPath;//设置路径
animation.rotationMode = kCAAnimationRotateAuto;//根据曲线的切线自动旋转
[shipLayer addAnimation:animation forKey:nil];
② 动画组(CAAnimationGroup)
CAAnimationGroup添加了一个animations数组的属性,用来组合别的动画。
CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
groupAnimation.animations = @[animation1, animation2];
groupAnimation.duration = 4.0;
[colorLayer addAnimation:groupAnimation forKey:nil];
③ 过渡动画(CATransition)
属性动画只对图层的可动画属性起作用,如果要改变一个不可动画的属性,或者从层级关系中移除添加图层,就需要用到CATransition。CATransition主要用于过渡,其中有两个重要的属性:type和subtype。
//type有四种类型:
kCATransitionFade;//默认值,淡入淡出
kCATransitionPush;//从一侧滑动进来,把旧图层推出去
kCATransitionMoveIn;//从顶部滑动进入
kCATransitionReveal;//把原始图层推出来来显示新图层
//subtype也有四个方法,主要来控制type的方向
kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom
有一点特别的是,例子中的CATransition只用于更换图片。另外在无论有没有设置key,它的key都是“transition”。其实,在系统中,设置layer的contens属性时,CATransition是默认的行为,也就是说,系统加上了隐式过渡行为。对于UIView,这个特性也像其他隐式动画一样被禁用。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CATransition *transition = [CATransition animation];
transition.type = kCATransitionMoveIn;
transition.subtype = kCATransitionFromBottom;
transition.duration = 2;
[self.colorLayer addAnimation:transition forKey:nil];
UIImage *image = [UIImage imageNamed:@"person"];
self.colorLayer.contents = (__bridge id)image.CGImage;
}
CATransition除了会对不可动画属性做过渡动画外,也可以对图层树做动画。比如给UITabBarController切换标签时加入淡入淡出的效果。
#import "AppDelegate.h"
#import "FirstViewController.h"
#import "SecondViewController.h"
#import <QuartzCore/QuartzCore.h>
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launch {
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:(UIViewContro {
//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
UIKit也有提供对应的方法来做过渡动画。 我们可以通过UIView的+transitionFromView:toView:duration:options:completion:和*+transitionWithView:duration:opt ions:animations: *来实现。
[UIView transitionWithView:self.view duration:2 options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{
UIImage *image = [UIImage imageNamed:@"person"];
self.view.layer.contents = (__bridge id)image.CGImage;
} completion:nil];
通过上面对CAAnimation子类的学习,我们了解了各个子类的特点和应用范围。以下我们再介绍几个它们共用的特性,也就是CAAnimation的某些特点:
Key的用途(标识符,移除图层)
当使用addAnimation:forKey:把动画添加到图层时,这个key是为了给动画取个标识符,也可以用animationKeys获取图层的所有动画标识符。
我们可以用animationForKey:来获取key对应的动画,因为不能在动画执行过程中修改动画,所以这个方法主要用来检测动画的属性,判断它是否被添加到图层中。
另外,为了终止动画,可以把它从图层中移除:
//通过key移除某个动画
- (void)removeAnimationForKey:(NSString *)key;
//移除该图层所有的动画
- (void)removeAllAnimations;
动画一旦被移除,图层的外观就立刻更新到当前的模型图层的值。一般来说,动画结束后会被自动移除,除非设置removedOnCompletion的值为NO。当你设置动画在结束后不被自动移除,那么当它不需要时你要手动移除,否则它会存在内存中,直到图层被销毁。
CAMediaTiming协议
CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以可以通过这两个类来控制时间。
其中CAMediaTiming协议有这么几个常用的属性:
- duration
动画时间。(默认值是0,不代表是0秒,而是代表着0.25秒) - repeatCount
重复次数。(默认值是0,不代表是0次,而是代表着1次) - repeatDuration
重复一个指定的时间。(比如duration为2,repeatDuration为10,代表执行总时间10s,重复5次) - autoreverses
在每次间隔交替循环过程中自动回放。(比如开门的动画,设置autoreverses为YES,门打开后会自动关闭)
以下属性主要用于相对时间:
- beginTime
动画开始之前的延迟时间。(这里的延迟从动画添加到可见图层的那一刻开始,默认是0,就是立刻开始执行) - timeOffset
让动画快进到某个时间点。(比如一个duration为1的动画,timeOffset为0.5,意思是从一半的地方开始执行,跳过前面那段的动画) - speed
speed是一个执行速度,默认是1。(比如一个duration为1的动画,speed为2,实际上动画0.5秒就可以完成,假如timeOffset为0.5,意味着动画从结束的位置开始,也就是说timeOffset其实不受speed影响,而duration会) - fillMode
kCAFillModeRemoved;//默认值,回到原始状态
kCAFillModeForwards;//保留动画结束后的状态
kCAFillModeBackwards;
kCAFillModeBoth;
实现一个手势控制开门的动画来了解各个属性:
- (void)createDoorAnimation{
//门
self.containView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 128, 256)];
self.containView.backgroundColor = [UIColor grayColor];
[self.view addSubview:self.containView];
//门板
self.doorLayer = [CALayer layer];
self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
self.doorLayer.position = CGPointMake(0, 128);
self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
UIImage *image = [UIImage imageNamed:@"person"];
self.doorLayer.contents = (__bridge id)image.CGImage;
[self.containView.layer addSublayer:self.doorLayer];
//设置3d效果
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0/500;
self.containView.layer.sublayerTransform = transform;
//添加手势
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
[pan addTarget:self action:@selector(pan:)];
[self.view addGestureRecognizer:pan];
//设置为0就没有动画,手动控制timeOffset来实现关门动画
self.doorLayer.speed = 0;
self.doorLayer.autoreverses = NO;
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 2.0;
[self.doorLayer addAnimation:animation forKey:nil];
}
- (void)pan:(UIPanGestureRecognizer *)pan{
CGFloat x = [pan translationInView:self.view].x;
x /= 200.0f;
CFTimeInterval timeOffset = self.doorLayer.timeOffset;
timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
self.doorLayer.timeOffset = timeOffset;
[pan setTranslation:CGPointZero inView:self.view];
}
缓冲函数
timingFunction是控制动画的缓冲函数,是CAMediaTimingFunction类的对象,调用+timingFunctionWithName:构造方法传入如下几个常量之一:
kCAMediaTimingFunctionLinear(线性):匀速,给你一个相对静态的感觉
kCAMediaTimingFunctionEaseIn(渐进):动画缓慢进入,然后加速离开
kCAMediaTimingFunctionEaseOut(渐出):动画全速进入,然后减速的到达目的地
kCAMediaTimingFunctionEaseInEaseOut(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。
同样的,针对UIView也支持缓冲方法,尽管语法和常量不一样。
UIViewAnimationOptionCurveEaseInOut
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionCurveLinear
//应用函数
[UIView animateWithDuration:1 delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
//动画内容
} completion:^(BOOL finished) {
//完成
}];
另外,CAKeyframeAnimation有个timingFunctions属性,是用来存放缓冲函数的数组,可以用它来对每次动画的步骤指定不同的计时函数。当然,我们也可以自定义缓冲函数,不过这个相对复杂挺多。
基于动画的定时器
动画能做到的仅仅是足够快地展示一系列静态图片,只是看起来像运动。iOS按照每秒60次刷新屏幕,然后CAAnimation计算出需要展示的新的帧,然后在屏幕更新的时候同步绘制上去。
在介绍基于动画的定时器前,我们先来熟悉下之前经常接触的定时器NSTimer,我们先来了解下NSTimer的工作原理。iOS每个线程上都管理着一个RunLoop,就是通过这个循环来完成一些任务列表,比如主线程的任务列表就有以下任务:
- 处理触摸事件
- 执行使用GCD的代码
- 处理计时器行为
- 屏幕重绘
当你设置一个NStimer,它会被插入当前的任务列表中,然后直到指定时间过去后才会被执行。当时它只会在列表中的上一个任务完成后才开始执行,这样有时候会导致延迟。(根据上一个任务的情况,少几毫秒,多则延迟很长时间)。
CADisplayLink是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。跟NSTimer以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后开始执行。
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。当然使用CADisplayLink也不能保证每一帧都按照计划执行,一些任务可能会导致动画偶尔丢帧。
CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。
//CADisplayLink
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
//NSTimer
self.timer = [NSTimer timerWithTimeInterval:1/60.0
target:self
selector:@selector(step:)
userInfo:nil
repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer
forMode:NSRunLoopCommonModes];