iOS动画详解----CAAnimation

789 阅读11分钟
简介

iOS动画主要是指Core Animation框架。Core Animation是iOS和macOS平台上负责图形渲染与动画的基础框架。Core Animation将大部分实际的绘图任务交给了图形硬件来处理,图形硬件会加速图形渲染的速度。这种自动化的图形加速技术让动画拥有更高的帧率并且显示效果更加平滑,不会加重CPU的负担而影响程序的运行速度。官方使用文档地址为:Core Animation Guide

layer内部维护着三分Layer Tree:

ModeLayer Tree(模型树):也就是我们通常所说的layer。 Presentation Tree(动画树):呈现出来的layer,也就是我们做动画时你看到的那个layer,可以通过layer.presentationLayer获得。 Render Tree(渲染树):私有,无法访问。主要是对Presentation Tree数据进行渲染,并且不会阻塞线程。

下面我们从上图的协议以及类的属性入手,分析一下上图结构:

CAMediaTiming协议中定义了时间,速度,重复次数等。属性定义如下:

其实不只是CAAnimation遵循CAMediaTiming协议,熟悉底层结构的小伙伴们应该知道CALayer也遵循这个协议,所有在一定程度上我们可以通过控制layer本身的协议属性来控制动画节奏。

@protocol CAMediaTiming
/* 
动画开始的时间。
动画都有一个timeline(时间线)的概念。动画开始执行都是基于这个时间线的绝对时间,这个时间和它的父类有关(系统的属性注释可以看到)。默认的CALayer的beginTime为零,如果这个值为零的话,系统会把它设置为CACurrentMediaTime(),那么这个时间就是正常执行动画的时间:立即执行。所以如果你设置beginTime=CACurrentMediaTime()+x,它会把它的执行时间线推迟x秒;如果你beginTime=CACurrentMediaTime()-x,那它开始的时候会从你动画对应的绝对时间开始执行.
*/
@property CFTimeInterval beginTime;

/* 
动画的持续时间。
此属性和speed有关系,动画执行时间则为duration/speed
*/
@property CFTimeInterval duration;

/*
动画执行速度,它duration的关系参考上面解释
*/
@property float speed;

/* 
动画时间偏移量
假设设置一个动画时间为5s,动画执行的过程为1->2->3->4->5,这时候如果你设置timeOffset = 2s那么它的执行过程就会变成3->4->5->1->2,如果你设置timeOffset = 4s那么它的执行过程就会变成5->1->2->3->4,
*/
@property CFTimeInterval timeOffset;

/* 
动画的重复次数
*/
@property float repeatCount;

/* 
动画的重复时间
此属性优先级大于repeatCount.也就是说如果repeatDuration设置为1秒重复10次,那么它会在1秒内执行完动画.
*/
@property CFTimeInterval repeatDuration;

/*
动画由初始值到最终值后,是否反过来回到初始值的动画,默认是 NO
*/
@property BOOL autoreverses;

/*
动画的填充方式,默认为:kCAFillModeRemoved
- kCAFillModeForwards//动画结束后,保持着最后的状态
- kCAFillModeBackwards//动画开始前,到达准备状态
- kCAFillModeBoth//动画开始前,进入准备状态,结束后,保持最后的状态
- kCAFillModeRemoved//动画完成后,移除,默认模式
*/
@property(copy) CAMediaTimingFillMode fillMode;

@end
CAAnimation核心动画基类,不能直接使用。除了CAMediaTiming协议中的方法,增加了timingFunctiondelegate等属性。具体如下:
/*
动画速度曲线函数
- kCAMediaTimingFunctionLinear 线性
- kCAMediaTimingFunctionEaseIn 慢进快出
- kCAMediaTimingFunctionEaseOut 快进慢出
- kCAMediaTimingFunctionEaseInEaseOut 慢进慢出 中间加速
- kCAMediaTimingFunctionDefault 默认
*/
@property(nullable, strong) CAMediaTimingFunction *timingFunction;

/* 
动画代理,能够检测动画的执行和结束
*/
@property(nullable, strong) id <CAAnimationDelegate> delegate;

/* 
动画完成后,render tree是否移除动画,默认是 YES
此属性为YES时, fillMode不可用
*/
@property(getter=isRemovedOnCompletion) BOOL removedOnCompletion;

