开篇叨叨

iOS动画简单说就是在一段时间内CALayer的Animatable Property发生了变化,一个完整动画包括时间成份(时长,时间变化曲线,动画速度),动画内容(哪些属性发生变化),属性变化范围:(fromValue,toValue),iOS的动画是基于Core Animation的,Core Animation将大部分实际的绘图任务交给了图形硬件GPU来处理,GPU会加速图形渲染的速度。这种加速技术让动画拥有更高的帧率并且显示效果更加平滑,不会加重CPU的负担而影响程序的运行速度。一般我们在项目中除了十分复杂或者比较简单的动画使用CAAnimation外,一般都是使用三方的动画库,比如JHChainableAnimationsLSAnimator,这两种都是支持Objective C 和 Swift, 并且是通过链式调用,用起来还是十分方便的,对了还有Facebook 的 POP,但是对于复杂的动画还是部分需要自己来封装。还有一类帧动画我们一般用lottie-ios,只要设计提供个json文件和资源文件就可以完成十分酷炫的动画。

这篇博客将从如下几个方面对iOS的动画做个总结:

  • CAAnimation的继承结构
  • 动画相关的CALayer
  • 时间系统
  • 动画事务管理
CAAnimation的继承结构

要了解iOS动画最重要的是对整个动画的继承结构,下图是CAAnimation的继承结构图:

CAAnimation

CAAnimation 这个是基类,它不是为了创建对象而存在的,它主要用于存放一些通用性的属性和方法,它有两个很重要的属性:timingFunction,delegate。

  • timingFunction 表示的是时间曲线,我们知道并不是所有的动画都是线性变化的,它们可能先快后慢,或者先慢后快,这就是由timingFunction决定的。timingFunction 是 CAMediaTimingFunction类型,它的可能值如下:
kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
  • delegate 这个是CAAnimationDelegate类型的代理。CAAnimationDelegate很简单,它只有两个方法:
- (void)animationDidStart:(CAAnimation *)anim;  
//动画开始
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag;
//动画停止,这种停止可以是因为动画结束,也有可能是因为动画从它的载体上移除,这里可以通过flag来区分。

同时CAAnimation遵循CAMediaTiming协议,这里包含了动画的很多基本属性:

duration:动画播放一次所用的时间
beginTime:动画起始时间
可以使用它来达到延迟执行动画的效果 beginTime = CACurrentMediaTime()+1;
speed:
如果把动画的duration设置为3秒,而speed设置为2,动画将会在1.5秒结束,并且动画速度是有层级关系的:一个动画的speed1.5,它同时是一个speed2的动画组的一个动画成员,则它将以3倍速度被执行。
timeOffset:
这个属性往往会结合其他属性类来控制动画的“当前时间”,下面是暂停动画的代码,在开发中十分实用,因为动画要么开始要么移除,但是如果只是暂停,后续还要继续就可以通过下面方法来实现。

-(void)pauseLayer:(CALayer*)layer {
CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];
layer.speed = 0.0;
layer.timeOffset = pausedTime;
}

-(void)resumeLayer:(CALayer*)layer {
CFTimeInterval pausedTime = [layer timeOffset];
layer.speed = 1.0;
layer.timeOffset = 0.0;
layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
layer.beginTime = timeSincePause;
}

这里简单得对上面的动画暂停恢复方法进行解释下:

在CAMediaTiming协议的timeOffset的注释上,官方给出以下公式:

t = (tp - begin) * speed + offset

tp是父layer的时间点,为了方便理解,可以认为是绝对时间。
暂停的时候speed等于0,t = offset.要让t停在此刻,也就是让t = [layer convertTime:CACurrentMediaTime() fromLayer:nil]。这时候offset就必须等于[layer convertTime:CACurrentMediaTime() fromLayer:nil]

我们再来看下恢复,恢复的时候speed = 1,offset = 0, t = 上一次停留时间也就是t = (tp - begin) = pausedTime;
所以begin = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pauseTime;

repeatCount/repeatDuration:动画的重复执行,二者不可同时使用
repeatCount指定重复的次数
repeatDuration指定重复执行持续的时间,一到时间就停止重复执行。
autoreverses:是否自动翻转动画
将使动画先正常走,完了以后反着从结束值回到起始值,如果指定了autoreverse = YES 那么完成一次autoreverse就需要 2*duration
fillMode : 动画填充模式

fillMode的作用就是决定当前对象过了非Active时间段的行为. 比如动画开始之前,动画结束之后,如果要让动画在开始之前显示fromValue的状态,设置fillMode为kCAFillModeBackwards。如果想让动画结束后停留在toValue的状态,就应该设置为kCAFillModeForwards。如果两种都要有,就设置kCAFillModeBoth。注意必须配合animation.removeOnCompletion = NO才能达到以上效果

CALayer遵循CAMediaTiming协议,每个CALayer都有个时间系统,可以通过CACurrentMediaTime() 来获得当前时间,可以通过CALayer的

- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;

在两个不同CALayer中进行转换。

下面是介绍这些属性的一个效果图,可以看这些参数来熟悉这些概念。

CAPropertyAnimation

CAPropertyAnimation 是属性动画,它其实也还是一个基类,不能用于创建对象,它有一个很重要的属性keyPath,用于指定哪些属性可以用于动画控制,那么我们怎么知道有哪些属性有属性动画呢?可以打开CALayer.h 搜索 “Animatable” 关键字,这些属性都是具有属性动画:将其罗列如下所示:

