引言
之前说到CALayer以及子类动画,并且CALayer的有些属性自带隐式动画,不明白的可自行查看之前的文章,今天说一说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}
CoreAnimaitonPlayground[3669:389016] modelLayer_NSPoint: {187.5, 333.5},presentLayer_NSPoint: {61, 567}
下面我们做一个修改:
在实现动画时,我们注释掉这两行:
//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 {
[super viewDidLoad];
// 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) {
case 0:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"position"];
//到达位置
basicAni.byValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)];
break;
case 1:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
//到达缩放
basicAni.toValue = @(0.1f);
break;
case 2:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"opacity"];
//透明度
basicAni.toValue=@(0.1f);
break;
case 3:
//初始化动画并设置keyPath
basicAni = [CABasicAnimation animationWithKeyPath:@"transform"];
//3D
basicAni.toValue=[NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI_2+M_PI_4, 1, 1, 0)];
break;
case 4:
//初始化动画并设置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() fromLayer: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 {
[super didReceiveMemoryWarning];
// 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;
}else if (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"];
}
运行效果:
附
- 动画节奏控制属性
timingFunction
:
CA_EXTERN NSString * const kCAMediaTimingFunctionLinear //线性匀速
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseIn //慢进快出
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseOut //快进慢出
CA_EXTERN NSString * const kCAMediaTimingFunctionEaseInEaseOut //慢进慢出
CA_EXTERN NSString * const kCAMediaTimingFunctionDefault //默认
当然你也可以自己创建对应的timingFunction
,创建方式自寻。
- 当前对象在非活动时间段的行为
fillMode
:
CA_EXTERN NSString * const kCAFillModeForwards //动画结束后,保持着动画最后的状态
CA_EXTERN NSString * const kCAFillModeBackwards //动画开始前,到达准备状态
CA_EXTERN NSString * const kCAFillModeBoth //动画开始前,进入准备状态,结束后,保持最后的状态
CA_EXTERN NSString * const kCAFillModeRemoved //动画完成后,移除,默认模式
- 与
transform
和用的属性valueFunction
:
我们做一个旋转动画,我们会使用transform.rotation
作为keyPath
,但是它并不真实存在,这个属性就是因此而存在的,系统提供给了如下方式:
CA_EXTERN NSString * const kCAValueFunctionRotateX
CA_EXTERN NSString * const kCAValueFunctionRotateY
CA_EXTERN NSString * const kCAValueFunctionRotateZ
CA_EXTERN NSString * const kCAValueFunctionScale
CA_EXTERN NSString * const kCAValueFunctionScaleX
CA_EXTERN NSString * const kCAValueFunctionScaleY
CA_EXTERN NSString * const kCAValueFunctionScaleZ
CA_EXTERN NSString * const kCAValueFunctionTranslate
CA_EXTERN NSString * const kCAValueFunctionTranslateX
CA_EXTERN NSString * const kCAValueFunctionTranslateY
CA_EXTERN NSString * const kCAValueFunctionTranslateZ
举个例子:
-(void)transformAnimation{
//绕z轴旋转的动画
CABasicAnimation * transformAni = [CABasicAnimation animationWithKeyPath:@"transform"];
//从0度开始
transformAni.fromValue = @0;
//旋转到180度
transformAni.toValue = [NSNumber numberWithFloat:M_PI];
//duration
transformAni.duration = 1;
//设置valueFunction
transformAni.valueFunction = [CAValueFunction functionWithName:kCAValueFunctionRotateZ];
//添加动画
[_aniLayer addAnimation:transformAni forKey:@"transformAnimation"];
}
其他取值大家可以自行试试。
- 属性动画可以做动画的属性:
opacity 透明度
backgroundColor 背景颜色
cornerRadius 圆角
borderWidth 边框宽度
contents 内容
shadowColor 阴影颜色
shadowOffset 阴影偏移量
shadowOpacity 阴影透明度
shadowRadius 阴影圆角
...
rotation 旋转
transform.rotation.x
transform.rotation.y
transform.rotation.z
...
scale 缩放
transform.scale.x
transform.scale.y
transform.scale.z
...
translation 平移
transform.translation.x
transform.translation.y
transform.translation.z
...
position 位置
position.x
position.y
...
bounds
bounds.size
bounds.size.width
bounds.size.height
bounds.origin
bounds.origin.x
bounds.origin.y
...
...
以及CALayer子类对应的各个属性(比如CAShapeLayer的path)
- 关键帧动画的计算模式
calculationMode
:
当存在多个关键帧时,我们把每一个关键帧看为一个点,那么这些点可以是离散的,也可以直线相连后进行插值计算,也可以使用圆滑的曲线将他们相连后进行插值计算,那么就应用到了这个属性,具体取值如下:
CA_EXTERN NSString * const kCAAnimationLinear 默认值,关键帧之间直接直线相连进行插值计算
CA_EXTERN NSString * const kCAAnimationDiscrete 离散的,就是不进行插值计算,所有关键帧直接逐个进行显示
CA_EXTERN NSString * const kCAAnimationPaced 动画均匀进行,此时keyTimes和timingFunctions的设置失效
CA_EXTERN NSString * const kCAAnimationCubic 关键帧进行圆滑曲线相连后插值计算,对于曲线的形状还可以通过tensionValues,continuityValues,biasValues来进行调整自定义(http://en.wikipedia.org/wiki/Kochanek-Bartels_spline),这里的主要目的是使得运行的轨迹变得圆滑
CA_EXTERN NSString * const kCAAnimationCubicPaced 在kCAAnimationCubic的基础上使得动画运行变得均匀,就是系统时间内运动的距离相同,此时keyTimes以及timingFunctions也是无效的
- 动画沿路径旋转方式
rotationMode
,默认为nil
,系统提供了两种方式:
CA_EXTERN NSString * const kCAAnimationRotateAuto 沿路径旋转
CA_EXTERN NSString * const kCAAnimationRotateAutoReverse 沿路径反向颠倒旋转
听起来比较不好理解,我们那之前的关键帧动画为例,看一波效果:
设置keyFrameAni.rotationMode = kCAAnimationRotateAuto;
,运行效果如下:
设置
keyFrameAni.rotationMode = kCAAnimationRotateAutoReverse;
,运行效果如下:
-
常用的转场动画类型:
方向子类型包括:
结语
学会了吗?是时候去掀起一场腥风血雨了!
----------------------------我是分割线-------------------------------
文章发布后很多人私信这样一个问题,感觉很多人有这个疑问,我大致说一下。
Q:他说,他之所以喜欢用UIView
动画是因为它的Block
的独立性,他给我举了这样一个例子:他在一个页面中要做多个动画,所以他把代理设置为self
,这几个动画如果不是完全并行,那他就要在代理方法里去判断这个动画到底是哪个动画,然后接下一个动画,那么他就需要给这个动画打上标志(⚠️:当动画结束后,layer.animationKeys
中已经没有添加动画的那个对应的key
,所以他没有办法去知道代理方法中返回的CAAnimation
对象是不是就是他设置的key
所对应的动画对象,他是通过- (void)setValue:(nullable id)value forKey:(NSString *)key;
的方式去对应动画对象),然后处理起来好像更麻烦了,所以哪怕牺牲一些效果,他宁愿用UIView
动画来实现。
A:其实这涉及到一个思路的问题,我一般是这么做的,直接上代码,相信你能看得懂:
首先,创建一个类:
#import
@interface YSCoreAnimationDelegate : NSObject
+(instancetype)coreAnimationDelegateDidStart:(void(^)())didStart didStop:(void(^)(BOOL finished))didStop;
@end
#import "YSCoreAnimationDelegate.h"
@interface YSCoreAnimationDelegate ()
@property(nonatomic,copy)void(^didStart)();
@property(nonatomic,copy)void(^didStop)(BOOL finished);
@end
@implementation YSCoreAnimationDelegate
+(instancetype)coreAnimationDelegateDidStart:(void(^)())didStart didStop:(void(^)(BOOL finished))didStop{
YSCoreAnimationDelegate *animationDelegate = [[YSCoreAnimationDelegate alloc] init];
animationDelegate.didStart = didStart;
animationDelegate.didStop = didStop;
return animationDelegate;
}
#pragma mark - CAAnimationDelegate
-(void)animationDidStart:(CAAnimation *)anim{
if (self.didStart) self.didStart(),self.didStart = nil;
}
//
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
if (self.didStop) self.didStop(flag),self.didStop = nil;
}
@end
然后这样调用调用:
basicAni.delegate = [YSCoreAnimationDelegate coreAnimationDelegateDidStart:^{
NSLog(@"基础动画开始");
} didStop:^(BOOL finished) {
NSLog(@"基础动画结束");
}];
你可以试一下,打印结果正确无误。是不是比UIView
动画更吸引你了呢?
其实这只是一种非常细节的有
iOS
特色的设计思路而已,突然想写一篇架构思路的文章(不如题目就叫来,年轻人,踩着我爬坑之一次架构讨论引发的血案
,结合自身故事,我蹲下,你踩着我爬坑)。其实有很多人对于MVC
,MVP
,MVVM
说的头头是道,但写起代码来,就刹不住车,UIViewController
里不写够2000行代码算我菜。拿MVC
来说,它已经30多岁了,而且很多平台开发都在用这种架构,作为iOS开发者,我们用该结合自身项目进行架构分析,不可盲目硬怼啊。其实个中思路不过是为了好维护,易扩展,低耦合,高复用... ...而已。