iOS动画篇_CoreAnimation(超详细解析核心动画)

引言

之前说到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,并且设置fillModekCAFillModeForwards,则保持动画执行后的状态。

  • CAPropertyAnimation 属性动画,针对对象的可动画属性进行效果的设置,不可直接使用。添加属性具体如下:
    keyPath -> CALayer的某个属性名,并通过这个属性的值进行修改,达到相应的动画效果。
    additive -> 属性动画是否以当前动画效果为基础,默认为NO。
    cumulative -> 指定动画是否为累加效果,默认为NO。
    valueFunction -> 此属性配合CALayertransform属性使用。

  • 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高,但是只对CALayeranchorPointposition起作用。
    keyTimes -> 每一帧对应的时间,如果不设置,则各关键帧平分设定时间。
    timingFunctions -> 每一帧对应的动画节奏。
    calculationMode -> 动画的计算模式,系统提供了对应的几种模式。
    tensionValues -> 动画张力控制。
    continuityValues -> 动画连续性控制。
    biasValues -> 动画偏差率控制。
    rotationMode -> 动画沿路径旋转方式,系统提供了两种模式。

  • CATransition 转场动画,系统提供了很多酷炫效果。属性如下:
    type -> 转场动画类型。
    subtype -> 转场动画方向。
    startProgress -> 动画起点进度(整体的百分比)。
    endProgress -> 动画终点进度(整体的百分比)。
    filter -> 自定义转场。

  • CAAnimationGroup 动画组,方便对于多动画的统一控制管理。
    animations -> 所有动画效果元素的数组。


CABasicAnimation

在一般的应用开发中,基础动画可以满足大部分的开发需求,主要完成对于对象指定动画属性两个Value之间的动画过度。
具体过程如下:

  • 初始化动画并设置动画keyPath(keyPath为指定动画效果的CALayer的某个属性名,比如position属性)
  • 设置动画其他属性,比如delegatefromValuetoValueduration
  • 利用- (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<UITouch *> *)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 ()<CAAnimationDelegate>
@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才引入的动画类,效果类似于UIViewspring动画,不过比其增加了质量劲度系数等属性的扩展,继承于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

在我们实际开发中,我们可能需要更加复杂的复合运动,那么需要给图层加多个动画,动画组也就应运而生,创建动画组也很简单,首先创建单个动画,然后将创建的多个动画添加到动画组,最后将动画组添加图层上就可以啦。不要认为动画组诗简单的动画的集合,因为其他动画有的属性很多动画组也有,比如timingFunctiondurationrepeatCount等,动画组和动画组的每一个元素都可以单独设置这些属性来实现一个不仅仅是单纯组合这么单纯的效果。(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 <UIKit/UIKit.h>

@interface YSCoreAnimationDelegate : NSObject<CAAnimationDelegate>

+(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特色的设计思路而已,突然想写一篇架构思路的文章(不如题目就叫来,年轻人,踩着我爬坑之一次架构讨论引发的血案,结合自身故事,我蹲下,你踩着我爬坑)。其实有很多人对于MVCMVPMVVM说的头头是道,但写起代码来,就刹不住车,UIViewController里不写够2000行代码算我菜。拿MVC来说,它已经30多岁了,而且很多平台开发都在用这种架构,作为iOS开发者,我们用该结合自身项目进行架构分析,不可盲目硬怼啊。其实个中思路不过是为了好维护,易扩展,低耦合,高复用... ...而已。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容