transform.scale     
transform.scale.x
transform.scale.y
transform.rotation
transform.rotation.x
transform.rotation.y
transform.rotation.z
transform.translation
transform.translation.x
transform.translation.y
transform.translation.z
opacity
margin
zPosition
backgroundColor
cornerRadius
borderWidth
bounds
contents
contentsRect
cornerRadius
frame
hidden
mask
masksToBounds
opacity
position
shadowColor
shadowOffset
shadowOpacity
shadowRadius

它还有两个很关键的属性:additive/cumulative

  • additive为YES时,变化值整体加上layer的当前值,也即是它其实是加性属性。
  • cumulative 为YES时,每次的值要加上上一次循环的的结束值。需要repeatCount>1的时候才能看出效果。
CAAnimationGroup

CAAnimationGroup 可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行,可以通过设置动画对象的beginTime属性来更改动画的开始时间

CATransition

CATransition用于做转场动画,也就是layer的两种状态之间的过渡。能够为层提供移出屏幕和移入屏幕的动画效果:
CATransition有如下关键属性:

type:动画过渡类型
subtype:动画过渡方向
startProgress:动画起点(在整体动画的百分比)
endProgress:动画终点(在整体动画的百分比)

type的值可以是如下枚举值:

kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal

subtypes的值可以是如下枚举值:

kCATransitionFromRight
kCATransitionFromLeft
kCATransitionFromTop
kCATransitionFromBottom

各个属性的说明如下图所示:

下面是一个简单的例子:

CATransition *animation = [CATransition animation];
animation.type = kCATransitionPush;//设置动画的类型
animation.subtype = kCATransitionFromRight; //设置动画的方向
animation.duration = 1.0f;
[testView.layer addAnimation:animation forKey:@"pushAnimation"];
CABasicAnimation

CABasicAnimation:基础动画,通过属性修改进行动画参数控制,它有三个关键属性fromValue和toValue,还有byValue,当然不要忘记了它也是CAPropertyAnimation,在CAPropertyAnimation中可以指定要变化的keyPath.到这里为止动画内容(keyPath)有了,动画时间(duration和timingFunction)有了,开始和结束状态(fromValue和toValue)有了。通过插值就可以得到任意一个时间点的状态,然后渲染绘制形成一系列关联的图像,形成动画,也就是说到CABasicAnimation为止已经可以创建出一个动画了。

CAKeyframeAnimation

如果说CABasicAnimation是一帧动画的话,CAKeyframeAnimation就是多个CABasicAnimation组成的帧动画,我们知道动画其实就是一帧帧画面连续变化得到的,我们不可能提供无限的连续的动画帧,我们只需要提供必要的关键帧就可以通过插值来完成了,再加上人眼的视觉暂留效应就可以在大脑中留下连续运动的动画。CAKeyframeAnimation有两个最为关键的属性values和keyTimes,values就是各个关键帧的数据,keyTimes是各个关键帧的时间点。keyTimes这个可选参数,当keyTimes没有设置的时候,各个关键帧的时间是均分的。

除了values和keyTimes属性 CAKeyframeAnimation 还有个path属性也很关键,为什么需要这个属性,大家试想下,如果我们要实现一个沿着心型❤️轨迹运动的动画,那么我们要怎么获得keyTimes和values?为了高度拟合轨迹我们要做很多计算才能得到,这显然不是正确的做法,API应该是以简洁易用为目的,所以这时候就可以通过path来描述运动轨迹。这个值默认是nil当其被设定的时候values属性就会被覆盖.

还有比较重要的属性就是calculationMode和rotationMode

calculationMode 影响着关键帧之间的数据如何进行推算:

kCAAnimationLinear          通过线性插值
kCAAnimationDiscrete 不进行插值,只显示关键帧的画面,看到的动画会是跳跃的
kCAAnimationPaced 这个也是线性插值,但跟第一个的区别是它是整体考虑的。它会忽略掉keyTimes属性,重新计算keyTimes以达到全局匀速的效果。注意这时候keyTimes和timingFunctions是不起作用的;
kCAAnimationCubic 效果就是把转折点变得圆滑
kCAAnimationCubicPaced kCAAnimationPaced和kCAAnimationCubic两种效果叠加

rotationMode只有帧动画使用path路径的时候才有效果的,当值为kCAAnimationRotateAuto是,会把layer旋转,使得layer自身的x轴是跟路径相切的,并且x轴方向跟运动方向一致,使用kCAAnimationRotateAutoReverse也是相切,但x轴方向跟运动方向相反。

CASpringAnimation

CASpringAnimation是iOS7.0后新增的,它提供了像弹簧一样的变化规律,它有如下关键的属性:

mass            弹簧质量,影响弹簧的惯性,质量越大,弹簧惯性越大,运动的幅度越大
stiffness 弹簧弹性系数,弹性系数越大,弹簧的运动越快
damping 弹簧阻尼系数,阻尼系数越大,弹簧的停止越快
initialVelocity 初始速率,弹簧动画的初始速度大小,弹簧运动的初始方向与初始速率的正负一致
CoreAnimation的使用步骤
  • 创建CAAnmation子对象
  • 设置CAAnmation的属性
  • 调用CALayer的addAnimation:forKey:将CAAnimation对象添加到CALayer上,就能执行动画
  • 调用CALayer的removeAnimationForKey方法可以停止CALayer中的动画。
CGAffineTransform 仿射变换

在最初开发Android 的时候接触到仿射变换,一直不理解什么是仿射,其实的仿射变换就是将视图的每个点乘以一个仿射矩阵,得到一个变换后的视图,具体变换过程这里不做展开,这里会涉及到数学的矩阵运算,但是要记住一点:图层中平行的两条线在变换之后任然会保持平行。