如果timingFunction自带的速度曲线函数不满意,可以通过这两个自定义速度曲线函数,具体用法可以参考这篇文章:CAMediaTimingFunction的使用

+ (instancetype)functionWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;
- (instancetype)initWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y;

获取曲线函数的缓冲点,具体用法可以参考这篇文章:iOS 核心动画的缓冲

- (void)getControlPointAtIndex:(size_t)idx values:(float[2])ptr;
CAPropertyAnimation属性动画,抽象类,不能直接使用,针对CALayer的属动画属性进行设置
/* 
动画的属性值
*/
@property(nullable, copy) NSString *keyPath;

/*
属性动画是否以当前动画效果为基础,默认为NO
*/
@property(getter=isAdditive) BOOL additive;

/* 
指定动画是否为累加效果,默认为NO
*/
@property(getter=isCumulative) BOOL cumulative;

/*
此属性配合CALayer的transform属性使用
*/
@property(nullable, strong) CAValueFunction *valueFunction;

看一下可以设置属性动画的属性归总:

CATransform3D{
    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
}

CGPoint{
    position
    position.x
    position.y
}

CGRect{
    bounds
    bounds.size
    bounds.size.width
    bounds.size.height

    bounds.origin
    bounds.origin.x
    bounds.origin.y
}

property{
    opacity
    backgroundColor
    cornerRadius
    borderWidth
    contents

    Shadow{
        shadowColor
        shadowOffset
        shadowOpacity
        shadowRadius
    }
}

常用KeyPath总结 KeyPath

总结: CAAnimation是基类, CAPropertyAnimation是抽象类,两者都不可以直接使用, 那我们只有使用它的子类.
CABasicAnimation基础动画,通过keyPath对应属性进行控制,需要设置fromValue以及toValue
/*
- fromValue和toValue不为空,动画的效果会从fromValue的值变化到toValue.
- fromValue和byValue都不为空,动画的效果将会从fromValue变化到fromValue+byValue
- toValue和byValue都不为空,动画的效果将会从toValue-byValue变化到toValue
- 只有fromValue的值不为空,动画的效果将会从fromValue的值变化到当前的状态.
- 只有toValue的值不为空,动画的效果将会从当前状态的值变化到toValue的值.
- 只有byValue的值不为空,动画的效果将会从当前的值变化到(当前状态的值+byValue)的值
*/
@property(nullable, strong) id fromValue;
@property(nullable, strong) id toValue;
@property(nullable, strong) id byValue;

CABasicAnimation 看下实现代码:

-(void)animationBegin:(UIButton *)btn{
    CABasicAnimation *animation = nil;
    switch (btn.tag) {
        case 0:{
            //淡如淡出
            animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
            [animation setFromValue:@1.0];
            [animation setToValue:@0.1];
        }break;
        case 1:{
            //缩放
            animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
            [animation setFromValue:@1.0];//设置起始值
            [animation setToValue:@0.1];//设置目标值
        }break;
        case 2:{
            //旋转
            animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];
            //setFromValue不设置,默认以当前状态为准
            [animation setToValue:@(M_PI)];
        }break;
        case 3:{
            //平移
            animation = [CABasicAnimation animationWithKeyPath:@"position"];
            //setFromValue不设置,默认以当前状态为准
            [animation setToValue:[NSValue valueWithCGPoint:CGPointMake(self.view.center.x, self.view.center.y + 200)]];
        }break;
        default:break;
    }
    [animation setDelegate:self];//代理回调
    [animation setDuration:0.25];//设置动画时间,单次动画时间
    [animation setRemovedOnCompletion:NO];//默认为YES,设置为NO时setFillMode有效
    /**
     *设置动画速度曲线函数CAMediaTimingFunction
     *kCAMediaTimingFunctionLinear 匀速
     *kCAMediaTimingFunctionEaseIn 开始速度慢,后来速度快
     *kCAMediaTimingFunctionEaseOut 开始速度快 后来速度慢
     *kCAMediaTimingFunctionEaseInEaseOut = kCAMediaTimingFunctionDefault 中间速度快,两头速度慢
     */
    [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
    //设置自动翻转
    //设置自动翻转以后单次动画时间不变,总动画时间增加一倍,它会让你前半部分的动画以相反的方式动画过来
    //比如说你设置执行一次动画,从a到b时间为1秒,设置自动翻转以后动画的执行方式为,先从a到b执行一秒,然后从b到a再执行一下动画结束
    [animation setAutoreverses:YES];
    //kCAFillModeForwards//动画结束后回到准备状态
    //kCAFillModeBackwards//动画结束后保持最后状态
    //kCAFillModeBoth//动画结束后回到准备状态,并保持最后状态
    //kCAFillModeRemoved//执行完成移除动画
    [animation setFillMode:kCAFillModeBoth];
    //将动画添加到layer,添加到图层开始执行动画,
    //注意:key值的设置与否会影响动画的效果
    //如果不设置key值每次执行都会创建一个动画,然后创建的动画会叠加在图层上
    //如果设置key值,系统执行这个动画时会先检查这个动画有没有被创建,如果没有的话就创建一个,如果有的话就重新从头开始执行这个动画
    //你可以通过key值获取或者删除一个动画:
    //[self.demoView.layer animationForKey:@""];
    //[self.demoView.layer removeAnimationForKey:@""]
    [self.demoView.layer addAnimation:animation forKey:@"baseanimation"];
}

