理解了隐式动画后,显式动画就更加通俗易懂了。区别于隐式动画的特点,显式动画就是需要我们明确指定类型、时间等参数来实现效果的动画。除此之外,我们也可以创建非线性动画,比如沿着任意一条曲线运动等; 我们平时最常用的也是显式动画,不仅系统为我们的视图提供了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使用详解
如果根据实现动画时直接操作对象的类型,我们可以简单的将动画分为视图和图层两种;但事实上,无论UIViewAnimaiton动画还是UIViewAnimaitonWithBlock动画都只是对UIView的关联图层CALayer动画的进一步封装。
实现动画的方式.png
我们在使用Core Animation动画之前,有必要对核心动画常见的类和动画属性做一个基本了解;从继承关系的图示中,我们可以十分清晰的看出这些属性设置设置因何而来,以及它们各自的联系。
核心动画类的继承关系.jpg
动画类动画特性CAMediaTiming协议;定义了一段动画内用于控制时间的属性的集合CAAnimation抽象类;作为所有动画类型父类,不可直接使用CAPropertyAnimation抽象类;作为基础动画和帧动画的父类,不可直接使用CABasicAnimation基础动画;用于实现单一属性变化的动画CAKeyFrameAnimation关键帧动画;用于实现单一属性连续变化的动画CAAnimaitionGroup组动画;用于实现多属性同时变化的动画CATrasition转场过渡动画;CAMediaTiming协议定义了一段动画内用于控制时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制,有关CAMediaTimg协议具体的属性如下:
属性参数类型具体描述beginTimeCFTimeInterval动画开始之前的延迟时间,这里的延迟从动画添加到可见图层上那一刻开始测量; (设置动画beginTime为1,动画将延时1秒后开始执行)durationCFTimeInterval动画持续时间; (默认值为0,但是实际动画默认持续时间为0.25秒)speedfloat动画执行的速度; (默认值为0,减少它会减慢动画的时间,增加它会加快速度) (设置speed为2时,则动画实际执行时间是duration的一半)timeOffsetCFTimeInterval动画时间偏移量; (设置时长3秒动画的timeOffset为1时,动画会从1秒位置执到最后,再执行之前跳过的部分)repeatCountfloat动画重复次数;默认值是0,但是实际默认动画执行1次; (设置为INFINITY,则一直执行); (duration是2,repeatCount设置为3.5,则完整动画时长7秒)repeatDurationCFTimeInterval动画重复的时间,让动画重复执行一个指定的时间; (设置为INFINITY,一直执行) repeatCount和repeatDuration可能会相互冲突,所以你只需要对其中一个指定非零值,对两个属性都设置非0值的行为没有被定义;autoreversesBOOL动画从初始值执行到最终值,是否会反向回到初始值; (设置为YES,动画完成后将以动画的形式回到初始位置)fillModeNSStrinng决定当前对象在非动画时间端段的动画属性值,如动画开始之前和动画结束之后试想这样一个问题:在beginTime非0(即动画未真正执行之前),以及removeOnCompletion被设置为NO的动画结束时,我们会遇到这样一个问题:被设置动画的属性应该是什么值? 一种可能是属性与动画没被添加之前保持一致,还有一种可能是保持动画开始之前那一帧或者动画结束那一帧,这就是所谓的填充。 CAMediaTiming的fillMode用来控制填充效果,它是一个NSString类型,有四种常量可供使用:
fillMode类型参数类型具体描述kCAFillModeRemoved (default)NSString默认值,动画开始前和结束后,动画对图层都没有影响,图层依然保持初始值kCAFillModeForwardsNSString动画结束后,图层一直保持动画后的最终状态kCAFillModeBackwardsNSString动画开始前,只要加入动画就会处于动画的初始状态kCAFillModeBothNSString综合了kCAFillModeForwards与kCAFillModeBackwards特性; (动画加入图层到真正执行动画的时间段里,图层保持动画初始状态;动画结束之后保持动画最终状态)特别注意:removedOnCompletion需要设置为NO,否则fillMode不起作用;
时间属性的综合应用.png
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协议,而且另外包含了三个常用的动画属性;下面是对这三个属性的总结:
动画实际上就是在一段时间内随着某个特定速率执行变化的过程,现实中的任何物体都会在运动中经历加速或者减速的过程,而不是速度骤变;因此,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先慢慢加速,再慢慢减速removedOnCompletion属性默认为YES,表示动画完成后就会从图层上移除,图层也会恢复到动画执行前的状态;当其修改为NO时,那么图层将会保持动画结束后的状态,此时的fillMode属性也将生效;
另外,removedOnCompletion设置为NO时,直到我们手动移除动画,否则动画将不会自动释放;所以通常我们此时会给动画添加一个非空的键,这样可以在不需要动画的时候把它从图层上移除;
当我们需要防止动画结束后回到初始状态时,
只需设置removedOnCompletion、fillMode两个属性就可以了。
transformAnima.removedOnCompletion = NO; transformAnima.fillMode = kCAFillModeForwards;解释:为什么动画结束后返回原状态? 首先我们需要搞明白一点的是,layer动画运行的过程是怎样的?其实在我们给一个视图添加layer动画时,真正移动并不是我们的视图本身,而是 presentation layer 的一个缓存。动画开始时 presentation layer开始移动,原始layer隐藏,动画结束时,presentation layer从屏幕上移除,原始layer显示。这就解释了为什么我们的视图在动画结束后又回到了原来的状态,因为它根本就没动过。
这个同样也可以解释为什么在动画移动过程中,我们为何不能对其进行任何操作。我们点击动画不能响应事件,因为移动的只是layer, view还是在原始的位置.
所以在我们完成layer动画之后,最好将我们的layer属性设置为我们最终状态的属性,然后将presentation layer 移除掉。
CAPropertyAnimation是一个抽象类,不能直接用于实现CALayer动画操作,但是它的类定义中增加用于设置CALayer可被实现动画的属性keyPath,总结这些属性如下:
属性解读transform.rotation默认围绕z轴旋转,相当于transform.rotation.ztransform.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轴方向上平移;zPositionz轴位置opacity透明度backgroundColor背景颜色cornerRadius圆角大小borderWidth边框宽度bounds图层大小contents寄宿图内容contentsRect可视内容position图层位置,类似transform.translationshadowColor阴影颜色shadowOffset阴影偏移shadowOpacity阴影透明度shadowRadius阴影角度附:KeyPath官方参考链接
CABasicAnimation即基础动画,在指定可动画属性后,动画会按照预定的参数持续一定时间由初始值变换为终点值。其实,CABasicAnimation就相当于只有开始和结束两个帧的特殊关键帧动画(后续会详解);
下面的示例使用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";对独立图层(即非UIView的关联图层,类似上述例子中的colorLayer)做更新属性的显式动画,我们需要设置一个事务来禁用图层行为,否则动画会发生两次,一次是因为显式的CABasicAnimation,另一次是因为隐式动画,从而导致我们看到的动画异常。
CACAKeyfameAnimation是CAPropertyAnimation的另一个子类,它和和CABasicAnimation一样都只能作用于图层对象的单一属性;它们的区别在于:CACAKeyfameAnimation不限制于设置一个起始值和结束值,而是可以根据一连串的值来做动画。其实,CABasicAnimation可看做是只有2个关键帧的CAKeyframeAnimation。
关键帧动画相对于基础动画的具有一些独特的属性,我们现将其总结如下:
属性具体描述values用于提供关键帧数据的数组,数组中每一个值都对应一个关键帧属性值; 数组中的数据类型根据动画类型(KeyPath)而不同; 当使用path的时候,values的值将会被自动忽略;path用于提供关键帧数据的路径; path与values属性作用相同,但是两者互斥,同时指定values和path,path会覆盖values的效果;keyTimesktyTimes与Values中的值具有一一对应的关系,用于指定关键帧在动画的时间点,取值范围是[0,1]; 若没有设置keyTimes,则每个关键帧的时间是平分动画总时长(duration);timingFunctions用于指定每个关键帧之间的动画缓冲效果,这类似于物体运动的加速度; 注意:存在几个子路径就应该在此数组中传入几个元素;calculationMode该属性决定了物体在每个子路径下是跳着走还是匀速走,跟timeFunctions属性有点类似;rotationMode设置帧动画是否需要按照路径切线的方向运动;从关键帧动画的属性可以看出,我们可以总结出关键帧动画的实现方式实际分为两种: 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
现在,我们测试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顾名思义,就是可以将不同的动画效果组合起来,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来实现,它同样是CAAnimation的子类;它并不像属性动画那样在平滑的两个值之间做动画,而是影响到整个图层的变化。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。
过渡动画通常用于删除子控件、添加子控件、切换两个子控件等。
过渡动画有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从下向上现在设想这样的一个需求:修改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.
CATransition可以对图层任何变化平滑过渡,这使得它成为那些不好做动画的属性图层行为的理想之选。所以,苹果将CATransition作为设置CALayer的contents属性时的默认行为,对图层contents图片做的改动都会自动附上淡入淡出的效果,这也就解释了隐式动画的原理; 但注意: 1.对于视图关联的图层,过渡动画的默认效果是禁用的; 2.我们不能错误的理解CATransition只可以改变非动画属性,其实它也可以对类似backgroundColor的属性做过渡效果动画;
过渡动画的过程就是对原始图层外观截图,然后添加一段动画,平滑过渡到图层改变之后的那个截图效果。如果我们知道如何对图层截图,我们就可以使用属性动画来自定义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可用于处理动画变换的虚拟属性;
为了理解虚拟属性的用处,我们现在考虑这样一个动画:对一个物体实现旋转动画,由于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做动画的好处如下:
可以不通过关键帧,只一个步骤就实现旋转多于180度的动画;可以使用相对值而不是绝对值旋转,设置byValue而不是toValue;可以不用创建CATransform3D,而是使用一个简单的数值来指定角度;不会和transform.position或者transfrom.scale冲突(同样是使用关键路径来做独立的动画属性);我们已经说过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;