可以通过如下方法创建CGAffineTransform:

CGAffineTransformMakeRotation(CGFloat angle)             //旋转变换
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy) //缩放变换
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty) //平移变换

下面是在一个仿射变换基础上叠加另一个仿射变换的方法。


CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)

如果已经存在了两个仿射变换,现在要将它们合在一起的时候可以调用:

CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);

如果需要一个什么都不变的变换,就可以实用

CGAffineTransformIdentity

下面是一个简单的例子:

CGAffineTransform transform = CGAffineTransformIdentity; 
transform = CGAffineTransformScale(transform, 2, 2);
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 45.0);
transform = CGAffineTransformTranslate(transform, 250, 0);
self.layerView.layer.affineTransform = transform;

UIView可以通过设置transform属性做变换。需要注意的是CALayer同样也有一个transform属性,但它的类型是CATransform3D,不要被误导了,真正用于仿射变换的是affineTransform属性。

CATransform3D 3D变换

和2D仿射变换类似CATransform3D也提供了对应的旋转,缩放,平移的方法,如下所示:

CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)

透视投影

上面的变换是没有透视效果的,原先平行的还是保持平行,因此我们需要做透视投影处理,要达到透视效果可以通过设置CATransform3D的m34值来实现,那么m34要怎么设置呢?


一般而言 m34 = -1.0 / distance (distance视角相机和屏幕之间的距离,以像素为单位,一般不需要仔细计算,根据实际效果500-1000中选择一个就好)

CATransform3D transform = CATransform3DIdentity;
transform.m34 = - 1.0 / 500.0;
transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0);
self.layerView.layer.transform = transform;

对于一个View,Core Animation定义消失点位于View的anchorPoint,但是这里会有个问题:一个界面可能有多个View要做3D变换,但是一个界面通常只能有一个消失点,所以不能通过设置position来移动,因为当改变一个图层的position,你也改变了它的消失点,因此在做3D变换的时候需要记住:当视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置,而不是直接改变它的position,这样所有的3D图层都共享一个消失点。还有一种方式就是通过sublayerTransform,sublayerTransform和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,因此我们可以把消失点设置在容器图层的中点,这样就不需要再对子图层分别设置了。这意味着你可以随意使用position和frame来放置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的消失点用变换来做平移。

CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = - 1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0);
self.layerView1.layer.transform = transform1;
CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0);
self.layerView2.layer.transform = transform2;
动画的事务管理

事务这个概念在很多地方都会遇到,比如数据库操作等,在iOS动画中事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何CALayer的Animatable Properties设置都应该属于某个CATransaction(在修改CALayer的Animatable Properties时如果发现当前没有事务,则会自动创建一个事务),在一个CATransaction中可以同时对多个layer的属性进行修改,CATransaction负责对layer的修改的捕获和提交,在事务中的变化并不会立刻生效,而是在事务提交的时候将这些图层树的变化成批包装起来,一次性发送到渲染服务进程,在我们看来就是图层的各个属性会在同一时刻由一个动画过渡到新值。Core Animation在每个Runloop周期中自动开始一次新的事务,事务的提交发生在RunLoop进入休眠或者退出期间,即使不显式的用[CATransaction begin]开始一次事务,任何在一次Runloop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。事务可以嵌套,当事务嵌套时候,只有当最外层的事务commit了之后,整个动画才开始.在没有RunLoop的地方设置CALayer的Animatable Properties,则必须使用显式的事务,有RunLoop的情况下会自动创建CATransation,CATransaction没有属性或者实例方法,并且也不能用+alloc和-init方法创建它。但是可以用+begin和+commit分别来入栈或者出栈,如下所示:

[CATransaction begin];
//动画内容
[CATransaction commit];

UIView动画中:+beginAnimations:context:和+commitAnimations ,以及UIView基于Block的动画方法:+animateWithDuration:animations:也都是基于CATransaction的封装。

动画相关的CALayer

在介绍iOS渲染的时候已经对iOS UIView以及CALayer做了较为详细的介绍,UIView 和 CALayer职责十分明确,一个是负责事件响应,一个是负责界面呈现,在介绍iOS渲染的时候主要关注的是CALayer内容显示的部分,而这里将会重点介绍CALayer的动画特性,其实给View加上动画,本质上是对其CALayer进行操作,CALayer有很多Animatable Property,我们可以基于这些属性做动画效果。

在介绍完CALayer后我们还需要了解一些特殊的CALayer,和CALayer基于Core Graphic的CPU渲染方式不同,它们大多数是基于GPU渲染的,下面是这些Layer的特点和作用:

  • CAShapeLayer
    CAShapeLayer是一个通过矢量图形而不是Bitmap来绘制的CALayer子类.
    CAShapeLayer相对于一般的CALayer有如下特点:

    1. CAShapeLayer使用了硬件加速,而CALayer是基于Core Graphics使用的是CPU绘图,因此渲染速度会快很多。
    2. CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存.
  • CATextLayer
    UILabel最早其实是通过WebKit来实现绘制的,这样就造成了当有很多文字的时候就会有极大的性能压力。而CATextLayer使用了Core Text,所以渲染性能十分快速。

  • CAGradientLayer
    CAGradientLayer是用来生成两种或更多颜色平滑渐变的图层,CAGradientLayer也是基于硬件加速对因此渲染效率也比基于Core Graphic 快很多。

  • CAGradientLayer
    CAGradientLayer 用于高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。

  • CAScrollLayer
    CAScrollLayer用于显示的是可滚动的图层的一部分,可以指定滑动方向和可视区域面积,限制不滑出区域外。

  • CATiledLayer
    有时候我们需要呈现一个很大的,质量很高的图片,这种情况下如果将整个图片加载到内存是不大现实的,一来会占用很大的空间,二来图片加载会很耗时,导致动画卡顿。还有个比较棘手的问题就是,OpenGL对纹理对大小是有限制的,如果超过最大纹理大小,Core Animation将会强制用CPU处理图片而不是GPU。为了解决这些问题,CATiledLayer将大图分解成小片然后将他们单独按需载入。从而减小内存占用和加载耗时。

  • CAEmitterLayer
    CAEmitterLayer是一个高性能的粒子引擎,被用来创建实时粒子动画如:烟雾,火,雨等等效果。

  • CAEAGLLayer
    CAEAGLLayer是CALayer的一个子类,用来显示任意的OpenGL图形。

  • AVPlayerLayer
    AVPlayerLayer是用来在iOS上播放视频的。它是高级接口例如MPMoivePlayer的底层实现,提供了显示视频的底层控制。

