iOS动画-CAAnimation使用详解

理解了隐式动画后,显式动画就更加通俗易懂了。区别于隐式动画的特点,显式动画就是需要我们明确指定类型、时间等参数来实现效果的动画。除此之外,我们也可以创建非线性动画,比如沿着任意一条曲线运动等;
我们平时最常用的也是显式动画,不仅系统为我们的视图提供了UIViewAnimationWithBlock的动画封装,而且我们在熟悉了Core Animation的动画属性后也可以很方便的设置显式动画;

本篇主要内容:
1.iOS动画的分类
2.CAMediaTiming协议
3.CAAnimation基类
4.CAPropertyAnimation基类
5.基础动画CABasicAnimation
6.关键帧动画CAKeyframeAnimation
7.动画组CAGroupAnimation
8.过渡动画CATransition
9.委托模式下的动画区分
10.虚拟属性及其作用
11.动画的取消

相关文章:
iOS动画-CALayer寄宿图与绘制原理
iOS动画-CALayer布局属性详解
iOS动画-CALayer隐式动画原理与特性
iOS动画-CAAnimation使用详解

一、动画的分类

1、实现动画的方式

如果根据实现动画时直接操作对象的类型,我们可以简单的将动画分为视图和图层两种;但事实上,无论UIViewAnimaiton动画还是UIViewAnimaitonWithBlock动画都只是对UIView的关联图层CALayer动画的进一步封装。


实现动画的方式.png

2.核心动画Core Animation常用类的继承关系

我们在使用Core Animation动画之前,有必要对核心动画常见的类和动画属性做一个基本了解;从继承关系的图示中,我们可以十分清晰的看出这些属性设置设置因何而来,以及它们各自的联系。

核心动画类的继承关系.jpg
动画类 动画特性
CAMediaTiming 协议;定义了一段动画内用于控制时间的属性的集合
CAAnimation 抽象类;作为所有动画类型父类,不可直接使用
CAPropertyAnimation 抽象类;作为基础动画和帧动画的父类,不可直接使用
CABasicAnimation 基础动画;用于实现单一属性变化的动画
CAKeyFrameAnimation 关键帧动画;用于实现单一属性连续变化的动画
CAAnimaitionGroup 组动画;用于实现多属性同时变化的动画
CATrasition 转场过渡动画;

二、CAMediaTiming协议

CAMediaTiming协议定义了一段动画内用于控制时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制,有关CAMediaTimg协议具体的属性如下:

属性 参数类型 具体描述
beginTime CFTimeInterval 动画开始之前的延迟时间,这里的延迟从动画添加到可见图层上那一刻开始测量;
(设置动画beginTime为1,动画将延时1秒后开始执行)
duration CFTimeInterval 动画持续时间;
(默认值为0,但是实际动画默认持续时间为0.25秒)
speed float 动画执行的速度;
(默认值为0,减少它会减慢动画的时间,增加它会加快速度)
(设置speed为2时,则动画实际执行时间是duration的一半)
timeOffset CFTimeInterval 动画时间偏移量;
(设置时长3秒动画的timeOffset为1时,动画会从1秒位置执到最后,再执行之前跳过的部分)
repeatCount float 动画重复次数;默认值是0,但是实际默认动画执行1次;
(设置为INFINITY,则一直执行);
(duration是2,repeatCount设置为3.5,则完整动画时长7秒)
repeatDuration CFTimeInterval 动画重复的时间,让动画重复执行一个指定的时间;
(设置为INFINITY,一直执行)
repeatCount和repeatDuration可能会相互冲突,所以你只需要对其中一个指定非零值,对两个属性都设置非0值的行为没有被定义;
autoreverses BOOL 动画从初始值执行到最终值,是否会反向回到初始值;
(设置为YES,动画完成后将以动画的形式回到初始位置)
fillMode NSStrinng 决定当前对象在非动画时间端段的动画属性值,如动画开始之前和动画结束之后

1.fillMode详细说明

试想这样一个问题:在beginTime非0(即动画未真正执行之前),以及removeOnCompletion被设置为NO的动画结束时,我们会遇到这样一个问题:被设置动画的属性应该是什么值?
一种可能是属性与动画没被添加之前保持一致,还有一种可能是保持动画开始之前那一帧或者动画结束那一帧,这就是所谓的填充。
CAMediaTiming的fillMode用来控制填充效果,它是一个NSString类型,有四种常量可供使用:

fillMode类型 参数类型 具体描述
kCAFillModeRemoved (default) NSString 默认值,动画开始前和结束后,动画对图层都没有影响,图层依然保持初始值
kCAFillModeForwards NSString 动画结束后,图层一直保持动画后的最终状态
kCAFillModeBackwards NSString 动画开始前,只要加入动画就会处于动画的初始状态
kCAFillModeBoth NSString 综合了kCAFillModeForwards与kCAFillModeBackwards特性;
(动画加入图层到真正执行动画的时间段里,图层保持动画初始状态;动画结束之后保持动画最终状态)

