CAAnimation

一、继承关系

CAAnimation - CAPropertyAnimation - CABasicAnimation - CASpringAnimation
CAAnimation - CAPropertyAnimation - CAKeyframeAnimation
CAAnimation - CATransition
CAAnimation - CAAnimationGroup

二、CAAnimation

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

@interface CAAnimation : NSObject<NSSecureCoding, NSCopying, CAMediaTiming, CAAction>
/* 构造方法 */
+ (instancetype)animation;

/* 动画变换速度*/
@property(nullable, strong) CAMediaTimingFunction *timingFunction;

/* 动画代理 */
@property(nullable, strong) id <CAAnimationDelegate> delegate;

/* removedOnCompletion属性默认为YES,表示动画完成后就会从图层上移除,图层也会恢复到动画执行前的状态;
 * 当其修改为NO时,那么图层将会保持动画结束后的状态,此时的fillMode属性也将生效。
 * removedOnCompletion设置为NO时,直到我们手动移除动画,否则动画将不会自动释放,
 * 所以通常我们此时会给动画添加一个非空的键,这样可以在不需要动画的时候把它从图层上移除*/
@property(getter=isRemovedOnCompletion) BOOL removedOnCompletion;

@end

1、CAMediaTiming协议

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

/* 动画开始之前的延迟时间,这里的延迟从动画添加到可见图层上那一刻开始测量
 * Defaults to 0.
 * 设置动画beginTime为1,动画将延时1秒后开始执行
 */
@property CFTimeInterval beginTime;

/* 动画持续时间;
 * (默认值为0,但是实际动画默认持续时间为0.25秒) */

@property CFTimeInterval duration;

/* 动画执行的速度;
 * (默认值为0,减少它会减慢动画的时间,增加它会加快速度)
 * (设置speed为2时,则动画实际执行时间是duration的一半) */

@property float speed;

/* 动画时间偏移量;
 * (设置时长3秒动画的timeOffset为1时,动画会从1秒位置执到最后,再执行之前跳过的部分)*/

@property CFTimeInterval timeOffset;

/* 动画重复次数;默认值是0,但是实际默认动画执行1次;
 * (设置为INFINITY,则一直执行);
 * (duration是2,repeatCount设置为3.5,则完整动画时长7秒)*/

@property float repeatCount;

/* 动画重复的时间,让动画重复执行一个指定的时间;
 * (设置为INFINITY,一直执行)
 * repeatCount和repeatDuration可能会相互冲突,
 * 所以你只需要对其中一个指定非零值,对两个属性都设置非0值的行为没有被定义*/

@property CFTimeInterval repeatDuration;

/* 动画从初始值执行到最终值,是否会反向回到初始值;
 * (设置为YES,动画完成后将以动画的形式回到初始位置)*/

@property BOOL autoreverses;

/* 决定当前对象在非动画时间端段的动画属性值,如动画开始之前和动画结束之后
 * 特别注意:removedOnCompletion需要设置为NO,否则fillMode不起作用
 * kCAFillModeRemoved,默认值,动画开始前和结束后,动画对图层都没有影响,图层依然保持初始值
 * kCAFillModeForwards,动画结束后,图层一直保持动画后的最终状态
 * kCAFillModeBackwards,动画开始前,只要加入动画就会处于动画的初始状态
 * kCAFillModeBoth,综合了kCAFillModeForwards与kCAFillModeBackwards特性;
(动画加入图层到真正执行动画的时间段里,图层保持动画初始状态;动画结束之后保持动画最终状态) */

@property(copy) CAMediaTimingFillMode fillMode;
属性总结

2、动画变换速度 CAMediaTimingFunction

指定动画以怎样的变换速度执行。系统为我们提供了 5 种变换速度:

+ (instancetype)functionWithName:(CAMediaTimingFunctionName)name;
  • KCAMediaTimingFuncationLinear,默认,匀速执行动画
  • KCAMediaTimingFuncationEaseIn,先慢慢加速,后突然停止
  • KCAMediaTimingFuncationEaseOut,先全速开始,再慢慢减速停止
  • KCAMediaTimingFuncationEaseInEaseOut,先慢慢加速,再慢慢减速
  • KCAMediaTimingFuncationDefault,先慢慢加速,再慢慢减速,加速过程更短,减速过程更长

对应速度曲线如下:

动画变换速度

可以看到,速度曲线实际上一种贝塞尔曲线,因此,我们可以使用 系统提供的另外两种方法自定义动画变换速度

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

这两种方法需要我们传入两个作用点,取值范围是[0,1]。

3、动画代理属性 delegate

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

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

@end

三、CAPropertyAnimation

基于属性的动画类,一般不使用它而是使用它的子类。

@interface CAPropertyAnimation : CAAnimation

/* 构造方法
 * path 动画作用于哪个属性*/

+ (instancetype)animationWithKeyPath:(nullable NSString *)path;

@property(nullable, copy) NSString *keyPath;

/* 该属性指定该属性动画是否以当前动画效果为基础 */
@property(getter=isAdditive) BOOL additive;

/* 该属性指定动画是否为累加效果 */
@property(getter=isCumulative) BOOL cumulative;

/* 该属性值是一个CAValueFunction对象,该对象负责对属性改变的插值计算,系统已经提供了默认的插值计算方式,因此一般无须指定该属性*/
@property(nullable, strong) CAValueFunction *valueFunction;

@end

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轴方向上平移;

shadowColor 阴影颜色
shadowOffset 阴影偏移
shadowOpacity 阴影透明度
shadowRadius 阴影角度