隐式动画

iOS中的动画有显式动画和隐式动画两种类型,上面介绍的动画都属于显式动画,接下来要介绍的是隐式动画,我们上面介绍过每一个View都有其对应的layer,这个layer是Root Layer,而其他通过CALayer或其子类直接创建的CALayer是非Root Layer。所有非Root Layer在我们设置Animatable Properties的时候都会存在duration为0.25s的隐式动画,而对Root Layer则没有这个过渡,这是为什么呢?实际上无论什么时候修改Animatable Properties。CALayer都会去查找并运行合适的action,什么是action呢?action实际上是一些遵循了CAAction协议的对象,用于定义一个动画需要做的事情,不论是否在block里面修改view的属性,都会触发CALayer查找合适的CAAction.

@protocol CAAction

- (void)runActionForKey:(NSString *)event object:(id)anObject
arguments:(nullable NSDictionary *)dict;

@end

CAAction协议中有个runActionForKey方法,我们可以在这个方法中对layer自定义某些动画效果。下面是一个简单的例子:

@interface CustomAction : NSObject<CAAction>
@property (nonatomic) CGColorRef currentColor;
@end
@implementation CustomAction
- (void)runActionForKey:(NSString *)key object:(id)anObject arguments:(NSDictionary *)dict {
CustomLayer *layer = anObject;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"backgroundColor"];
animation.fromValue = (id)[UIColor greenColor].CGColor;
animation.toValue = (id)[UIColor redColor].CGColor;
animation.duration = 5;
[layer addAnimation:animation forKey:@"backgroundColor"];
}
@end

除了修改layer属性外,触发搜索action事件的触发点包括如下几个:

layer的属性被修改。包括layer的任何属性,不仅仅只是会产生动画的部分。
layer被添加到layer阶层。标识符key是kCAOnOrder。
layer被移除layer阶层。标示符key是kCAOnOrderOut。

action的搜索的过程:

layer调用actionForKey:方法搜索需要执行的action对象

如果layer设置了代理,layer会向它的delegate发送actionForLayer:forKey:消息来要求返回对应当前属性变化的CAAction。
actionForLayer:forKey:有三种返回情况:
1. 返回CAAction的对象,这时候将会使用这个CAAction来实现这个动画
2. 返回NSNull,这时候就会停止搜索,并且告诉layer不需要执行任何动画
3. 返回nil,这时候layer就会继续往下找
4. 查找layer的action属性,看可以是否有对应的值
5. 查找layer的style属性。
6. 调用defaultActionForKey返回对应key的默认action,一般是CABasicAnimation。

找到action对象后,调用action对象的runActionForLayer:object:arguments:方法执行相关操作。

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event{
if ([event isEqualToString:@"backgroundColor"]) {
MyAction *action = [MyAction new];
return action;
}
return nil;
}

@interface MyAction : NSObject<CAAction>
@end

@implementation MyAction
- (void)runActionForKey:(NSString *)event object:(id)anObject arguments:(NSDictionary *)dict{
CustomLayer *layer = anObject;
CABasicAnimation *animation = [CABasicAnimation animation];
animation.duration = 3.0f;
[layer addAnimation:animation forKey:@"backgroundColor"];
}
@end

有了上面的介绍大家应该会对从修改属性到动画执行的整个流程有了比较详细的了解了吧,这里就很好理解为什么Root Layer没有隐式动画而非Root Layer会有隐式动画了,其实最大的玄机在于CALayer的delegate对象,我们知道Root Layer的delegte是对应的UIView,因此可以推测之所以Root Layer没有隐式动画就是因为UIView在一般情况下actionForLayer:forKey返回一个 NSNull,只有当属性改变发生在动画block 中时,view 才会返回实际的动作。而非Root Layer,delegate在不设置的情况下为空,所以返回的是通过defaultActionForKey返回的对应key的默认Action.

但是有时候我们又需要关闭这些隐式动画,这种情况就可以通过如下方式来关闭:

[CATransaction begin];
// 关闭隐式动画
[CATransaction setDisableActions:YES];
//原本会产生隐式动画的部分
[CATransaction commit];

设置setDisableActions:为YES后,layer的actionForKey:方法将不会被调用,隐式动画也不会生成。

UIView 过渡动画

视图过渡动画一般用在比如删除或增加子视图的时候。

+ (void)transitionWithView:(UIView *)view 
duration:(NSTimeInterval)duration
options:(UIViewAnimationOptions)options
animations:(void (^)(void))animations
completion:(void (^)(BOOL finished))completion;