特别注意:removedOnCompletion需要设置为NO,否则fillMode不起作用;

2.CAMediaTiming属性应用总结

时间属性的综合应用.png

三、CAAnimation基类

CAAnimation作为所有动画类型父类,是一个抽象类;我们不能直接使用CAAnimation类,而是使用它的子类;关于它的定义如下:

@interface CAAnimation : NSObject<NSSecureCoding, NSCopying, CAMediaTiming, CAAction>

@property(nullable, strong) CAMediaTimingFunction *timingFunction;
@property(nullable, strong) id <CAAnimationDelegate> delegate;
@property(getter=isRemovedOnCompletion) BOOL removedOnCompletion;

@end

可以看到,CAAnimation动画基类遵循了CAMediaTiming协议,而且另外包含了三个常用的动画属性;下面是对这三个属性的总结:

1.动画缓冲属性timingFunction

动画实际上就是在一段时间内随着某个特定速率执行变化的过程,现实中的任何物体都会在运动中经历加速或者减速的过程,而不是速度骤变;因此,CoreAnimation也内嵌了一系列标准的缓冲函数来使动画看起来更平滑自然,这就是我们要说到的动画缓冲。
timingFunction属性是CAMediaTimingFunction类的一个对象,用来控制图层动画变换的速度;使用它需要调用+functionWithName:的构造方法,下面是可传入的变量的介绍:

变量名 具体说明
KCAMediaTimingFuncationLinear 默认,匀速执行动画
KCAMediaTimingFuncationEaseIn 先慢慢加速,后突然停止
KCAMediaTimingFuncationEaseOut 先全速开始,再慢慢减速停止
KCAMediaTimingFuncationEaseInEaseOut 先慢慢加速,再慢慢减速
KCAMediaTimingFuncationDefault 效果同KCAMediaTimingFuncationEaseInEaseOut

这五种不同的缓冲效果如下:

动画缓冲属性timingFunction.jpg

通过这种方法控制动画速度,其实是使用不同的变量创建了不同的计时函数。比如KCAMediaTimingFuncationLinear选项创建的是一个线性的计时函数,这也是CAAnimation的timingFunction属性为空时候的默认函数。

注意:KCAMediaTimingFuncationDefault相比KCAMediaTimingFuncationEaseInEaseOut的加速和减速过程稍微有些慢,两者区别很难察觉;可能苹果也觉得它更适合用于隐式动画,就作为了隐式动画的默认效果;但是创建显式的CAAnimation时,KCAMediaTimingFuncationLinear才是默认效果而非KCAMediaTimingFuncationDefault;

UIKit动画其实也同样支持这些缓冲效果的使用,在我们使用UIViewAnimationBlock实现动画的时候,可以给options参数提供了如下的常量来修改缓冲效果:

变量名 具体说明
UIViewAnimationOptionCurveLinear 默认,匀速执行动画
UIViewAnimationOptionCurveEaseIn 先慢慢加速,后突然停止
UIViewAnimationOptionCurveEaseOut 先全速开始,再慢慢减速停止
UIViewAnimationOptionCurveEaseInOut 先慢慢加速,再慢慢减速

2.动画代理属性delegate

/* Delegate methods for CAAnimation. */
@protocol CAAnimationDelegate <NSObject>

@optional
//动画开始时调用
- (void)animationDidStart:(CAAnimation *)anim;
//动画结束时调用
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;
@end

3.removedOnCompletion

removedOnCompletion属性默认为YES,表示动画完成后就会从图层上移除,图层也会恢复到动画执行前的状态;当其修改为NO时,那么图层将会保持动画结束后的状态,此时的fillMode属性也将生效;

另外,removedOnCompletion设置为NO时,直到我们手动移除动画,否则动画将不会自动释放;所以通常我们此时会给动画添加一个非空的键,这样可以在不需要动画的时候把它从图层上移除;

四、CAPropertyAnimation基类

CAPropertyAnimation是一个抽象类,不能直接用于实现CALayer动画操作,但是它的类定义中增加用于设置CALayer可被实现动画的属性keyPath,总结这些属性如下:

