CoreAnimation核心动画。
在开发过程中,对于动画效果,很多人好像都青睐于UIView动画,简单快捷,一个代码块能实现一个动画让很多其他系统下开发的小伙伴看红了眼。但是当有一些特殊的需求时,那你难免会有大量的Block嵌套产生,同时如何高效的控制动画效果,比如停止动画,控制动画节奏等会让你无从下手,难免出现出现上图这种情况。那么,为了能更加顺畅的吹牛逼,可能你需要了解核心动画CoreAnimation。
首先我们先补习一下关于CALyaer你可能不知道的事情:
我们来看一种layer的层次结构Layer Tree,这种层次结构分为以下三种:
Model Tree :也就是我们通常所说的layer。
Presentation Tree:呈现出来的layer,也就是我们做动画时你看到的那个layer,可以通过layer.presentationLayer获得。
Render Tree :私有,无法访问。主要是对Presentation Tree数据进行渲染,并且不会阻塞线程。
是不是你听不明白?你是不是还很懵逼?是不是想再详细点?
??:别砍我,等举个????的时候我们再详细分析!!!
??:我们在文章末尾对一些系统提供的属性字段以及枚举值进行解释。
切入主题
我们看一下核心动画的几个类:
下面我们从上图的协议以及类的属性入手,分析一下上图结构:
CAMediaTiming 协议中定义了时间,速度,重复次数等。属性定义如下:
beginTime -> 用来设置动画延时,若想延迟1秒,就设置为CACurrentMediaTime()+1,其中CACurrentMediaTime()为图层当前时间。
duration -> 动画的持续时间。
speed -> 动画速率,决定动画时间的倍率。当speed为2时,动画时间为设置的duration的1/2。
timeOffset -> 动画时间偏移量。比如设置动画时长为3秒,当设置timeOffset为1.5时,当前动画会从中间位置开始,并在到达指定位置时,走完之前跳过的前半段动画。
repeatCount -> 动画的重复次数。
repeatDuration -> 动画的重复时间。
autoreverses -> 动画由初始值到最终值后,是否反过来回到初始值的动画。如果设置为YES,就意味着动画完成后会以动画的形式回到初始值。
fillMode -> 决定当前对象在非动画时间段的行为.比如动画开始之前,动画结束之后。
??:其实不只是CAAnimation遵循CAMediaTiming协议,熟悉底层结构的小伙伴们应该知道CALayer也遵循这个协议,所有在一定程度上我们可以通过控制layer本身的协议属性来控制动画节奏。
CAAnimation 核心动画基础类,不能直接使用。除了CAMediaTiming协议中的方法,增加了CAAnimationDelegate的代理属性等。具体如下:
timingFunction -> 控制动画的节奏。系统提供的包括:kCAMediaTimingFunctionLinear (匀速),kCAMediaTimingFunctionEaseIn (慢进快出),kCAMediaTimingFunctionEaseOut (快进慢出),kCAMediaTimingFunctionEaseInEaseOut (慢进慢出,中间加速),kCAMediaTimingFunctionDefault (默认),当然也可通过自定义创建CAMediaTimingFunction。
delegate -> 代理。
removedOnCompletion -> 是否让图层保持显示动画执行后的状态,默认为YES,也就是动画执行完毕后从涂层上移除,恢复到执行前的状态,如果设置为NO,并且设置fillMode为kCAFillModeForwards,则保持动画执行后的状态。
CAPropertyAnimation 属性动画,针对对象的可动画属性进行效果的设置,不可直接使用。添加属性具体如下:
keyPath -> CALayer的某个属性名,并通过这个属性的值进行修改,达到相应的动画效果。
additive -> 属性动画是否以当前动画效果为基础,默认为NO。
cumulative -> 指定动画是否为累加效果,默认为NO。
valueFunction -> 此属性配合CALayer的transform属性使用。
CABasicAnimation基础动画,通过keyPath对应属性进行控制,需要设置fromValue以及toValue。添加属性如下:
fromValue -> keyPath相应属性的初始值。
toValue -> keyPath相应属性的结束值。
byValue -> 在不设置toValue时,toValue = fromValue + byValue,也就是在当前的位置上增加多少。
CASpringAnimation 带有初始速度以及阻尼指数等物理参数的属性动画。我们可以把它看成在不绝对光滑的地面上,一个弹簧拴着别小球,那么我们可以这么理解他的属性(物理知识请问一下牛顿大叔):
mass -> 小球质量,影响惯性。
stiffness -> 弹簧的劲度系数。
damping -> 阻尼系数,地面的摩擦力。
initialVelocity -> 初始速度,相当于给小球一个初始速度(可正可负,方向不同)
settlingDuration -> 结算时间,根据上述参数计算出的预计时间,相对于你设置的时间,这个时间比较准确。
CAKeyframeAnimation 关键帧动画,同样通过keyPath对应属性进行控制,但它可以通过values或者path进行多个阶段的控制。属性如下:
values -> 关键帧组成的数组,动画会依次显示其中的每一帧。
path -> 关键帧路径,动画进行的要素,优先级比values高,但是只对CALayer的anchorPoint和position起作用。
keyTimes -> 每一帧对应的时间,如果不设置,则各关键帧平分设定时间。
timingFunctions -> 每一帧对应的动画节奏。
calculationMode -> 动画的计算模式,系统提供了对应的几种模式。
tensionValues -> 动画张力控制。
continuityValues -> 动画连续性控制。
biasValues -> 动画偏差率控制。
rotationMode -> 动画沿路径旋转方式,系统提供了两种模式。
CATransition 转场动画,系统提供了很多酷炫效果。属性如下:
type -> 转场动画类型。
subtype -> 转场动画方向。
startProgress -> 动画起点进度(整体的百分比)。
endProgress -> 动画终点进度(整体的百分比)。
filter -> 自定义转场。
CAAnimationGroup 动画组,方便对于多动画的统一控制管理。
animations -> 所有动画效果元素的数组。
CABasicAnimation
在一般的应用开发中,基础动画可以满足大部分的开发需求,主要完成对于对象指定动画属性两个Value之间的动画过度。
具体过程如下:
初始化动画并设置动画keyPath(keyPath为指定动画效果的CALayer的某个属性名,比如position属性)
设置动画其他属性,比如delegate,fromValue,toValue,duration等
利用- (void)addAnimation:(CAAnimation *)anim forKey:(nullable NSString *)key;添加给指定layer添加动画
利用- (void)removeAllAnimations;或者- (void)removeAnimationForKey:(NSString *)key;方法停止所有或者指定动画
我们下面写一个简单的位移动画:
首先,创建一个做动画的layer:
self.aniLayer = [[CALayer alloc] init];
_aniLayer.bounds = CGRectMake(0, 0, 100, 100);
_aniLayer.position = self.view.center;
_aniLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:_aniLayer];
生成一个CADisplayLink,我们来看一下我们上面说过的layer的层级结构:
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
_displayLink.frameInterval = 30;
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
//
-(void)handleDisplayLink:(CADisplayLink *)displayLink{
NSLog(@"modelLayer_%@,presentLayer_%@",[NSValue valueWithCGPoint:_aniLayer.position],[NSValue valueWithCGPoint:_aniLayer.presentationLayer.position]);
}
在点击屏幕时,触发动画:
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event{
CGPoint positon = [touches.anyObject locationInView:self.view];
[self basicAnimation_PositionTo:positon];
}
动画:
-(void)basicAnimation_PositionTo:(CGPoint)position{
//初始化动画并设置keyPath
CABasicAnimation *basicAni = [CABasicAnimation animationWithKeyPath:@"position"];
//设置代理
basicAni.delegate = self;
//到达位置
basicAni.toValue = [NSValue valueWithCGPoint:position];
//延时执行
//basicAni.beginTime = CACurrentMediaTime() + 2;
//动画时间
basicAni.duration = 3;
//动画节奏
basicAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
//动画速率
//basicAni.speed = 0.1;
//图层是否显示执行后的动画执行后的位置以及状态
basicAni.removedOnCompletion = NO;
basicAni.fillMode = kCAFillModeForwards;
//动画完成后是否以动画形式回到初始值
//basicAni.autoreverses = YES;
//动画时间偏移
//basicAni.timeOffset = 0.5;
//添加动画
[_aniLayer addAnimation:basicAni forKey:NSStringFromSelector(_cmd)];
}
运行效果:
打印结果:
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {187.5, 333.5}
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {182.1122316699475, 343.44501110725105}
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {161.77888754755259, 380.97731033712626}
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {128.82425409555435, 441.80661398172379}
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {94.108665972948074, 505.88637545704842}
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {70.035845696926117, 550.32118600606918}
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {61.103064090013504, 566.80975916981697}
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {61, 567}
下面我们做一个修改:
1
2
3在实现动画时,我们注释掉这两行:
//basicAni.removedOnCompletion = NO;
//basicAni.fillMode = kCAFillModeForwards;
运行结果:
打印结果:
CoreAnimaitonPlayground[3710:397153] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {187.5, 333.5}
CoreAnimaitonPlayground[3710:397153] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {187.33332352247089, 333.85210405878024}
CoreAnimaitonPlayground[3710:397153] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {178.31957995891571, 352.89363733679056}
CoreAnimaitonPlayground[3710:397153] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {155.03444194793701, 402.08349138498306}
CoreAnimaitonPlayground[3710:397153] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {121.92566871643066, 472.02577483654022}
CoreAnimaitonPlayground[3710:397153] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {90.934395790100098, 537.49483889341354}
CoreAnimaitonPlayground[3710:397153] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {72.198107242584229, 577.07524845004082}
CoreAnimaitonPlayground[3710:397153] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {187.5, 333.5}
去掉这两行代码后,layer完成动画后跳回到开始的位置,我们看到的这种现象结合layer层级的打印,我们可以确定:动画本身并没有改变model tree的位置,我们看到的动画是presentation tree运动的轨迹。当设置removedOnCompletion 属性为NO以及fillMode属性为kCAFillModeForwards时,也并未改变model tree的位置,但是可以使动画结束后,防止presentation tree被移除并回到动画开始的位置。所以并不建议使用removedOnCompletion配合fillMode的方式来实现动画结束时,图层不跳转回原位的实现,我们应该在动画开始或者结束时重新设置它的位置。我们这么做:
//储存结束位置
[basicAni setValue:[NSValue valueWithCGPoint: position] forKey:@"positionToEnd"];
//动画结束后,重新设置它的位置
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
_aniLayer.position = [[anim valueForKey:@"positionToEnd"] CGPointValue];
}
我们来看一下效果:
我们发现了另一个问题,当动画完成后,它会重新从起点运动到终点,一看就是上一节课没认真听,这就是因为我们之前提到的,对于非根图层,设置它的可动画属性是有隐式动画的,那么我们需要关闭图层的隐式动画,我们就需要用到动画事务CATransaction:
说到这,我们就简单介绍一下CATransaction,有人说,我好像没见过这个东西,他是个什么鬼?和NSAutoreleasePool一样,当我们不手动创建时,系统会在一帧开始时生成一个事务,并在这一帧结束时commit,这也就是隐式CATransaction。当然你也可以利用[CATransaction begin]方法开始,调用[CATransaction commit]方法结束,中间便是事务的作用域,然后把需要更改可动画属性的操作放在该作用域内,这就是显式CATransaction,它常常用于关闭隐式动画和调整动画时间。下面我们就用它来关闭修改图层的position时所带来的隐式动画:
动画结束时我们这样写:
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
//开始事务
[CATransaction begin];
//关闭隐式动画
[CATransaction setDisableActions:YES];
_aniLayer.position = [[anim valueForKey:@"positionToEnd"] CGPointValue];
//提交事务
[CATransaction commit];
}
运行效果:
我们可以看到,这样就解决了隐式动画导致的问题啦。
下面我们利用CABasicAnimation实现几种动画效果:
#import "ViewController.h"
#define buttonName @[@"位移",@"缩放",@"透明度",@"旋转",@"圆角"]
@interface ViewController ()
@property(nonatomic,strong)CALayer *aniLayer;
//
@property(nonatomic,strong)CADisplayLink *displayLink;
@end
@implementation ViewController
- (void)viewDidLoad {
[superviewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.aniLayer = [[CALayer alloc] init];
_aniLayer.bounds = CGRectMake(0, 0, 100, 100);
_aniLayer.position = self.view.center;
_aniLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:_aniLayer];
//
for(int i = 0; i < 5; i++) {
UIButton *aniButton = [UIButton buttonWithType:UIButtonTypeCustom];
aniButton.tag = i;
[aniButton setTitle:buttonName[i] forState:UIControlStateNormal];
aniButton.exclusiveTouch = YES;
aniButton.frame = CGRectMake(10, 50 + 60 * i, 100, 50);
aniButton.backgroundColor = [UIColor blueColor];
[aniButton addTarget:self action:@selector(tapAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:aniButton];
}
//
// _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
// _displayLink.frameInterval = 30;
// [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
-(void)tapAction:(UIButton*)button{
[self basicAnimationWithTag:button.tag];
}
-(void)handleDisplayLink:(CADisplayLink *)displayLink{
NSLog(@"modelLayer_%@,presentLayer_%@",[NSValue valueWithCGPoint:_aniLayer.position],[NSValue valueWithCGPoint:_aniLayer.presentationLayer.position]);
}
-(void)basicAnimationWithTag:(NSInteger)tag{
CABasicAnimation *basicAni = nil;
switch(tag) {
case0:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"position"];
//到达位置
basicAni.byValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)];
break;
case1:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
//到达缩放
basicAni.toValue = @(0.1f);
break;
case2:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"opacity"];
//透明度
basicAni.toValue=@(0.1f);
break;
case3:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"transform"];
//3D
basicAni.toValue=[NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI_2+M_PI_4, 1, 1, 0)];
break;
case4:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"cornerRadius"];
//圆角
basicAni.toValue=@(50);
break;
default:
break;
}
//设置代理
basicAni.delegate = self;
//延时执行
//basicAni.beginTime = CACurrentMediaTime() + 2;
//动画时间
basicAni.duration = 1;
//动画节奏
basicAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
//动画速率
//basicAni.speed = 0.1;
//图层是否显示执行后的动画执行后的位置以及状态
//basicAni.removedOnCompletion = NO;
//basicAni.fillMode = kCAFillModeForwards;
//动画完成后是否以动画形式回到初始值
basicAni.autoreverses = YES;
//动画时间偏移
//basicAni.timeOffset = 0.5;
//添加动画
[_aniLayer addAnimation:basicAni forKey:NSStringFromSelector(_cmd)];
}
//暂停动画
-(void)animationPause{
//获取当前layer的动画媒体时间
CFTimeInterval interval = [_aniLayer convertTime:CACurrentMediaTime() toLayer:nil];
//设置时间偏移量,保证停留在当前位置
_aniLayer.timeOffset = interval;
//暂定动画
_aniLayer.speed = 0;
}
//恢复动画
-(void)animationResume{
//获取暂停的时间
CFTimeInterval beginTime = CACurrentMediaTime() - _aniLayer.timeOffset;
//设置偏移量
_aniLayer.timeOffset = 0;
//设置开始时间
_aniLayer.beginTime = beginTime;
//开始动画
_aniLayer.speed = 1;
}
//停止动画
-(void)animationStop{
//[_aniLayer removeAllAnimations];
//[_aniLayer removeAnimationForKey:@"groupAnimation"];
}
#pragma mark - CAAnimationDelegate
-(void)animationDidStart:(CAAnimation *)anim{
}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
}
- (void)didReceiveMemoryWarning {
[superdidReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
CASpringAnimation
CASpringAnimation是iOS9才引入的动画类,效果类似于UIView的spring动画,不过比其增加了质量,劲度系数等属性的扩展,继承于CABaseAnimation,用法也很简单:
-(void)springAnimation{
CASpringAnimation *springAni = [CASpringAnimation animationWithKeyPath:@"position"];
springAni.damping = 2;
springAni.stiffness = 50;
springAni.mass = 1;
springAni.initialVelocity = 10;
springAni.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 400)];
springAni.duration = springAni.settlingDuration;
[_aniLayer addAnimation:springAni forKey:@"springAnimation"];
}
CAKeyframeAnimation
关键帧动画和CABasicAnimation一样是CApropertyAnimation的子类,但是CABasicAnimation只能从一个数值(fromValue)变到另一个数值(toValue)或者添加一个增量数值(byValue),而CAKeyframeAnimation使用values数组可以设置多个关键帧,同时可以利用path可以进行位置或者锚点的动画操作。操作起来也很简单:
-(void)keyframeAnimationWithTag:(NSInteger)tag{
CAKeyframeAnimation *keyFrameAni = nil;
if(tag == 6) {
keyFrameAni = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
keyFrameAni.duration = 0.3;
keyFrameAni.values = @[@(-(4) / 180.0*M_PI),@((4) / 180.0*M_PI),@(-(4) / 180.0*M_PI)];
keyFrameAni.repeatCount=MAXFLOAT;
}elseif(tag == 7){
//曲线位移
keyFrameAni = [CAKeyframeAnimation animationWithKeyPath:@"position"];
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:_aniLayer.position];
[path addCurveToPoint:CGPointMake(300, 500) controlPoint1:CGPointMake(100, 400) controlPoint2:CGPointMake(300, 450)];
keyFrameAni.path = path.CGPath;
keyFrameAni.duration = 1;
}
[_aniLayer addAnimation:keyFrameAni forKey:@"keyFrameAnimation"];
}
运行效果:
CATransition
转场动画是一种显示样式向另一种显示样式过渡的效果,不太需要脑子就能制作出酷炫的效果,系统给出的效果也很多,不过谨慎使用私有API,防止被拒的悲剧。创建转场动画真的很简单:
创建转场动画
设置转场类型type,以及自类型subtype(也就是转场方向,不是所有的效果都有子类型)及其他属性。
设置新的显示效果后,添加动画到图层。
//转场动画
-(void)transitionAnimation{
CATransition *transtion = [CATransition animation];
transtion.type = @"rippleEffect";
transtion.subtype = kCATransitionFromLeft;//kCATransitionFromLeft kCATransitionFromRight
transtion.duration = 1;
_transtionIndex++;
if(_transtionIndex > 4) {
_transtionIndex = 1;
}
_aniLayer.contents = (id)[UIImage imageNamed:[NSString stringWithFormat:@"%@.jpg",@(_transtionIndex)]].CGImage;
[_aniLayer addAnimation:transtion forKey:@"transtion"];
}
运行效果:
CAAnimationGroup
在我们实际开发中,我们可能需要更加复杂的复合运动,那么需要给图层加多个动画,动画组也就应运而生,创建动画组也很简单,首先创建单个动画,然后将创建的多个动画添加到动画组,最后将动画组添加图层上就可以啦。不要认为动画组诗简单的动画的集合,因为其他动画有的属性很多动画组也有,比如timingFunction,duration,repeatCount等,动画组和动画组的每一个元素都可以单独设置这些属性来实现一个不仅仅是单纯组合这么单纯的效果。(eg:生成一个动画,晃动位移1s到指定位置,并原地晃动2s,然后回到原位重新开始动画),代码如下:
-(void)animationGroup{
//晃动动画
CAKeyframeAnimation *keyFrameAni = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
keyFrameAni.values = @[@(-(4) / 180.0*M_PI),@((4) / 180.0*M_PI),@(-(4) / 180.0*M_PI)];
//每一个动画可以单独设置时间和重复次数,在动画组的时间基础上,控制单动画的效果
keyFrameAni.duration = 0.3;
keyFrameAni.repeatCount=MAXFLOAT;
keyFrameAni.delegate = self;
//
//位移动画
CABasicAnimation *basicAni = [CABasicAnimation animationWithKeyPath:@"position"];
//到达位置
basicAni.byValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)];
//
basicAni.duration = 1;
basicAni.repeatCount = 1;
//
basicAni.removedOnCompletion = NO;
basicAni.fillMode = kCAFillModeForwards;
//设置代理
basicAni.delegate = self;
//动画时间
basicAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CAAnimationGroup *aniGroup = [CAAnimationGroup animation];
aniGroup.animations = @[keyFrameAni,basicAni];
aniGroup.autoreverses = YES;
//动画的表现时间和重复次数由动画组设置的决定
aniGroup.duration = 3;
aniGroup.repeatCount=MAXFLOAT;
//
[_aniLayer addAnimation:aniGroup forKey:@"groupAnimation"];
}