view 就是指定的需要做动画过渡的视图,或者要做动画视图的容器视图。
animations 中可以执行比如添加、删除、显示或隐藏指定view 的子视图
其他的和UIView的block动画类似。

+ (void)transitionFromView:(UIView *)fromView 
toView:(UIView *)toView
duration:(NSTimeInterval)duration
options:(UIViewAnimationOptions)options
completion:(void (^ __nullable)(BOOL finished))completion

这个动画用于从一个view转变到另一个view过程的动画,在动画过程中,首先将 fromView 从父视图中删除,然后将 toView 添加,就是做了一个替换操作

UIImageView 帧动画
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSArray *imageArray = [self getImageArrayWithGIFNameWit:@"aisi"];
self.imageView.animationImages = imageArray;
self.imageView.animationDuration = 3;
self.imageView.animationRepeatCount = MAXFLOAT;
[self.imageView startAnimating];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[_imageView stopAnimating];
});
}

- (NSArray<UIImage *> *)getImageArrayWithGIFNameWit:(NSString *)imageName {
NSMutableArray *imageArray = [NSMutableArray array];
NSString *path = [[NSBundle mainBundle] pathForResource:imageName ofType:@"gif"];
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
return nil;
}
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
size_t count = CGImageSourceGetCount(source);
if (count <= 1) {
[imageArray addObject:[[UIImage alloc] initWithData:data]];
} else {
for (size_t i = 0; i < count; i++) {
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);

[imageArray addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];

CGImageRelease(image);
}
}
CFRelease(source);
return imageArray;
}
转场动画
1. UIViewController 容器转场动画

这个适用于多个子UIViewController在一个容器UIViewController中进行切换的动画,它的好处在没有转换到toViewController的时候toViewController没有显示也不会load,这样减少内存的使用。

 UIViewController *firstViewController = [UIViewController new];
firstViewController.view.backgroundColor = [UIColor redColor];
[self addChildrenController:firstViewController
locateSubViewBlock:^(UIView * _Nonnull parentControllerRootView, UIView * _Nonnull childControllerRootView) {
childControllerRootView.frame = parentControllerRootView.frame;
}];

UIViewController *secondViewController = [UIViewController new];
secondViewController.view.backgroundColor = [UIColor yellowColor];
secondViewController.view.frame = self.view.frame;
self.currentViewController = firstViewController;

要切换的时候调用:

[self changeControllerFromOldController:self.currentViewController toNewController:secondViewController];

- (void)changeControllerFromOldController:(UIViewController *)oldController toNewController:(UIViewController *)newController {
[self addChildViewController:newController];

[self transitionFromViewController:oldController toViewController:newController duration:8 options:UIViewAnimationOptionTransitionCurlUp animations:^{

} completion:^(BOOL finished) {

if (finished) {
//移除oldController,但在removeFromParentViewController:方法前不会调用willMoveToParentViewController:nil 方法,所以需要显示调用
[newController didMoveToParentViewController:self];
[oldController willMoveToParentViewController:nil];
[oldController removeFromParentViewController];
self.currentViewController = newController;

} else {
self.currentViewController = oldController;
}
}];
}

如果不是非常理解,大家还可以查看这篇博客:iOS addChildViewController方法

2. UIViewController之间跳转的转场动画

在iOS中一个ViewController又被称为一个场景,转场动画就是两个场景之间切换时候的动画,iOS已经默认为我们提供了四种转场动画,可以通过UIViewController的modalTransitionStyle属性来指定:

typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
UIModalTransitionStyleCoverVertical = 0, //从下向上弹起
UIModalTransitionStyleFlipHorizontal, //水平翻转
UIModalTransitionStyleCrossDissolve, //渐隐渐现
UIModalTransitionStylePartialCurl, //翻页
};

有些情况下这些转场动画并不能满足我们产品的需求这时候就需要我们自定义转场动画来满足了。iOS中的场景切换是一个蛮强大的一个模块,下面是整个模块的关系图,后面会针对这个图进行梳理:

2.1 Present / Dismiss 动画

首先我们以最常见的一个ViewController present 另外一个ViewController 为例子:

1. 代码触发一个presentViewController。
2. UIKit询问要过渡到的target ViewController 是否有自定义的过渡动画代理。如果没有,则UIKit将使用iOS自带的过渡动画
3. 如果有过渡动画代理,UIKit则会通过过渡动画代理transitioningDelegate,获取到动画控制器。比如通过 animationControllerForPresentedController(_:presentingController:sourceController:)方法获取到动画控制器,如果返回空,则使用默认的动画控制器。
4. 一旦找到了动画控制器,UIKit构建上下文对象UIViewControllerContextTransitioning。
5. 接着,UIKit通过动画控制器UIViewControllerContextTransitioning 的 transitionDuration(_:)方法获取动画执行时长。
6. 再接着调用动画控制器的animateTransition(_:)完成过渡动画。
7. 最后动画控制器调用上下文对象的completeTransition(_:)方法指示动画完成。

这里也介绍下一个最初困惑我比较久的概念presentingViewController/presentedViewController

假如我们有两个 VC A/B,我们要从A转换到B,我们称A为presentingViewController,称B presentedViewController,当从 B 结束转换回到 A 时,我们仍然称呼 A 为 presentingViewController,B 为 presentedViewController。这是让我每次比较懵逼的地方。

再说得详细点:

  • presentingViewController[负责呈现的ViewController]

关于presentingViewController 官方的说明如下:

The view controller that presented this view controller.
When you present a view controller modally (either explicitly or implicitly) using the presentViewController:animated:completion: method, the view controller that was presented has this property set to the view controller that presented it. If the view controller was not presented modally, but one of its ancestors was, this property contains the view controller that presented the ancestor. If neither the current view controller or any of its ancestors were presented modally, the value in this property is nil.

也就是说如果我们沿着viewController堆栈,但凡有一个是通过 presentViewController:animated:completion: 方法推出的,那么presentingViewController 的值就是推出的堆栈的起点viewController.也就是说只有在调用presentViewController:animated:completion: 的时候才会更改这个值,如果通过push的话,就会继承操作的发起者的presentingViewController值。

举个简单的例子:

有五个控制器 ABCDEF,应用启动首先显示RootViewController A,之后A通过present方式推出带导航栏的B,B再通过push的方式推出C,C再通过push的方式推出D,D再通过push的方式推出E,那么E的 presentingViewController 就是 B。上面例子中只有B是通过present方式推出的,且B是CDE的父级,那么 D 的presentingViewController也将是B。

  • presentedViewController[被呈现的ViewController]

关于presentedViewController 官方的说明如下:

The view controller that is presented by this view controller, or one of its ancestors in the view controller hierarchy.
When you present a view controller modally (either explicitly or implicitly) using the presentViewController:animated:completion: method, the view controller that called the method has this property set to the view controller that it presented. If the current view controller did not present another view controller modally, the value in this property is nil.

也就是你通过present模态推出了谁,你的presentedViewController就是谁.

OK 回到转场动画的解释上来:

下面以一个例子来说明如何自定义一个转场动画:

正常情况下我们跳转页面代码如下:

IDLTargetViewController * targetViewController = [IDLTargetViewController new];
[self presentViewController:targetViewController animated:YES completion:nil];

但是上面由于没有自定transitioningDelegate所以用的是系统的默认转场动画也就是从下往上弹出,我们现在要自定义转场动画,就需要指定一个transitioningDelegate,这样系统在跳转的时候就会从transitioningDelegate获取对应的遵循UIViewControllerAnimatedTransitioning协议的对象。

targetViewController.transitioningDelegate = [[IDLTransitionDelegate alloc] init];

我们先来看UIViewControllerTransitioningDelegate协议中的几个函数:

//这个函数用来设置当执行present方法时 进行的转场动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
//这个函数用来设置当执行dismiss方法时 进行的转场动画
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

//这个函数用来设置当执行present方法时 进行可交互的转场动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
//这个函数用来设置当执行dismiss方法时 进行可交互的转场动画
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;

//返回UIPresentationController处理转场
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(nullable UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

这里我们只实现present动画那么只需要在IDLTransitionDelegate中实现animationControllerForPresentedController方法。

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
return [IDLPresenteContentController new];
}

IDLPresenteContentController是一个实现了UIViewControllerAnimatedTransitioning协议的对象。它用于提供动画事件,动画上下文的对象。

//这个函数用来设置动画执行的时长
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext{
return 2;
}
//这个函数用来处理具体的动画
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {

//1.获取动画的源控制器和目标控制器
ViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
DetailViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];

//2.创建一个imageView 的截图,并把 imageView 隐藏,造成使用户以为移动的就是 imageView 的假象
UIView *snapshotView = [fromVC.ImageView snapshotViewAfterScreenUpdates:NO];
//计算fromVC.view上的fromVC.ImageView.frame相对于container的坐标
snapshotView.frame = [container convertRect:fromVC.ImageView.frame fromView:fromVC.view];

//3.设置目标控制器的位置,并把透明度设为0,在后面的动画中慢慢显示出来变为1
toVC.view.frame = [transitionContext finalFrameForViewController:toVC];
toVC.view.alpha = 0;
toVC.bgImageView.hidden = YES;

//4.都添加到 container 中。注意顺序不能错了
[container addSubview:toVC.view];
[container addSubview:snapshotView];

[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
snapshotView.frame = [container convertRect:toVC.bgImageView.frame fromView:toVC.view];
fromVC.view.alpha = 0;
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
fromVC.ImageView.hidden = NO;
toVC.bgImageView.hidden = NO;
[snapshotView removeFromSuperview];

//一定要记得动画完成后执行此方法,让系统管理 navigation 如果设置为no。可以自己试试
[transitionContext completeTransition:YES];
}];
}

这里最重要的就是animateTransition方法,这个方法的参数中transitionContext也是关键中的关键,可以通过viewControllerForKey在transitionContext中取出跳转的ViewController,可以通过viewForKey获取到对应的view,还可以通过它来获得动画事件等。在animateTransition方法中一般是如下步骤实现整个动画的:

  1. 通过viewControllerForKey/viewForKey获取所有需要的 view 以及 VC
  2. 设定fromView, toView的初始状态
  3. 将toView通过addSubview 添加到 containerView
  4. 获取动画时间
  5. 设定动画
  6. 在动画结束的时候调用[transitionContext completeTransition:YES]来结束动画。

从上面可以看出transitionContext是一个十分关键的地方,它用于提供动画过程中所需要的各种数据,我们来看下它的声明:

//容器视图 用来表现动画
@property(nonatomic, readonly) UIView *containerView;
//下面是几个只读属性
//是否应该执行动画
@property(nonatomic, readonly, getter=isAnimated) BOOL animated;
//是否是可交互的
@property(nonatomic, readonly, getter=isInteractive) BOOL interactive; // This indicates whether the transition is currently interactive.
//是否被取消了
@property(nonatomic, readonly) BOOL transitionWasCancelled;
//转场风格
@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;
//调用这个函数来更新转场过程的百分比 用于可交互动画的阈值
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
//完成可交互的转场交互动作时调用
- (void)finishInteractiveTransition;
//取消可交互的转场交互动作时调用
- (void)cancelInteractiveTransition;
//转场动画被中断 暂停时调用
- (void)pauseInteractiveTransition;
//转场动画完成时调用
- (void)completeTransition:(BOOL)didComplete;
//获取转场中的两个视图控制器
/*
UITransitionContextViewControllerKey的定义
UITransitionContextFromViewControllerKey //原视图控制器
UITransitionContextToViewControllerKey //跳转的视图控制器
*/
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
//直接获取转场中的视图
/*
UITransitionContextFromViewKey //原视图
UITransitionContextToViewKey //转场的视图
*/
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key;
//获取视图控制器的初识位置
- (CGRect)initialFrameForViewController:(UIViewController *)vc;
//获取视图控制器转场后的位置
- (CGRect)finalFrameForViewController:(UIViewController *)vc;
2.2 Interactive 交互动画

上面仅仅介绍的是present动画,但是iOS还支持交互动画,也就是通过手势等界面交互来触发动画。

有了上面的介绍接下来的介绍会稍稍简单点:

首先我们还是先设置transitioningDelegate,并在上面IDLTransitionDelegate中添加如下方法实现:

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{
return [IDLInteractiveTransition new];
}

接下来实现 IDLInteractiveTransition

- (void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
// 把 context 对象保存起来
self.transitionContext = transitionContext;
[super startInteractiveTransition:transitionContext];
}


// 根据手势的偏移来计算当前动画应该有的完成度
- (CGFloat)percentForGesture:(UIScreenEdgePanGestureRecognizer *)gesture {
// 根据 container view 以及 gesture recognizer 计算偏移量
UIView *transitionContainerView = self.transitionContext.containerView;
CGPoint locationInSourceView = [gesture locationInView:transitionContainerView];

// 根据偏移量得出百分比
CGFloat width = CGRectGetWidth(transitionContainerView.bounds);
return (width - locationInSourceView.x) / width;
}

// gesture recognizer 的回调
- (IBAction)gestureRecognizeDidUpdate:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer {
switch (gestureRecognizer.state)
{
case UIGestureRecognizerStateBegan:
break;
case UIGestureRecognizerStateChanged:
// 计算百分比,并返回
[self.transitionContext updateInteractiveTransition:[self percentForGesture:gestureRecognizer]];
break;
case UIGestureRecognizerStateEnded:
// 根据预先设定的阈值决定是结束还是取消,这里我们设定 view 中间是分界线
if ([self percentForGesture:gestureRecognizer] >= 0.5f)
[self.transitionContext finishInteractiveTransition];
else
[self.transitionContext cancelInteractiveTransition];
break;
default:
// 其他情况,取消转场
[self.transitionContext cancelInteractiveTransition];
break;
}
}

上面是通过自己实现一个遵循UIViewControllerInteractiveTransitioning协议的对象,当然也可以通过系统为我们提供的UIPercentDrivenInteractiveTransition来简化代码

2.3 UIPresentationController 实现弹窗效果

UIPresentationController 有很多属性下面将比较重要的给抠出来给大家介绍:

@interface UIPresentationController : NSObject <UIAppearanceContainer, UITraitEnvironment, UIContentContainer, UIFocusEnvironment>

//这个概念见文章上面介绍
@property(nonatomic, strong, readonly) UIViewController *presentingViewController;
@property(nonatomic, strong, readonly) UIViewController *presentedViewController;
//弹窗的模态形式
@property(nonatomic, readonly) UIModalPresentationStyle presentationStyle;
// 转场发生的容器视图
@property(nullable, nonatomic, readonly, strong) UIView *containerView;
// 初始化方法
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(nullable UIViewController *)presentingViewController NS_DESIGNATED_INITIALIZER;
// 动画容器布局,可以在这里布局子元素
- (void)containerViewWillLayoutSubviews;
- (void)containerViewDidLayoutSubviews;
//presentView的位置参数
@property(nonatomic, readonly) CGRect frameOfPresentedViewInContainerView;
//呈现动画时机回调
- (void)presentationTransitionWillBegin;
- (void)presentationTransitionDidEnd:(BOOL)completed;
- (void)dismissalTransitionWillBegin;
- (void)dismissalTransitionDidEnd:(BOOL)completed;

@end

下面是一个使用例子:

@interface IDLPopPresentationController : UIPresentationController<UIViewControllerTransitioningDelegate,UIViewControllerAnimatedTransitioning>

@property (nonatomic, strong, readonly) RACSubject *dissmissSignal;

@end
@implementation IDLPopPresentationController

#pragma mark overide UIPresentationController
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController {
if (self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController]) {
presentedViewController.modalPresentationStyle = UIModalPresentationCustom;
}
return self;
}

- (void)dealloc {
[self.dissmissSignal sendCompleted];
self.dissmissSignal = nil;
}

- (void)presentationTransitionWillBegin {

[self.containerView addSubview:self.dimmingView];
[self.dimmingView addSubview:self.closeBtn];
[self.closeBtn nm_makeFrame:^(NMFrameMaker *make) {
make.right.equalTo(self.dimmingView).margin(25);
make.top.equalTo(self.dimmingView).margin(40);
make.size.nm_equalTo(CGSizeMake(20, 20));
}];
@weakify(self);
[self.closeBtn addTapBlock:^(id obj) {
@strongify(self);
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
[self.dissmissSignal sendNext:nil];
[self.dissmissSignal sendCompleted];
}];

//背景 self.dimmingView 的淡入效果与过渡效果一起执
id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;
[transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
@strongify(self);
self.dimmingView.alpha = 0.65f;
} completion:nil];
}