属性 解读
transform.rotation 默认围绕z轴旋转,相当于transform.rotation.z
transform.rotation.x
transform.rotation.y
transform.rotation.z
分别围绕x轴、y轴、z轴旋转;
transform.scale 在所有方向上进行缩放
transform.scale.x
transform.scale.y
transform.scale.z
分别在x轴、y轴、z轴方向上缩放;
transform.translation 平移到指定坐标点
transform.translation.x
transform.translation.y
transform.translation.z
分别在x轴、y轴、z轴方向上平移;
zPosition z轴位置
opacity 透明度
backgroundColor 背景颜色
cornerRadius 圆角大小
borderWidth 边框宽度
bounds 图层大小
contents 寄宿图内容
contentsRect 可视内容
position 图层位置,类似transform.translation
shadowColor 阴影颜色
shadowOffset 阴影偏移
shadowOpacity 阴影透明度
shadowRadius 阴影角度

附:KeyPath官方参考链接

五、基础动画CABasicAnimation

CABasicAnimation即基础动画,在指定可动画属性后,动画会按照预定的参数持续一定时间由初始值变换为终点值。其实,CABasicAnimation就相当于只有开始和结束两个帧的特殊关键帧动画(后续会详解);

1.属性说明

属性 属性说明
fromValue 起始值
toValue 结束值
byValue keyPath属性的变化值

2.动画演示

下面的示例使用CABasicAnimation实现了修改颜色图层colorLayer的背景色为随机颜色的动画,具体的代码如下:

@interface TestBacicAnimation1VC ()<CAAnimationDelegate>
@property (nonatomic,strong) CALayer *colorLayer;
@end

@implementation TestBacicAnimation1VC

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建显示颜色的图层,添加于视图控制器的View上
    CALayer *colorLayer = [CALayer layer];
    colorLayer.frame = CGRectMake(50, 50, 100, 100);
    colorLayer.backgroundColor = [UIColor redColor].CGColor;
    self.colorLayer = colorLayer;
    [self.view.layer addSublayer:colorLayer];
}

- (IBAction)changeColor:(UIButton *)sender{
    //步骤1:创建动画
    CABasicAnimation *animation = [CABasicAnimation animation];
    animation.keyPath = @"backgroundColor";
    //步骤2:设定动画属性
    animation.autoreverses = NO;
    animation.duration = 0.25;
    animation.repeatCount = 1;
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;
    animation.delegate = self;
    UIColor *randomColor = [UIColor randomColor];  //自定义获取随机色的方法
    animation.toValue = (__bridge id _Nullable)(randomColor.CGColor);
    //步骤3:添加动画到图层
    [self.colorLayer addAnimation:animation forKey:@"keyPath_backgroundColor"];
}

- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag{
    //禁用隐式动画
    [CATransaction begin];
    [CATransaction setDisableActions:true];
    self.colorLayer.backgroundColor = (__bridge CGColorRef)anim.toValue;
    [CATransaction commit];
}

效果图如下:

CABasicAnimation.gif

总结创建动画的两种方式如下:

//方法1:实例化同时指定动画类型
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];

//方法2:先实例化,再指定动画类型
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"backgroundColor";

3.关闭隐式动画

对独立图层(即非UIView的关联图层,类似上述例子中的colorLayer)做更新属性的显式动画,我们需要设置一个事务来禁用图层行为,否则动画会发生两次,一次是因为显式的CABasicAnimation,另一次是因为隐式动画,从而导致我们看到的动画异常。

六、关键帧动画CAKeyframeAnimation

CACAKeyfameAnimation是CAPropertyAnimation的另一个子类,它和和CABasicAnimation一样都只能作用于图层对象的单一属性;它们的区别在于:CACAKeyfameAnimation不限制于设置一个起始值和结束值,而是可以根据一连串的值来做动画。其实,CABasicAnimation可看做是只有2个关键帧的CAKeyframeAnimation。

1.关键帧动画常用属性总结

关键帧动画相对于基础动画的具有一些独特的属性,我们现将其总结如下:

属性 具体描述
values 用于提供关键帧数据的数组,数组中每一个值都对应一个关键帧属性值;
数组中的数据类型根据动画类型(KeyPath)而不同;
当使用path的时候,values的值将会被自动忽略;
path 用于提供关键帧数据的路径;
path与values属性作用相同,但是两者互斥,同时指定values和path,path会覆盖values的效果;
keyTimes ktyTimes与Values中的值具有一一对应的关系,用于指定关键帧在动画的时间点,取值范围是[0,1];
若没有设置keyTimes,则每个关键帧的时间是平分动画总时长(duration);
timingFunctions 用于指定每个关键帧之间的动画缓冲效果,这类似于物体运动的加速度;
注意:存在几个子路径就应该在此数组中传入几个元素;
calculationMode 该属性决定了物体在每个子路径下是跳着走还是匀速走,跟timeFunctions属性有点类似;
rotationMode 设置帧动画是否需要按照路径切线的方向运动;

2.实现帧动画:使用values

