目标:1. 学会使用图层精简非交互式绘图;2. 通过核心动画创建基础动画,关键帧动画,动画组,转场动画;3. 学习使用UIView对这些动画简化操作的装饰方法.
CALyer包含在QuartzCore框架中,这是一个跨平台的框架(iOS 和 MAC OSX);在使用Core Animation开发动画的本质就是将CALayer中的内容转化为位图从而供硬件操作;
在Core Animation中我们更多是直接操作图层;UIView中有一个layer属性作为根图层,根图层上可以放其他子图层,UIView中所有能看到的内容都包含在layer中;
iOS中CALayer的设计主要是为了内容展示和动画操作,它本身并不包含在UIKit中,所以不能响应事件.CALyer设计之初就考虑它的动画操作功能,因此它的很多属性在修改时都能形成动画效果,这种属性称为“隐式动画属性”,很多开发的时候做平移动画直接使用隐式动画即可;
但是,对于UIView的根图层而言,属性修改不会形成动画效果,因为很多情况下根图层是充当容器的作用,如果的它的属性变动形成的动画效果会直接影响子图层.而且根视图的创建工作完全由系统完成,无法重新创建,只能往根视图中添加或移除子图层.层级结构如下:
列表CALyer的常用属性如下:
扩展:若有特殊需求,要关闭隐式动画-需要用到动画事务CATransaction,在事务内将隐式动画关闭
//开启事务
[CATransaction begin];
//禁用隐式动画
[CATransaction setDisableActions:YES];
在这部分更改图层属性.将不会产生隐式动画
//提交事务
[CATransaction commit];
上一章中使用的Quartz 2D绘图其实已经用到了CALayer,当利用drawRect:方法绘图的本质就是绘制到图层中,只是drawRect: 是由UIKit组件进行调用,所以里面可以使用一些UIKit封装的方法进行绘图;
而直接绘制到图层的方法由于并非UIKit直接调用,因此只能用原生的Core Graphics方法绘制.
图层绘图有两种方法,不管哪种绘制完成都必须调用图层的setNeedDisplay方法 (注意:图层的方法,不是上一章UIView的方法.)
步骤:
代码:
#define PHOTO_HEIGHT 50
//自定义图层
CALayer *Layer = [[CALayer alloc] init];
Layer.bounds = CGRectMake (0,0,PHOTO_HEIGHT,PHOTO_HEIGHT);
layer.position = CGPointMake (160,200);
layer.backgroundColor = [UIColor redColor].CGColor;
layer,cornerRadius = 50/2;
//仅仅设置圆角,对于图形而言可以正常显示,但是对于图层中绘制的图片无法正常显示,需要:
layer.masksToBounds = YES; 来裁剪超出图层部分 才能显示出圆角;
//但是同理:阴影效果无法和masksToBounds 同时使用.阴影也会被剪掉;
//设置边框
layer.borderColor = [UIColor whiteColor].CGColor;
layer.borderWidth = 2;
//添加到根图层
[self.view.layer addSublayer: layer ]
//1. 设置图层代理
layer.delegate = self;
//2. 调用
[layer setNeedsDisplay];
//3. 实现代理方法
- (void) -(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
// 传入的layer是上面定义的图层 图形上下文也是此图层的上下文.
//保存绘图状态
CGContestSaveGState(cox);
//图形上下文形变,解决图片倒立 问题 //UIKit 坐标系与 Core Graphics 坐标系不同;
CGContextScaleCTM (ctx, 1, -1);
CGContextTranslateCTM(cox, 0, -PHOTO_HEIGHT);
UIImage *image = [UIImage imageName:@"photo.png"];
//绘图 注意:这个位置时先对于图层 不是屏幕;
CGContextDrawImage(ctx, CGRectMake(0, 0, PHOTO_HEIGHT, PHOTO_HEIGHT), image.CGImage);
CGContextRestoreGState(ctx);
}
扩展1.带阴影效果的图片裁剪:
由于上面的矛盾,两个设置不能同时显示;换个思路:使用两个图层,下面的用来绘制阴影作为容器图层,上面的用来显示图片,裁剪上面的即可;
扩展2.图层的形变:
//利用图层形变解决图像倒立问题
layer.transform = CATransform3DMakeRotation(M_PI, 1, 0, 0);
例:[layer setValue:@M_PI forKeyPath:@"transform.rotation.x"];
具体属性可查询文档“CATransform3D Key Paths”一节.步骤:
如下:
//1. 创建继承CALayer的KALayer类;
//2. 在KALayer中重写drawInContext:(CGContextRef)ctx 进行具体绘图
-(void)drawInContext:(CGContextRef)ctx{
//打印一下上下文地址
NSLog(@"CGContext:%@",ctx);
//// 开始画图
CGContextMoveToPoint(ctx, 94.5, 33.5);
CGContextAddLineToPoint(ctx,104.02, 47.39);
CGContextClosePath(ctx);
CGContextSetRGBFillColor(ctx, 135.0/255.0, 232.0/255.0, 84.0/255.0, 1);
CGContextSetRGBStrokeColor(ctx, 135.0/255.0, 232.0/255.0, 84.0/255.0, 1);
CGContextDrawPath(ctx, kCGPathFillStroke);
}
//3. 在view文件中.
-(instancetype)initWithFrame:(CGRect)frame{
NSLog(@"initWithFrame:");
if (self=[super initWithFrame:frame]) {
KCLayer *layer=[[KCLayer alloc]init];
//确定layer位置大小即可
layer.bounds=CGRectMake(0, 0, 185, 185);
layer.position=CGPointMake(160,284);
layer.backgroundColor=[UIColor colorWithRed:0 green:146/255.0 blue:1.0 alpha:1.0].CGColor;
//显示图层
[layer setNeedsDisplay];
[self.layer addSublayer:layer];
}
return self;
}
//验证 :drawRect:方法绘图的本质就是绘制到图层中;
//这里能调用是应为UIView创建图层会自动设置图层代理为其自身
- (void)drawLayer:(CALayer *)layer inContext(CGContextRef)ctx{
[super drawLayer:layer inContext:ctx];
//打印上下文
NSLog(@"CGContext:%@",ctx);
}
- (void)drawRect:(CGRect) rect {
[super drawRect:rect]
// 我们在这里获取到的当前图形上下文正是drawLayer:中传递的
NSLog(@"CGContext:%@",UIGraphicsGetCurrentContext())
}
UIView在显示时其根图层会自动创建一个CGContextRef(CALayer本质使用的是位图上下文),同时调用图层代理(UIView创建图层会自动设置图层代理为其自身,引申的,不能给给自定义layer的代理设为view,应为view已经做自己根图层的代理了)的draw: inContext:方法并将图形上下文作为参数传递给这个方法。而在UIView的draw:inContext:方法中会调用其drawRect:方法,在drawRect:方法中使用UIGraphicsGetCurrentContext()方法得到的上下文正是前面创建的上下文。
在iOS中实现一个动画相当简单,只要调用UIView的块代码即可实现一个动画效果,使用上面UIView封装的方法进行动画设置固然十分方便,但是具体动画如何实现我们是不清楚的,而且上面的代码还有一些问题是无法解决的,例如:如何控制动画的暂停?如何进行动画的组合?。。。这就需要了解核心动画.
很多情况通过基础动画就能满足要求.如果不使用UIView的封装后方法,一般步骤:
[self.redView.layer setValue:@0.5 forKeyPath:@"transform.scale"];
示例代码如下:
//1.创建动画并指定动画属性
CABasicAnimation *basicAnimation=[CABasicAnimation animationWithKeyPath:@"position"];
//2.设置动画属性初始值和结束值
// basicAnimation.fromValue=[NSNumber numberWithInteger:50];//可以不设置,默认为图层初始状态
//toValue表示最终到哪个值 byValue 相对于上一次增加多少值
basicAnimation.toValue=[NSValue valueWithCGPoint:location];
//设置其他动画属性
basicAnimation.duration=5.0;//动画时间5秒
// basicAnimation.repeatCount=HUGE_VALF;//设置重复次数,HUGE_VALF可看做无穷大,起到循环动画的效果
// 设置不反弹-两者不可缺一
// basicAnimation.removedOnCompletion=NO;//运行完毕一次后不要移除动画
// basicAnimation.fillMode= KCAFillModeForwards //保存最新模式
//3.添加动画到图层,注意key相当于给动画进行命名,以后获得该动画时可以使用此名称获取
[_layer addAnimation:basicAnimation forKey:@"KCBasicAnimation_Translation"];
存在问题:动画结束后动画图层回到了原来的位置,当然使用UIView封装的方法是没这个问题的;问题的原因:图层动画的本质就是将图层内部的内容转化为位图经硬件操作形成一种动画效果,其实图层本身并没有任何的变化,动画效果是假象;
核心动画的运行有一个媒体时间的概念:假设将一个旋转动画设置旋转一周要用时60秒,那么当都动画旋转90度后媒体时间就是15秒. 此时如果要将动画暂停,只需要让媒体时间偏离量设置为15秒,然后动画运行速度设置为0时期停止运动;
类似的,如果暂停了50秒后需要恢复动画(此时媒体时间为65秒); 这时只要将动画开始时间设置为当前媒体时间65秒减去暂停时的时刻(之前的设置的偏移量)即(75-15 =50),即暂停的时时长,与此同时将偏移量重置为0,运行速度设为1;
这个过程中真正起到暂停作用的是动画速度;媒体事件偏移量以及恢复时的开始事件设置主要是为了让动画更加连贯,不会闪跳;
#pragma mark 动画暂停
-(void)animationPause{
//取得指定图层动画的媒体时间,后面参数用于指定子图层,这里不需要
CFTimeInterval interval=[_layer convertTime:CACurrentMediaTime() fromLayer:nil];
//设置时间偏移量,保证暂停时停留在旋转的位置
[_layer setTimeOffset:interval];
//速度设置为0,暂停动画
_layer.speed=0;
}
#pragma mark 动画恢复
-(void)animationResume{
//获得暂停的时间
CFTimeInterval beginTime= CACurrentMediaTime()- _layer.timeOffset;
//设置偏移量
_layer.timeOffset=0;
//设置开始时间
_layer.beginTime=beginTime;
//设置动画速度,开始运动
_layer.speed=1.0;
}
注意:动画暂停针对的是图层而不是图层中的某个动画。;
使用类似于基础动画,但可以在无数个值(称为关键帧)间改变.有个属性values数组,用来接受这些值;
关键帧动画开发分为两种形式:一种是通过设置不同的属性值进行关键帧控制,另一种是通过绘制路径进行关键帧控制。后者优先级高于前者,如果设置了路径则属性值就不再起作用。
属性关键帧
//1.创建关键帧动画并设置动画属性
CAKeyframeAnimation *keyframeAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
//2.设置关键帧,这里有四个关键帧
NSValue *key1=[NSValue valueWithCGPoint:_layer.position];//对于关键帧动画初始值不能省略
NSValue *key2=[NSValue valueWithCGPoint:CGPointMake(80, 220)];
NSValue *key3=[NSValue valueWithCGPoint:CGPointMake(45, 300)];
NSValue *key4=[NSValue valueWithCGPoint:CGPointMake(55, 400)];
NSArray *values=@[key1,key2,key3,key4];
keyframeAnimation.values=values;
//设置其他属性
keyframeAnimation.duration=8.0;
keyframeAnimation.beginTime=CACurrentMediaTime()+2;//设置延迟2秒执行
//3.添加动画到图层,添加动画后就会执行动画
[_layer addAnimation:keyframeAnimation forKey:@"KCKeyframeAnimation_Position"];
路径关键帧
//1.创建关键帧动画并设置动画属性
CAKeyframeAnimation *keyframeAnimation=[CAKeyframeAnimation animationWithKeyPath:@"position"];
//2.设置路径
//绘制贝塞尔曲线
CGPathRef path=CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, _layer.position.x, _layer.position.y);//移动到起始点
CGPathAddCurveToPoint(path, NULL, 160, 280, -30, 300, 55, 400);//绘制二次贝塞尔曲线
keyframeAnimation.path=path;//设置path属性
CGPathRelease(path);//释放路径对象
//设置其他属性
keyframeAnimation.duration=8.0;
keyframeAnimation.beginTime=CACurrentMediaTime()+2;//设置延迟2秒执行
//3.添加动画到图层,添加动画后就会执行动画
[_layer addAnimation:keyframeAnimation forKey:@"KCKeyframeAnimation_Position"];
不同动画效果如下:
实际开发中一个物体的运动往往是复合运动,单一属性的运动情况比较少,但恰恰属性动画每次进行动画设置时一次只能设置一个属性进行动画控制,这样一来要做一个复合运动的动画就必须创建多个属性动画进行组合。动画组的产生就是基于这样一种情况而产生的.
动画组是一系列动画的组合,凡是添加到动画组中的动画都受控于动画组,这样一来各类动画公共的行为就可以统一进行控制而不必单独设置,而且放到动画组中的各个动画可以并发执行,共同构建出复杂的动画效果。
一般步骤:首先单独创建单个动画(可以是基础动画也可以是关键帧动画),之后添加到动画组上,再把动画组添加到图层,就可以统一,同时执行这些动画.
//1.创建动画组
CAAnimationGroup *animationGroup=[CAAnimationGroup animation];
//2.设置组中的动画和其他属性
CABasicAnimation *basicAnimation=[self rotationAnimation]; //是封装上面的基础动画代码
CAKeyframeAnimation *keyframeAnimation=[self translationAnimation];//封装上面帧动画代码
animationGroup.animations=@[basicAnimation,keyframeAnimation];
animationGroup.duration=10.0;//设置动画时间,如果动画组中动画已经设置过动画属性则不再生效
animationGroup.beginTime=CACurrentMediaTime()+5;//延迟五秒执行
//3.给图层添加动画
[_layer addAnimation:animationGroup forKey:nil];
苹果封装的一个场景以动画形式转到另一个场景.
步骤:
下表列出了常用的转场类型(注意私有API是苹果官方没有公开的动画类型,但是目前通过字符串仍然可以使用):
动画类型 | 说明 | 对应常量 | 是否支持方向设置 |
---|---|---|---|
公开API | |||
fade | 淡出效果 | kCATransitionFade | 是 |
movein | 新视图移动到旧视图上 | kCATransitionMoveIn | 是 |
push | 新视图推出旧视图 | kCATransitionPush | 是 |
reveal | 移开旧视图显示新视图 | kCATransitionReveal | 是 |
私有API | 私有API只能通过字符串访问 | ||
cube | 立方体翻转效果 | 无 | 是 |
oglFlip | 翻转效果 | 无 | 是 |
suckEffect | 收缩效果 无 | 否 | |
rippleEffect | 水滴波纹效果 | 无 | 否 |
pageCurl | 向上翻页效果 | 无 | 是 |
pageUnCurl | 向下翻页效果 | 无 | 是 |
cameralIrisHollowOpen | 摄像头打开效果 | 无 | 否 |
cameraIrisHollowClose | 摄像头关闭效果 | 无 | 否 |
另外对于支持方向设置的动画类型还包含子类型:
动画子类型 | 说明 |
---|---|
kCATransitionFromRight | 从右侧转场 |
kCATransitionFromLeft | 从左侧转场 |
kCATransitionFromTop | 从顶部转场 |
kCATransitionFromBottom | 从底部转场 |
//1.创建转场动画对象
CATransition *transition=[[CATransition alloc]init];
//2.设置动画类型,注意对于苹果官方没公开的动画类型只能使用字符串,并没有对应的常量定义
transition.type=@"cube";
//设置子类型
if (isNext) {
transition.subtype=kCATransitionFromRight;
}else{
transition.subtype=kCATransitionFromLeft;
}
//设置动画时常
transition.duration=1.0f;
//3.设置转场后的新视图 添加转场动画
_imageView.image=[self getImage:isNext];
[_imageView.layer addAnimation:transition forKey:@"KCTransitionAnimation"];
通过设置UIImageView的animationImages属性,然后调用它的startAnimating方法去播放这组图片。(存在着很大的性能问题,并且这种方法一旦设置完图片中间的过程就无法控制了。TOM猫例)
但是对于一些事物的运动又不得不选择使用逐帧动画,例如人得运动,这是一个高度复杂的运动,基本动画、关键帧动画是不可能解决的。所大家一定要注意在循环方法中尽可能的降低算法复杂度,同时保证循环过程中内存峰值尽可能低(延迟释放)。
可以和其他动画组合使用.
UIView本身对于基本动画和关键帧动画、转场动画都有相应的封装,在对动画细节没有特殊要求的情况下使用起来也要简单的多。可以说在日常开发中90%以上的情况使用UIView的动画封装方法都可以搞定.
原理讲过直接上封装的方法:
//方法1:block方式
/*开始动画,UIView的动画方法执行完后动画会停留在重点位置,而不需要进行任何特殊处理
duration:执行时间
delay:延迟时间
options:动画设置,例如自动恢复、匀速运动等
completion:动画完成回调方法
*/
// [UIView animateWithDuration:5.0 delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
// _imageView.center=location;
// } completion:^(BOOL finished) {
// NSLog(@"Animation end.");
// }];
//方法2:静态方法
//开始动画
[UIView beginAnimations:@"KCBasicAnimation" context:nil];
[UIView setAnimationDuration:5.0];
//[UIView setAnimationDelay:1.0];//设置延迟
//[UIView setAnimationRepeatAutoreverses:NO];//是否回复
//[UIView setAnimationRepeatCount:10];//重复次数
//[UIView setAnimationStartDate:(NSDate *)];//设置动画开始运行的时间
//[UIView setAnimationDelegate:self];//设置代理
//[UIView setAnimationWillStartSelector:(SEL)];//设置动画开始运动的执行方法
//[UIView setAnimationDidStopSelector:(SEL)];//设置动画运行结束后的执行方法
_imageView.center=location;
//提交动画
[UIView commitAnimations];
由于在iOS开发中弹性动画使用很普遍,所以在iOS7苹果官方直接提供了一个方法用于弹性动画开发.
/*创建弹性动画 - block
damping:阻尼,范围0-1,阻尼越接近于0,弹性效果越明显
velocity:弹性复位的速度
*/
[UIView animateWithDuration:5.0 delay:0 usingSpringWithDamping:0.1 initialSpringVelocity:1.0 options:UIViewAnimationOptionCurveLinear animations:^{
_imageView.center = location; //CGPointMake(160, 284);
} completion:nil];
在动画方法中有一个option参数,UIViewAnimationOptions类型,它是一个枚举类型,动画参数分为三类,可以组合使用:
UIViewAnimationOptionLayoutSubviews:动画过程中保证子视图跟随运动。
UIViewAnimationOptionAllowUserInteraction:动画过程中允许用户交互。
UIViewAnimationOptionBeginFromCurrentState:所有视图从当前状态开始运行。
UIViewAnimationOptionRepeat:重复运行动画。
UIViewAnimationOptionAutoreverse :动画运行到结束点后仍然以动画方式回到初始点。
UIViewAnimationOptionOverrideInheritedDuration:忽略嵌套动画时间设置。
UIViewAnimationOptionOverrideInheritedCurve:忽略嵌套动画速度设置。关键帧动画没有
UIViewAnimationOptionAllowAnimatedContent:动画过程中重绘视图(注意仅仅适用于转场动画)。
UIViewAnimationOptionShowHideTransitionViews:视图切换时直接隐藏旧视图、显示新视图,而不是将旧视图从父视图移除(仅仅适用于转场动画)
UIViewAnimationOptionOverrideInheritedOptions :不继承父动画设置或动画类型。
UIViewAnimationOptionCurveEaseInOut:动画先缓慢,然后逐渐加速。
UIViewAnimationOptionCurveEaseIn :动画逐渐变慢。
UIViewAnimationOptionCurveEaseOut:动画逐渐加速。
UIViewAnimationOptionCurveLinear :动画匀速执行,默认值。
UIViewAnimationOptionTransitionNone:没有转场动画效果。
UIViewAnimationOptionTransitionFlipFromLeft :从左侧翻转效果。
UIViewAnimationOptionTransitionFlipFromRight:从右侧翻转效果。
UIViewAnimationOptionTransitionCurlUp:向后翻页的动画过渡效果。
UIViewAnimationOptionTransitionCurlDown :向前翻页的动画过渡效果。
UIViewAnimationOptionTransitionCrossDissolve:旧视图溶解消失显示下一个新视图的效果。
UIViewAnimationOptionTransitionFlipFromTop :从上方翻转效果。
UIViewAnimationOptionTransitionFlipFromBottom:从底部翻转效果。
从iOS7开始UIView动画中封装了关键帧动画
/*关键帧动画
options:
*/
[UIView animateKeyframesWithDuration:5.0 delay:0 options: UIViewAnimationOptionCurveLinear| UIViewAnimationOptionCurveLinear animations:^{
//第二个关键帧(准确的说第一个关键帧是开始位置):从0秒开始持续50%的时间,也就是5.0*0.5=2.5秒
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
_imageView.center=CGPointMake(80.0, 220.0);
}];
//第三个关键帧,从0.5*5.0秒开始,持续5.0*0.25=1.25秒
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.25 animations:^{
_imageView.center=CGPointMake(45.0, 300.0);
}];
//第四个关键帧:从0.75*5.0秒开始,持所需5.0*0.25=1.25秒
[UIView addKeyframeWithRelativeStartTime:0.75 relativeDuration:0.25 animations:^{
_imageView.center=CGPointMake(55.0, 400.0);
}];
} completion:^(BOOL finished) {
NSLog(@"Animation end.");
}];
关键帧的options,绝大多数同上,另有动画模式设置.
动画模式设置(同前面关键帧动画动画模式一一对应,可以从其中选择一个进行设置)
UIViewKeyframeAnimationOptionCalculationModeLinear:连续运算模式。
UIViewKeyframeAnimationOptionCalculationModeDiscrete :离散运算模式。
UIViewKeyframeAnimationOptionCalculationModePaced:均匀执行运算模式。
UIViewKeyframeAnimationOptionCalculationModeCubic:平滑运算模式。
UIViewKeyframeAnimationOptionCalculationModeCubicPaced:平滑均匀运算模式。
注意:前面说过关键帧动画有两种形式,上面演示的是属性值关键帧动画,路径关键帧动画目前UIView还不支持。
从iOS4.0开始,UIView直接封装了转场动画.
[UIView transitionWithView:_imageView duration:1.0 options:option animations:^{
_imageView.image=[self getImage:isNext];
} completion:nil];
#pragma mark 取得当前图片
-(UIImage *)getImage:(BOOL)isNext{
if (isNext) {
_currentIndex=(_currentIndex+1)%IMAGE_COUNT;
}else{
_currentIndex=(_currentIndex-1+IMAGE_COUNT)%IMAGE_COUNT;
}
NSString *imageName=[NSString stringWithFormat:@"%i.jpg",_currentIndex];
return [UIImage imageNamed:imageName];
}
上面的转场动画演示中,其实仅仅有一个视图UIImageView做转场动画,每次转场通过切换UIImageView的内容而已。如果有两个完全不同的视图,并且每个视图布局都很复杂,此时要在这两个视图之间进行转场可以使用+ (void)transitionFromView:(UIView )fromView toView:(UIView )toView duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options completion:(void (^)(BOOL finished))completion NS_AVAILABLE_IOS(4_0)方法进行两个视图间的转场,需要注意的是默认情况下转出的视图会从父视图移除,转入后重新添加,可以通过 UIViewAnimationOptionShowHideTransitionViews参数设置,设置此参数后转出的视图会隐藏(不会移除)转入后再显示。
注意:转场动画设置参数完全同基本动画参数设置;同直接使用转场动画不同的是使用UIView的装饰方法进行转场动画其动画效果较少,因为这里无法直接使用私有API。
参考博客