- (void)presentationTransitionDidEnd:(BOOL)completed {
//如果呈现没有完成,那就移除背景 View
if(!completed){
[self.dimmingView removeFromSuperview];
self.dimmingView = nil;
}
}

- (void)dismissalTransitionWillBegin {
id<UIViewControllerTransitionCoordinator> coordinator = self.presentingViewController.transitionCoordinator;
@weakify(self);
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
@strongify(self);
self.dimmingView.alpha = 0.0;
} completion:nil];
}

- (void)dismissalTransitionDidEnd:(BOOL)completed {
if(completed) {
[self.dimmingView removeFromSuperview];
}
}

//Notifies an interested controller that the preferred content size of one of its children changed.
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container {
[super preferredContentSizeDidChangeForChildContentContainer:container];
if (container == self.presentedViewController) {
[self.containerView setNeedsLayout];
}
}

- (void)containerViewWillLayoutSubviews {
[super containerViewWillLayoutSubviews];
self.dimmingView.frame = self.containerView.bounds;
}

- (CGRect)frameOfPresentedViewInContainerView {
//要呈现的ViewController区域
return CGRectMake(15.f, 73.f, SCREEN_MIN_LENGTH - 30.f , SCREEN_MAX_LENGTH - 103.f);
}

#pragma mark UIViewControllerTransitioningDelegate

- (UIPresentationController* )presentationControllerForPresentedViewController:(UIViewController *)presented
presentingViewController:(UIViewController *)presenting
sourceViewController:(UIViewController *)source {
return self;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source {
return self;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return self;
}

#pragma mark - UIViewControllerAnimatedTransitioning
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return [transitionContext isAnimated] ? 0.3 : 0;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {

UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

__block UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
__block UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

UIView *containerView = transitionContext.containerView;
[containerView addSubview:toView];

BOOL isPresenting = (fromViewController == self.presentingViewController);
if (isPresenting) {
toView.frame = [self frameOfPresentedViewInContainerView];
toView.alpha = self.closeBtn.alpha = 0.0f;
toView.layer.masksToBounds = YES;
toView.layer.cornerRadius = 3;
}

NSTimeInterval duration = [self transitionDuration:transitionContext];
[UIView animateWithDuration:duration animations:^{
if (isPresenting) {
toView.alpha = self.closeBtn.alpha = 1.0f;
} else {
fromView.alpha = self.closeBtn.alpha = 0.0f;
}
} completion:^(BOOL finished) {
BOOL wasCancelled = [transitionContext transitionWasCancelled];
[transitionContext completeTransition:!wasCancelled];
}];
}

- (void)animationEnded:(BOOL) transitionCompleted {

}


#pragma mark Getter/Setters

- (UIView *)dimmingView {
if (!_dimmingView) {
_dimmingView = [[UIView alloc] initWithFrame:self.containerView.bounds];
_dimmingView.alpha = 0.0f;
_dimmingView.backgroundColor = [UIColor colorWithWhite:0.0f alpha:0.65f];
_dimmingView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
}
return _dimmingView;
}

- (NMFocusExpandView *)closeBtn {
if (!_closeBtn) {
_closeBtn = [[NMFocusExpandView alloc] initWithFrame:CGRectZero];
[_closeBtn setImage:NMImage.login_instagram_close_icon];
}
return _closeBtn;
}

- (RACSubject *)dissmissSignal {
if (!_dissmissSignal) {
_dissmissSignal = [RACSubject subject];
}
return _dissmissSignal;
}

使用方法

IDLPopPresentationController *presentationController = [[IDLPopPresentationController alloc] initWithPresentedViewController:instagramLoginViewController
presentingViewController:self];
instagramLoginViewController.transitioningDelegate = presentationController;
[self presentViewController:instagramLoginViewController animated:YES completion:nil];
3 转场动画深入文章推荐

关于转场动画是一个十分大的一个话题,该博客只是帮大家理清楚转场动画的整个过程,对于细节大家还需要深入去挖掘,这里推荐几个比较好的文章大家可以在后续学习中供大家深入学习:

  1. iOS 视图控制器转场详解 Github地址 iOS 视图控制器转场详解 简书地址

  2. 玩转iOS转场动画

Contents
  1. 1. 开篇叨叨
  2. 2. CAAnimation的继承结构
    1. 2.1. CAAnimation
    2. 2.2. CAPropertyAnimation
    3. 2.3. CAAnimationGroup
    4. 2.4. CATransition
    5. 2.5. CABasicAnimation
    6. 2.6. CAKeyframeAnimation
    7. 2.7. CASpringAnimation
  3. 3. CoreAnimation的使用步骤
  4. 4. CGAffineTransform 仿射变换
  5. 5. CATransform3D 3D变换
  6. 6. 动画的事务管理
  7. 7. 动画相关的CALayer
  8. 8. 隐式动画
  9. 9. UIView 过渡动画
  10. 10. UIImageView 帧动画
  11. 11. 转场动画
    1. 11.1. 1. UIViewController 容器转场动画
    2. 11.2. 2. UIViewController之间跳转的转场动画
    3. 11.3. 2.1 Present / Dismiss 动画
    4. 11.4. 2.2 Interactive 交互动画
    5. 11.5. 2.3 UIPresentationController 实现弹窗效果
    6. 11.6. 3 转场动画深入文章推荐