从关键帧动画的属性可以看出,我们可以总结出关键帧动画的实现方式实际分为两种:
1.通过values设置关键帧属性值数组;
2.通过path设置关键帧路径,而且此种方式的优先级较高;
这里首先测试第一种方式,实现这样的关键帧动画:创建一个紫色滑块在四个坐标点之间滑动;具体的代码实现如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建测试帧动画的紫色图层
    UIView *purpleView = [UIView new];
    purpleView.frame = CGRectMake(0, 0, 50, 50);
    purpleView.center = CGPointMake(50, 100);
    purpleView.backgroundColor = [UIColor purpleColor];
    [self.view addSubview:purpleView];
    
    //步骤1:创建动画
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    //步骤2:设置动画关键帧数据
    NSValue *value1 = [NSValue valueWithCGPoint:CGPointMake(50, 100)];
    NSValue *value2 = [NSValue valueWithCGPoint:CGPointMake(kDeviceWidth -50, 100)];
    NSValue *value3 = [NSValue valueWithCGPoint:CGPointMake(kDeviceWidth -50, kDeviceWidth- 100)];
    NSValue *value4 = [NSValue valueWithCGPoint:CGPointMake(50, kDeviceWidth -100)];
    NSValue *value5 = [NSValue valueWithCGPoint:CGPointMake(50, 100)];
    animation.values = @[value1,value2,value3,value4,value5];
    //步骤3:设定动画属性
    animation.repeatCount = MAXFLOAT; //重复执行
    animation.autoreverses = NO;
    animation.removedOnCompletion = NO;
    animation.duration = 4;
    //animation.keyTimes = @[@(0), @(1 / 10.0), @(5 / 10.0), @(9 / 10.0), @(1) ];
    animation.timingFunctions  = @[[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear],
                                   [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn],
                                   [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut],
                                   [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];

    [purpleView.layer addAnimation:animation forKey:nil];
}

关键帧动画效果如下:

CAKeyframeAnimation_values.gif

3.实现关键帧动画:使用path

现在,我们测试CAKeyframeAnimation使用path实现这样一个动画:一架飞机沿着一个简单的曲线运动飞行;具体的操作包括以下几个步骤:
1.使用UIKit提供的UIBezierPath类创建贝塞尔曲线,作为飞机飞行的路线轨迹;
2.使用CAShapeLayer在屏幕上绘制曲线(此步骤对于动画不是必须的,只是为了动画看起来更直观);
3.创建用于显示飞机的视图,将其设置在贝塞尔曲线的初始位置;
4.创建并执行关键帧动画,实现飞机飞行的曲线动画;

- (void)viewDidLoad {
    [super viewDidLoad];
    //1.创建三次贝塞尔曲线(一种使用起始点,结束点和另外两个控制点定义的曲线);
    UIBezierPath *bezierPath  = [[UIBezierPath alloc] init];
    [bezierPath moveToPoint:CGPointMake(50, 200)];
    [bezierPath addCurveToPoint:CGPointMake(kDeviceWidth - 50, 200) controlPoint1:CGPointMake(150, 50) controlPoint2:CGPointMake(kDeviceWidth - 150, 250)];
    
    //2.绘制飞行路线
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.view.layer addSublayer:pathLayer];
    
    //3.创建显示飞机的视图
    UIImageView *airPlaneImgView = [[UIImageView alloc] init];
    airPlaneImgView.frame = CGRectMake(0, 0, 50, 50);
    airPlaneImgView.center = CGPointMake(50, 200);
    airPlaneImgView.image = [UIImage imageNamed:@"airplane"];
    [self.view addSubview:airPlaneImgView];
    
    //4.设置关键帧动画
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 5.0;
    animation.path = bezierPath.CGPath;
    animation.rotationMode = kCAAnimationRotateAuto; //设置根据曲线的切线自动旋转,让动画更加真实
    [airPlaneImgView.layer addAnimation:animation forKey:nil];
}

关键帧动画效果图如下:

CAKeyframeAnimation_path.gif

七、动画组CAGroupAnimation

CAGroupAnimation顾名思义,就是可以将不同的动画效果组合起来,CABasicAnimation和CAKeyframeAnimation都仅仅作用于单一的属性,而CAAnimationGrop可以设置其animations数组的属性来组合别的动画,从而达到混合多种动画效果的目的;

下面演示一个动画组的示例:组合基础动画和关键帧动画,实现一个滑块在沿path运动过程修改其颜色,具体的测试代码如下:

@interface TestAnimationGroupVC ()

@property (nonatomic,strong) UIView *colorView;
@property (nonatomic,strong) UIBezierPath *bezierPath;

@end