/**
 *  动画开始和动画结束时 self.demoView.center 是一直不变的,说明动画并没有改变视图本身的位置
 */
- (void)animationDidStart:(CAAnimation *)anim{
    NSLog(@"动画开始------:%@",    NSStringFromCGPoint(self.demoView.center));
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"动画结束------:%@",    NSStringFromCGPoint(self.demoView.center));
}
CASpringAnimation弹性动画,可以把看成在不绝对光滑的地面上,一个弹簧拴着别小球
/* 
质量,振幅和质量成反比,必须大于0,默认为1
*/
@property CGFloat mass;

/* 
刚度系数(劲度系数/弹性系数),刚度系数越大,形变产生的力就越大,运动越快,必须大于0,默认为100
*/
@property CGFloat stiffness;

/* 
阻尼系数,阻止弹簧伸缩的系数,阻尼系数越大,停止越快,可以认为它是阻力系数,必须大于或等于0。默认为10
*/
@property CGFloat damping;

/* 
初始速率,动画视图的初始速度大小速率为正数时,速度方向与运动方向一致,速率为负数时,速度方向与运动方向相反
*/
@property CGFloat initialVelocity;

/* 
返回弹簧动画到停止时的估算时间,根据当前的动画参数估算通常弹簧动画的时间使用结算时间比较准确
*/
@property(readonly) CFTimeInterval settlingDuration;

spring弹性动画 看下实现代码:

CASpringAnimation *spring = [CASpringAnimation animationWithKeyPath:@"position.y"];
spring.damping = 5;
spring.stiffness = 100;
spring.mass = 1;
spring.initialVelocity = 0;
spring.duration = spring.settlingDuration;
spring.fromValue = @(self.demoView1.center.y);
spring.toValue = @(self.demoView1.center.y + (btn.selected?+200:-200));
spring.fillMode = kCAFillModeForwards;
[self.demoView1.layer addAnimation:spring forKey:nil];
CAKeyframeAnimation关键帧动画,同样通过keyPath对应属性进行控制,但它可以通过values或者path进行多个阶段的控制
/*
关键帧值数组,一组变化值
*/
@property(nullable, copy) NSArray *values;

/*
关键帧路径,优先级比values高,但是只对CALayer的anchorPoint和position起作用。 */
@property(nullable) CGPathRef path;

/*
每一帧对应的时间,时间可以控制速度.它和每一个帧相对应,取值为0.0-1.0,不设则每一帧时间相等
*/
@property(nullable, copy) NSArray<NSNumber *> *keyTimes;

/* 
每一帧对应的动画速度曲线函数,也就是每一帧的运动节奏
如果“values”数组定义了n个关键帧,“timingFunctions”数组中应该有n-1个对象
*/
@property(nullable, copy) NSArray<CAMediaTimingFunction *> *timingFunctions;