zPosition z轴位置
opacity 透明度
backgroundColor 背景颜色
cornerRadius 圆角大小
borderWidth 边框宽度
bounds 图层大小
contents 寄宿图内容
contentsRect 可视内容
position 图层位置,类似transform.translation

四、基础动画CABasicAnimation

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

@interface CABasicAnimation : CAPropertyAnimation

@property(nullable, strong) id fromValue;
@property(nullable, strong) id toValue;
@property(nullable, strong) id byValue;

@end

下面的示例使用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];
}

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

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

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

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

五、CASpringAnimation

继承于CABasicAnimation类,是弹框动画。

@interface CASpringAnimation : CABasicAnimation

/* 质量,影响图层运动时的弹簧惯性,质量越大,弹簧拉伸和压缩的幅度越大。默认为1,且设置的时候必须大于0*/
@property CGFloat mass;

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

/* 阻尼系数,阻止弹簧伸缩的系数,阻尼系数越大,停止越快,默认为10,且设置的时候必须大于0 */
@property CGFloat damping;

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

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

@end

例子:

CASpringAnimation *springAnimation=[CASpringAnimation animationWithKeyPath:@"position"];

// 设置阻尼系数(此值越大弹框效果越不明显)
springAnimation.damping = 1;

// 设置刚度系数(此值越大弹框效果越明显)
springAnimation.stiffness = 10;

// 设置质量大小(越大惯性越大)
springAnimation.mass = 2;

// 设置初始速度
springAnimation.initialVelocity = 10;

[springAnimation setFromValue:[NSValue valueWithCGPoint:layer.frame.origin]];

// 设置结束值
[springAnimation setToValue:[NSValue valueWithCGPoint:CGPointMake(self.view.center.x-50, self.view.center.y-200)]];

springAnimation.autoreverses = YES;

// 设置动画为累加效果(此属性默认为NO。
springAnimation.cumulative = YES;

// 设置动画完成后不返回为原位置。(此属性默认为YES。)
springAnimation.removedOnCompletion = NO;

// springAnimation.repeatCount=MAXFLOAT;(读者自行测试添加此属性的时候会有如何效果)
[springAnimation setDuration:5.0];

[layer addAnimation:springAnimation forKey:@"spring"];

六、关键帧动画CAKeyframeAnimation

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

@interface CAKeyframeAnimation : CAPropertyAnimation

/* 用于提供关键帧数据的数组,数组中每一个值都对应一个关键帧属性值
 * 数组中的数据类型根据动画类型(KeyPath)而不同
 * 当使用path的时候,values的值将会被自动忽略 */
@property(nullable, copy) NSArray *values;

/* 用于提供关键帧数据的路径
 * path与values属性作用相同,但是两者互斥,同时指定values和path,path会覆盖values的效果 */
@property(nullable) CGPathRef path;

/* ktyTimes与Values中的值具有一一对应的关系,用于指定关键帧在动画的时间点,取值范围是[0,1]
 * 若没有设置keyTimes,则每个关键帧的时间是平分动画总时长(duration);*/
@property(nullable, copy) NSArray<NSNumber *> *keyTimes;

/* 用于指定每个关键帧之间的动画缓冲效果,这类似于物体运动的加速度
 * 注意:存在几个子路径就应该在此数组中传入几个元素;*/
@property(nullable, copy) NSArray<CAMediaTimingFunction *> *timingFunctions;

/* 该属性决定了物体在每个子路径下是跳着走还是匀速走,跟timeFunctions属性有点类似 */
@property(copy) CAAnimationCalculationMode calculationMode;

/* For animations with the cubic calculation modes, these properties
 * provide control over the interpolation scheme. Each keyframe may
 * have a tension, continuity and bias value associated with it, each
 * in the range [-1, 1] (this defines a Kochanek-Bartels spline, see
 * http://en.wikipedia.org/wiki/Kochanek-Bartels_spline).
 *
 * The tension value controls the "tightness" of the curve (positive
 * values are tighter, negative values are rounder). The continuity
 * value controls how segments are joined (positive values give sharp
 * corners, negative values give inverted corners). The bias value
 * defines where the curve occurs (positive values move the curve before
 * the control point, negative values move it after the control point).
 *
 * The first value in each array defines the behavior of the tangent to
 * the first control point, the second value controls the second
 * point's tangents, and so on. Any unspecified values default to zero
 * (giving a Catmull-Rom spline if all are unspecified). */

@property(nullable, copy) NSArray<NSNumber *> *tensionValues;
@property(nullable, copy) NSArray<NSNumber *> *continuityValues;
@property(nullable, copy) NSArray<NSNumber *> *biasValues;

/* 设置帧动画是否需要按照路径切线的方向运动 */
@property(nullable, copy) CAAnimationRotationMode rotationMode;

@end

从关键帧动画的属性可以看出,我们可以总结出关键帧动画的实现方式实际分为两种:
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使用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];
}

七、CAAnimationGroup

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

@interface CAAnimationGroup : CAAnimation

@property(nullable, copy) NSArray<CAAnimation *> *animations;

@end

下面演示一个动画组的示例:组合基础动画和关键帧动画,实现一个滑块在沿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"];
}

八、过渡动画CATransition

@interface CATransition : CAAnimation

/* 指定动画类型 */
@property(copy) CATransitionType type;

/* 指定动画移动的方向 */
@property(nullable, copy) CATransitionSubtype subtype;

/* */
@property float startProgress;
@property float endProgress;

@end

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];
}

过渡动画的效果如下:

image

注意:和属性动画不同,对指定图层一次只能使用那一次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];
    }];
 }

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

image

注意:-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);

再来看动画的效果如下:

image

总结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");
}

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

image

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

作者:梧雨北辰
链接:https://www.jianshu.com/p/c22918a5e7ca
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容