@implementation TestAnimationGroupVC

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建显示颜色的图层
    self.colorView = [UIView new];
    self.colorView.frame = CGRectMake(0, 0, 60, 60);
    self.colorView.center = CGPointMake(50, 200);
    self.colorView.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:self.colorView];
    
    //创建贝塞尔曲线,即帧动画运动轨迹
    self.bezierPath  = [[UIBezierPath alloc] init];
    [self.bezierPath moveToPoint:CGPointMake(50, 200)];
    [self.bezierPath addCurveToPoint:CGPointMake(kDeviceWidth - 50, 200) controlPoint1:CGPointMake(150, 50) controlPoint2:CGPointMake(kDeviceWidth - 150, 250)];
    
    //绘制绘制path,便于观察动画;
    CAShapeLayer *pathLayer = [CAShapeLayer layer];
    pathLayer.path = self.bezierPath.CGPath;
    pathLayer.fillColor = [UIColor clearColor].CGColor;
    pathLayer.strokeColor = [UIColor redColor].CGColor;
    pathLayer.lineWidth = 3.0f;
    [self.view.layer addSublayer:pathLayer];
}

- (IBAction)startAnimation:(UIButton *)sender{   
    //移除可能未执行完的动画,防止多重动画导致异常
    [self.colorView.layer removeAnimationForKey:@"groupAnimation"];
    
    //1.创建基础动画:修改背景色为紫色
    CABasicAnimation *basicAnimation = [CABasicAnimation animation];
    basicAnimation.keyPath = @"backgroundColor";
    basicAnimation.toValue = (__bridge id _Nullable)([UIColor purpleColor].CGColor);
    
    //2.创建关键帧动画
    CAKeyframeAnimation *keyFrameAnimation = [CAKeyframeAnimation animation];
    keyFrameAnimation.keyPath = @"position";
    keyFrameAnimation.path = self.bezierPath.CGPath;
    keyFrameAnimation.rotationMode = kCAAnimationRotateAuto;
    
    //3.创建组动画:组合基础动画和关键帧动画
    CAAnimationGroup *groupAnimation = [CAAnimationGroup animation];
    groupAnimation.animations = @[basicAnimation, keyFrameAnimation];
    groupAnimation.duration = 4.0;
    [self.colorView.layer addAnimation:groupAnimation forKey:@"groupAnimation"];
}

动画组的效果如下:

CAGroupAnimation.gif

八、过渡动画CATransition

1.过渡动画简介

属性动画只能对图层的可动画属性起作用,而过渡动画可以改变非动画属性(比如交换一段文本和图片),或者从层级关系中添加或者移除图层;于是就有了过渡的概念;

过渡动画使用CATransition来实现,它同样是CAAnimation的子类;它并不像属性动画那样在平滑的两个值之间做动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。

过渡动画通常用于删除子控件、添加子控件、切换两个子控件等。

2.过渡动画属性介绍

过渡动画有type和subtype两个关键属性,type用于指定动画类型,subtype用于指定动画移动的方向;
type属性:
type属性是一个NSString类型,用于控制整体动画效果类型,具体的可选类型如下:

type值 动画效果 对应常量 是否支持方向
fade 默认效果,渐变 kCATransitionFade
moveIn 覆盖 kCATransitionMoveIn
Push 退出 kCATransitionPush
Reveal 揭开 kCATransitionReveal
cube 立方体 无(私有类型)
suckEffect 收缩 无(私有类型)
oglFlip 翻转 无(私有类型)
rippleEffect 水波动画 无(私有类型)
pageCurl 页面揭开 无(私有类型) 只支持左右方向
vpageUnCurl 放下页面 无(私有类型) 只支持左右方向
cameraIrisHollowOpen 镜头打开 无(私有类型)
cameraIrisHollowClose 镜头关闭 无(私有类型)

目前为止,我们只能使用type的前四种公开属性,但是我们可以通过一些别的方法来自定义过渡效果(后续介绍);

subtype属性:
subtype属性也是一个NSString类型,用于控制动画方向,具体的可选类型如下:

Subtype类型 具体描述
kCATransitionFromRight 从右向左
kCATransitionFromLeft 从左向右
kCATransitionFromTop 从上向下
kCATransitionFromBottom 从下向上

3.过渡动画的使用

现在设想这样的一个需求:修改UIImageView的image属性,实现淡入淡出的平滑动画的效果;此时我们需要使用CATransition来对非动画属性做动画,具体的关键代码如下:

@interface TestTransition1VC ()

@property (nonatomic,strong) UIImageView *imageView;
@property (nonatomic,strong) NSArray *images;

@property (nonatomic, copy) NSString *type;
@property (nonatomic, copy) NSString *subtype;

@end