/*
动画的计算模式,当设置为“paced”或“cubicPaced”时,动画的“keyTimes”和“timingFunctions”属性无效
- kCAAnimationLinear//关键帧为座标点的时候,关键帧之间直接直线相连进行插值计算
- kCAAnimationDiscrete//离散的,也就是没有补间动画
- kCAAnimationPaced//平均,keyTimes跟timeFunctions失效
- kCAAnimationCubic对关键帧为座标点的关键帧进行圆滑曲线相连后插值计算,对于曲线的形状还可以通过tensionValues,continuityValues,biasValues来进行调整自定义
- kCAAnimationCubicPaced在kCAAnimationCubic的基础上使得动画运行变得均匀,就是系统时间内运动的距离相同,
*/
@property(copy) CAAnimationCalculationMode calculationMode;

/* 
动画的张力,当动画为立方计算模式的时候此属性提供了控制插值,因为每个关键帧都可能有张力所以连续性会有所偏差它的范围为[-1,1]
*/
@property(nullable, copy) NSArray<NSNumber *> *tensionValues;

/* 
动画的连续性值
*/
@property(nullable, copy) NSArray<NSNumber *> *continuityValues;

/* 
动画的偏斜率
*/
@property(nullable, copy) NSArray<NSNumber *> *biasValues;

/* 
动画沿路径旋转方式,默认为nil
- kCAAnimationRotateAuto//自动旋转,
- kCAAnimationRotateAutoReverse//自动翻转
*/
@property(nullable, copy) CAAnimationRotationMode rotationMode;

CAKeyframeAnimation 看下实现代码:

-(void)animationBegin:(UIButton *)bt {
    switch (bt.tag) {
        case 0:
        case 1:
        case 2:
        case 3:
        case 4:
        case 5:
            [self path:bt.tag];
            break;
        case 6:
        case 7:
            [self values:bt.tag];
            break;
        default:
            break;
    }
}

- (void)path:(NSInteger)tag {
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    switch (tag) {
        case 0:{
            //椭圆
            CGMutablePathRef path = CGPathCreateMutable();//创建可变路径
            CGPathAddEllipseInRect(path, NULL, CGRectMake(0, 0, 320, 500));
            [animation setPath:path];
            CGPathRelease(path);
            animation.rotationMode = kCAAnimationRotateAuto;
            
        }break;
        case 1:{
            //贝塞尔,矩形
            UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 320, 320)];
            //animation需要的类型是CGPathRef,UIBezierPath是ui的,需要转化成CGPathRef
            [animation setPath:path.CGPath];
        }break;
        case 2:{
            //贝塞尔,抛物线
            UIBezierPath *path = [UIBezierPath bezierPath];
            [path moveToPoint:self.demoView.center];
            [path addQuadCurveToPoint:CGPointMake(0, 568)
                         controlPoint:CGPointMake(400, 100)];
            [animation setPath:path.CGPath];
        }break;
        case 3:{
            //贝塞尔,s形曲线
            UIBezierPath *path = [UIBezierPath bezierPath];
            [path moveToPoint:CGPointZero];
            [path addCurveToPoint:self.demoView.center
                    controlPoint1:CGPointMake(320, 100)
                    controlPoint2:CGPointMake(  0, 400)];
            ;
            [animation setPath:path.CGPath];
        }break;
        case 4:{
            //贝塞尔,圆形
            UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:self.view.center
                                                                 radius:150
                                                             startAngle:- M_PI * 0.5
                                                               endAngle:M_PI * 2
                                                              clockwise:YES];
            [animation setPath:path.CGPath];
        }break;
        case 5:{
            CGPoint point = CGPointMake(self.view.center.x, 400);
            CGFloat xlength = point.x - self.demoView.center.x;
            CGFloat ylength = point.y - self.demoView.center.y;
            CGMutablePathRef path = CGPathCreateMutable();
            //移动到目标点
            CGPathMoveToPoint(path, NULL, self.demoView.center.x, self.demoView.center.y);
            //将目标点的坐标添加到路径中
            CGPathAddLineToPoint(path, NULL, point.x, point.y);
            //设置弹力因子,
            CGFloat offsetDivider = 5.0f;
            BOOL stopBounciong = NO;
            while (stopBounciong == NO) {
                CGPathAddLineToPoint(path, NULL, point.x + xlength / offsetDivider, point.y + ylength / offsetDivider);
                CGPathAddLineToPoint(path, NULL, point.x, point.y);
                offsetDivider += 6.0;
                //当视图的当前位置距离目标点足够小我们就退出循环
                if ((ABS(xlength / offsetDivider) < 10.0f) && (ABS(ylength / offsetDivider) < 10.0f)) {
                    break;
                }
            }
            [animation setPath:path];
            
        }break;
        default:break;
    }
    
    [animation setDuration:0.5];
    [animation setRemovedOnCompletion:NO];
    [animation setFillMode:kCAFillModeBoth];
    [self.demoView.layer addAnimation:animation forKey:nil];
}