@implementation TestTransition1VC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.images = @[[UIImage imageNamed:@"tree_spring"],
                    [UIImage imageNamed:@"tree_summer"],
                    [UIImage imageNamed:@"tree_autumn"],
                    [UIImage imageNamed:@"tree_winter"]];
    
    self.type = kCATransitionFade;
    self.subtype = kCATransitionFromRight;
}

- (void)perforomTransitionAnimation{
    CATransition *transition = [[CATransition alloc] init];
    transition.type = _type;
    transition.subtype = _subtype;
    transition.duration = 0.5;
    [self.imageView.layer addAnimation:transition forKey:nil];
    
    UIImage *currentImage = self.imageView.image;
    NSUInteger index = [self.images indexOfObject:currentImage];
    index = (index + 1) % self.images.count;
    self.imageView.image = self.images[index];
}

过渡动画的效果如下:

CATransition.gif

注意:和属性动画不同,对指定图层一次只能使用那一次CATransition,因此无论对动画的键设置为什么值,过渡动画都会对它的键设置为”transition”,也就是常量KCATransition.

4.隐式过渡

CATransition可以对图层任何变化平滑过渡,这使得它成为那些不好做动画的属性图层行为的理想之选。所以,苹果将CATransition作为设置CALayer的contents属性时的默认行为,对图层contents图片做的改动都会自动附上淡入淡出的效果,这也就解释了隐式动画的原理;
但注意:
1.对于视图关联的图层,过渡动画的默认效果是禁用的;
2.我们不能错误的理解CATransition只可以改变非动画属性,其实它也可以对类似backgroundColor的属性做过渡效果动画;

5.自定义过渡动画

过渡动画的过程就是对原始图层外观截图,然后添加一段动画,平滑过渡到图层改变之后的那个截图效果。如果我们知道如何对图层截图,我们就可以使用属性动画来自定义CATransition动画了。

CALayer有一个-renderInContenxt:方法,通过它可以将图层绘制到Core Graphics的上下文中捕获当前内容的图片;所以现在我们尝试这样的实现:对当前视图控制器View进行截图,然后在改变其背景色的时候对截图快速旋转并且淡出,以达到一种过渡的效果;具体的代码示例如下:

- (void)performAnimation{
    UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
    [self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
    UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
    coverView.frame = self.view.bounds;
    [self.view addSubview:coverView];
    
    //使用自定义方法得到随机颜色(切换后的颜色)
    UIColor *randomColor = [UIColor randomColor];
    self.view.backgroundColor = randomColor;
    
    //使用UIView动画方法来代替属性动画(为了简化代码步骤)
    [UIView animateWithDuration:1 animations:^{
        CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
        transform = CGAffineTransformRotate(transform, M_PI_2);
        coverView.transform = transform;
        coverView.alpha = 0.0;
    } completion:^(BOOL finished) {
        [coverView removeFromSuperview];
    }];
 }

自定义过渡动画的效果如下:

CATransitionn_Custom.gif

注意:-renderInContext:捕获了图层的图片和子图层,但是不能对子图层正确的处理变换效果,而且对视频和OpenGL内容也不起作用。但是使用CATransition,或者使用私有的截屏方式就没有这个限制了。

九、委托模式下的动画区分

对于CAAnimation而言,使用委托模式而不是一个完成块会带来一个问题,那就是设置多个动画时,无法在回调方法中区分。通常视图控制器本身会作为一个委托,但所有动画都会调用同一个回调方法,所以我们需要判断到底是哪个图层的动画调用;

首先,动画本身会作为一个参数传入委托的方法,也许你会认为可以在控制器中把动画存储为一个属性,然后在回调用比较,但实际上并不起作用,因为委托传入的动画参数是原始值的一个深拷贝,从而不是同一个值。最后,这里提供两种思路来解决这个问题:
思路1:唯一key参数
当使用-addAnimation:forkey:添加动画到图层时,对每个动画都关联一个唯一的键,这样就可以对每个图层循环所有键,然后调用animationForKey:来对比结果;

思路2:KVC(键-值-编码)协议
像所有NSObject子类一样,CAAnimation也遵循了KVC协议,就像一个NSDictionary一样允许我们随意设置键值对;于是我们可以使用setValue:forKey:和-valueForKey:来存取属性,通过为对象创建一个键值对来判断区分动画;

验证上述两种思路的具体的代码使用如下:

@interface TestBacicAnimation2VC ()<CAAnimationDelegate>

@property (nonatomic,strong) UIView *colorView;
@property (nonatomic,strong) UIView *opacityView;

@end

@implementation TestBacicAnimation2VC

#pragma mark - Life Cycle

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建显示颜色的图层
    UIView *colorView = [UIView new];
    colorView.frame = CGRectMake(50, 50, 100, 100);
    colorView.backgroundColor = [UIColor redColor];
    self.colorView = colorView;
    [self.view addSubview:self.colorView];
    
    //创建透明度视图
    UIView *opacityView = [UIView new];
    opacityView.frame = CGRectMake(50, 200, 100, 100);
    opacityView.backgroundColor = [UIColor blueColor];
    self.opacityView = opacityView;
    [self.view addSubview:self.opacityView];
}