-(void)values:(NSInteger)tag{
    CAKeyframeAnimation *animation = nil;
    switch (tag) {
        case 6:{
            animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation"];
            
            CGFloat angle = M_PI_4 * 0.5;
            NSArray *values = @[@(angle),@(-angle),@(angle)];
            [animation setValues:values];
            [animation setRepeatCount:3];
            [animation setDuration:0.5];
        }break;
        case 7:{
            animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
            NSValue *p1 = [NSValue valueWithCGPoint:self.demoView.center];
            NSValue *p2 = [NSValue valueWithCGPoint:CGPointMake(self.view.center.x + 100, 200)];
            NSValue *p3 = [NSValue valueWithCGPoint:CGPointMake(self.view.center.x, 300)];
            //设置关键帧的值
            [animation setValues:@[p1,p2,p3]];
            [animation setDuration:0.5];
        }break;
        default:break;
    }
    UIGraphicsBeginImageContext(self.view.frame.size);
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [animation setRemovedOnCompletion:NO];
    [animation setFillMode:kCAFillModeBoth];
    [self.demoView.layer addAnimation:animation forKey:nil];
}

- (void)animationDidStart:(CAAnimation *)anim{
    NSLog(@"animation-start ======start:%@",NSStringFromCGRect(self.demoView.frame));
}

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"animation-start ======end:%@",NSStringFromCGRect(self.demoView.frame));
}
CATransition转场动画,系统提供了很多酷炫效果

系统默认提供了12种动画样式,加上4个动画方向,除了方向不可控的四种效果外,大概一共提供了36种动画.

/* 
转场类型
- kCATransitionFade//逐渐消失
- kCATransitionMoveIn//移进来
- kCATransitionPush//推进来
- kCATransitionReveal//揭开
另外,除了系统给的这几种动画效果,我们还可以使用系统私有的动画效果
- @"cube",//立方体翻转效果
- @"oglFlip",//翻转效果
- @"suckEffect",//收缩效果,动画方向不可控
- @"rippleEffect",//水滴波纹效果,动画方向不可控
- @"pageCurl",//向上翻页效果
- @"pageUnCurl",//向下翻页效果
- @"cameralIrisHollowOpen",//摄像头打开效果,动画方向不可控
- @"cameraIrisHollowClose",//摄像头关闭效果,动画方向不可控
- ["spewEffect","genieEffect","unGenieEffect","twist","tubey","swirl","charminUltra", "zoomyIn", "zoomyOut", "oglApplicationSuspend"]
*/
@property(copy) CATransitionType type;

/* 
转场方向
- kCATransitionFromRight//从右开始
- kCATransitionFromLeft//从左开始
- kCATransitionFromTop//从上开始
- kCATransitionFromBottom//从下开始
*/
@property(nullable, copy) CATransitionSubtype subtype;

/* 
动画进度(整体的百分比),默认0.0.如果设置0.3,那么动画将从动画的0.3的部分开始
*/
@property float startProgress;

/* 
结束进度(整体的百分比),默认1.0.如果设置0.6,那么动画将从动画的0.6部分以后就会结束
*/
@property float endProgress;

CATransition

CAAnimationGroup 动画组
/* 
只有一个属性,数组中接受CAAnimation元素
*/
@property(nullable, copy) NSArray<CAAnimation *> *animations;

参考: iOS动画-从不会到熟练应用

iOS动画(Core Animation)总结

很好动画示例 1