- (IBAction)startAnimation:(UIButton *)sender{    
    //背景色颜色动画
    CABasicAnimation *animation1 = [CABasicAnimation animation];
    animation1.keyPath = @"backgroundColor";
    animation1.autoreverses = NO;
    animation1.duration = 1;
    animation1.repeatCount = 1;
    animation1.removedOnCompletion = NO;
    animation1.fillMode = kCAFillModeForwards;
    animation1.delegate = self;
    UIColor *randomColor = [UIColor randomColor];  //自定义获取随机色的方法
    animation1.toValue = (__bridge id _Nullable)(randomColor.CGColor);
    [animation1 setValue:@"animation_background" forKey:@"AnimationKey"];
    [self.colorView.layer addAnimation:animation1 forKey:@"key_backgroundColor"];

    //透明度动画
    CABasicAnimation *animation2 = [CABasicAnimation animation];
    animation2.keyPath = @"opacity";
    animation2.autoreverses = NO;
    animation2.duration = 5;
    animation2.repeatCount = 1;
    animation2.removedOnCompletion = NO;
    animation2.fillMode = kCAFillModeForwards;
    animation2.delegate = self;
    animation2.fromValue = @(1);
    animation2.toValue = @(0);
    [animation2 setValue:@"animation_opacity" forKey:@"AnimationKey"];
    [self.opacityView.layer addAnimation:animation2 forKey:@"key_opacity"];
}

//动画结束的代理:区分动画
 - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
     //方法1:唯一key参数
     if([[self.colorView.layer animationForKey:@"key_backgroundColor"] isEqual:anim]){
     
     }
     if([[self.opacityView.layer animationForKey:@"key_opacity"] isEqual:anim]){
     
     }
     
     //方法2:KVC
     NSString *animationValue = [anim valueForKey:@"AnimationKey"];
     NSLog(@"animationValue:%@",animationValue);
     if([animationValue isEqualToString:@"animation_background"]){
         
     }else if([animationValue isEqualToString:@"animation_opacity"]){
         
     }
 }

注意:使用唯一key参数这种方法,必须设置removeOnCompletion为NO,否则通过animaitonForKey:获取的CAAnimation对象为空对象无法进行比较。

十、虚拟属性

属性动画CAPropertyAnimation的keyPath实际上针对的是关键路径而不是一个键,这就意味着属性动画作用的对象可以子属性(即属性的属性)甚至虚拟属性;

那么什么是虚拟属性呢?举个例子来讲,CATransform3D实际上是一个结构体而非一个对象,所以它并不符合KVC相关属性,但是我们却可以使用transform.rotation来实现动画;这其实就是因为transform.rotation是一个CALayer可用于处理动画变换的虚拟属性;

1.虚拟属性的作用

为了理解虚拟属性的用处,我们现在考虑这样一个动画:对一个物体实现旋转动画,由于CALayer并没有显式的给提供角度或者方向之类的属性,所以我们自然想到使用transform属性来实现动画,测试代码具体如下:

@interface TestBacicAnimation3VC ()

@property(nonatomic, strong) UILabel *txtLabel;

@end

@implementation TestBacicAnimation3VC

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建测试虚拟属性的Label
    _txtLabel = [UILabel new];
    _txtLabel.frame = CGRectMake(50, 300, kDeviceWidth -100 , 50);
    _txtLabel.backgroundColor = [UIColor purpleColor];
    _txtLabel.font = [UIFont boldSystemFontOfSize:15];
    _txtLabel.textAlignment = NSTextAlignmentCenter;
    _txtLabel.text = @"测试虚拟属性";
    [self.view addSubview:_txtLabel];
}

 
- (IBAction)startAnimation:(UIButton *)sender{
     //步骤1:创建动画
     CABasicAnimation *animation = [CABasicAnimation animation];
     animation.keyPath = @"transform”;  //代码1
     //步骤2:设定动画属性
     animation.autoreverses = NO;
     animation.removedOnCompletion = NO;
     animation.fillMode = kCAFillModeForwards;
     animation.duration = 1;
     animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI * 2, 0, 0, 1)]; //代码2
     [_txtLabel.layer addAnimation:animation forKey:nil];
 }

在此例中,我们把旋转角度从M_PI(180度)调整到M_PI*2(360度),对比两次动画会发现,txtLabel完全看不到旋转的动画效果;这是因为CATransform3D矩阵做了360度旋转其实适合0度是一样的,所以最后的值根本就没变;

这里就需要用到上述说到的虚拟属性了,为了旋转图层,我们可以针对于transform.rotation关键路径应用动画,而不是transform本身;现在将对上述代码进行修改如下:

//animation.keyPath = @"transform";  //代码1
animation.keyPath = @"transform.rotation”;

//animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI * 2, 0, 0, 1)]; //代码2
animation.byValue = @(M_PI * 2);

再来看动画的效果如下:

CABasicAnimation_VirtualProperty.gif

总结transform.rotation相比transfrom做动画的好处如下:

  1. 可以不通过关键帧,只一个步骤就实现旋转多于180度的动画;
  2. 可以使用相对值而不是绝对值旋转,设置byValue而不是toValue;
  3. 可以不用创建CATransform3D,而是使用一个简单的数值来指定角度;
  4. 不会和transform.position或者transfrom.scale冲突(同样是使用关键路径来做独立的动画属性);

2.虚拟属性原理

我们已经说过CATransform3D是一个结构体而非一个对象,所以transfrom.rotation其实是不存在的,我们不可以直接设置transform.rotation或者transform.scale;

实际上,Core Animation是自动通过CAValueFunction计算的值来更新transform属性的,CAValueFunction将我们赋值虚拟属性transfom.rotation的浮点值转换成了真正能用于摆放图层的CATransform3D矩阵值;我们也可以通过设置CAPropertyAnimation的valuefunction属性来改变,这样我们自定义函数就会覆盖默认函数。

CAValueFuncation对于那些不能简单相加的属性(例如变换矩阵)做动画十分有用,但是此方法的实现细节是私有的,所以,目前我们并不能通过继承来自定义此方法;我们可以通过使用苹果已经提供的常量来改善动画(目前都是和变换矩阵的虚拟属性相关,所以没太多的应用场景了,因为这些属性都有了默认的实现方式)。

十一、在动画过程中取消动画

在使用动画的过程中,我们可能需要适时的移除不要的动画,否则就可能造成内存的泄漏问题;从图层中取消动画的方法有以下两种方式:

//方法1:取消指定动画 
/* Remove any animation attached to the layer for 'key'. */
- (void)removeAnimationForKey:(NSString *)key;
 
//方法2:移除所有动画
/* Remove all animations attached to the layer. */
- (void)removeAllAnimations;

关于移除动画的几点说明:
1.动画一旦被移除,图层的外观就立刻更新到当前的模型图层的值;
2.动画通常默认结束之后被自动移除,除非设置了removeCompletion为NO;
3.动画若设置为结束之后不自动移除,那么我们在不需要的时候需手动移除,否则它会一直在内存中,直到图层被销毁;

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建测试停止动画的Label
    _txtLabel = [UILabel new];
    _txtLabel.frame = CGRectMake(50, 200, kDeviceWidth -100 , 50);
    _txtLabel.backgroundColor = [UIColor purpleColor];
    _txtLabel.font = [UIFont boldSystemFontOfSize:18];
    _txtLabel.textAlignment = NSTextAlignmentCenter;
    _txtLabel.textColor = [UIColor whiteColor];
    _txtLabel.text = @"测试停止动画的Label";
    [self.view addSubview:_txtLabel];
    //添加开始动画的按钮
    [self.view addSubview:self.button];
    [self.button mas_makeConstraints:^(MASConstraintMaker *make) {
        make.bottom.equalTo(self.view).offset(-50);
        make.leading.equalTo(self.view).offset(60);
        make.trailing.equalTo(self.view).offset(-60);
        make.height.mas_equalTo(50);
    }];
}

- (void)onBtnClick:(UIButton *)btn {
    btn.selected = !btn.selected;
    if (btn.selected) {
        //停止动画
        [self.txtLabel.layer removeAnimationForKey:@"Animation_transform_rotation"];
        [self.button setTitle:@"开始动画" forState:UIControlStateNormal];
    }else{
        //开始动画
        CABasicAnimation *animation = [CABasicAnimation animation];
        animation.keyPath = @"transform.rotation";
        animation.delegate = self;
        animation.duration = 5;
        animation.byValue = @(M_PI * 2);
        [self.txtLabel.layer addAnimation:animation forKey:@"Animation_transform_rotation"];
        [self.button setTitle:@"停止动画" forState:UIControlStateNormal];
    }
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
    NSLog(@"The animation stopped (finished:%@)",flag? @"YES" : @"NO");
}

测试取消动画效果图如下:

CAAnimation_cancel.gif

代码分析:
-animationDidStop:finished:方法中的flag参数表明了动画是自然结束还是被打断的;此例中通过停止按钮来终止动画会打印NO,自然完成动画时打印YES;

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