iOS核心动画
核心动画框架
CoreAnimation框架是基于OpenGL与CoreGraphics图像处理框架的一个跨平台的动画框架。
在CoreAnimation中大部分的动画都是通过Layer层来实现的,通过CALayer,我们可以组织复杂的层级结构。
在CoreAnimation中,大多数的动画效果是添加在图层属性的变化上,例如,改变图层的位置,大小,颜色,圆角半径等。Layer层并不决定视图的展现,它只是存储了视图的几何属性状态。
核心动画类
核心动画中的虚类不能,而应该使用它子类中的实类。
下面介绍几个实类的简单用法
CABasicAnimation——基本动画
基本动画,是CAPropertyAnimation的子类。
🌰一个简单的动画效果
CABasicAnimation*moveAnimation = [CABasicAnimationanimationWithKeyPath:@"position.y"];moveAnimation.duration =0.8;//动画时间//动画起始值和终止值的设置moveAnimation.fromValue = @(self.imageView.center.x);moveAnimation.toValue = @(self.imageView.center.x-30);//一个时间函数,表示它以怎么样的时间运行moveAnimation.timingFunction = [CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn];moveAnimation.repeatCount = HUGE_VALF;moveAnimation.repeatDuration =2;moveAnimation.removedOnCompletion =NO;moveAnimation.fillMode = kCAFillModeForwards;//添加动画,后面有可以拿到这个动画的标识[self.imageView.layer addAnimation:moveAnimationforKey:@"可以拿到这个动画的Key值"];
相关属性
keyPath:要改变的属性名称(传字符串)
fromValue:keyPath相应属性的初始值
toValue:keyPath相应属性的结束值
timingFunction:动画随时间运行的关系
动画相关属性
动画过程说明
随着动画的进行,在长度为duration的持续时间内,keyPath相应属性的值fromValue渐渐地变为toValue
keyPath内容是CALayer的可动画Animation属性
如果fillMode=kCAFillModeForwards同时removedOnComletion=NO,那么在动画执行完毕后,图层会保持显示动画执行后的状态。但在实质上,图层的属性值还是动画执行前的初始值,并没有真正被改变。
在CAAnimation中可以实现代理方法
- animationDidStart: //开始时调用'
- animationDidStop:finished: //结束时调用
在addAnimation:forKey:方法中,也可以给这个动画设置一个键,可以在其他地方将其取出来,进行一些操作,比如删除。这也充分体现了kvc的灵活。
用到CALayer的 removeAnimationForKey:方法。
CAKeyframeAnimation——关键帧动画
关键帧动画,也是CAPropertyAnimation的子类。
如果是简单的动画CABasicAnimation就能完成,CAKeyframeAnimation(关键帧动画)弥补了基本动画只能传入一对对应值的不足,关键帧动画支持传入一套数值或一个路径来完成动画。
⚠️与CABasicAnimation的区别是:
CABasicAnimation:只能从一个数值(fromValue)变到另一个数值(toValue)
CAKeyframeAnimation:会使用一个NSArray保存这些数值
🌰一个关键帧动画代码
CAKeyframeAnimation *animaiton = [CAKeyframeAnimationanimationWithKeyPath:@"transform.rotation"]; NSArray *rotationVelues = @[@(M_PI_4), @(-M_PI_4), @(M_PI_4)]; animaiton.values = rotationVelues; animation.rotationMode = kCAAnimationRotateAuto;//方向animaiton.duration =3.0f; animation.keyTimes = @[@0.2,@0.8,@1];animation.path = bezierPath.CGPath;animaiton.repeatCount = HUGE_VALF;//#define HUGE_VALF 1e50f [self.imageView.layeraddAnimation:animaitonforKey:nil];
属性说明
values:上述的NSArray对象。里面的元素称为“关键帧”(keyframe)。动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧
path:可以设置一个CGPathRef、CGMutablePathRef,让图层按照路径轨迹移动。path只对CALayer的anchorPoint和position起作用。如果设置了path,那么values将被忽略
keyTimes:可以为对应的关键帧指定对应的时间点,其取值范围为0到1.0,keyTimes中的每一个时间值都对应values中的每一帧。如果没有设置keyTimes,各个关键帧的时间是平分的
bezierPath:贝赛尔曲线路径,为动画提供一个动画移动的路线。
UIBezierPath - 贝赛尔曲线
//创建路径UIBezierPath*bezierPath = [[UIBezierPathalloc] init];[bezierPath moveToPoint:CGPointMake(0,450)];[bezierPath addCurveToPoint:CGPointMake(370,500) controlPoint1:CGPointMake(350,200) controlPoint2:CGPointMake(300,600)];//一个曲线 //路径样式CAShapeLayer*shapeLayer = [CAShapeLayerlayer];shapeLayer.path = bezierPath.CGPath;shapeLayer.fillColor = [UIColorclearColor].CGColor;//填充色<默认黑色>shapeLayer.strokeColor = [UIColorblueColor].CGColor;//线色shapeLayer.lineWidth =2;[self.view.layer addSublayer:shapeLayer];
UIBezierPath创建一个路径,画出一条曲线。栗子中 起点(0,450)、终点(370,500) 和 (350,200)、(370,500)来个点来确定线的路径。模拟器更多参考
CAShapeLayer对上面的线进行属性上的设置。最后添加到一个layer上。
++CABasicAnimation可看做是只有2个关键帧的CAKeyframeAnimation++
CAAnimationGroup——动画组
动画组,是CAAnimation的子类,可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行
属性说明
animations:用来保存一组动画对象的NSArray
CAAnimationGroup*animationGroup = [CAAnimationGroupanimation];animationGroup.animations = @[animation,basicAnimation];animationGroup.duration =4;animation.repeatCount =9;[_imageLayer addAnimation:animationGroup forKey:@"changeColor"];
++默认情况下,一组动画对象是同时运行的,也可以通过设置动画对象的beginTime属性来更改动画的开始时间++
转场动画——CATransition
CATransition是CAAnimation的子类,用于做转场动画,能够为层提供移出屏幕和移入屏幕的动画效果。iOS比Mac OS X的转场动画效果少一点。
++UINavigationController就是通过CATransition实现了将控制器的视图推入屏幕的动画效果++
CATransition*caTransition = [CATransitionanimation];caTransition.duration =0.5;caTransition.delegate =self;caTransition.timingFunction = [CAMediaTimingFunctionfunctionWithName:@"easeInEaseOut"];//切换时间函数caTransition.type = kCATransitionReveal;//动画切换风格caTransition.subtype = kCATransitionFromLeft;//动画切换方向// 子视图交换位置//[self.parentView exchangeSubviewAtIndex:0 withSubviewAtIndex:1];//动画在父视图[self.parentView.layer addAnimation:caTransition forKey:@"Key"];
动画属性
type:动画过渡类型
subtype:动画过渡方向
startProgress:动画起点(在整体动画的百分比)
endProgress:动画终点(在整体动画的百分比)
// 导航栏切换UIViewController*viewCtr = [[UIViewControlleralloc] init];viewCtr.view.backgroundColor = [UIColorredColor];[self.navigationController pushViewController:viewCtr animated:NO];// 动画设置 NO 效果比较好[self.navigationController.view.layer addAnimation:caTransition forKey:@"animation"];
CALayer
它有一些方法和属性来做动画和变换,和UIView最大的不同是CALayer不处理用户的交互事件。
UIView和CALayer的关系 - 平行关系
每个UIView都有一个CALayer实例的图层属性。
视图的职责就是创建并管理这个图层。
UIView和CALyer有着平行的层级关系,职责分离。
图层能力
UIView的高级API可以实现一些简单的动画效果,但更多的细节和底层还需要通过CALayer直接处理CAAnimation来处理。
UIView能实现但CALayer不能实现
1.处理触摸事件
UIView没有暴露出来的CALayer的功能
1.阴影、圆角、带颜色的边框
2.3D变换
3.非矩形范围
4.透明覆盖
5.多级非线性动画
解释:为什么动画结束后返回原状态?
首先我们需要搞明白一点的是,layer动画运行的过程是怎样的?其实在我们给一个视图添加layer动画时,真正移动并不是我们的视图本身,而是 presentation layer 的一个缓存。动画开始时 presentation layer开始移动,原始layer隐藏,动画结束时,presentation layer从屏幕上移除,原始layer显示。这就解释了为什么我们的视图在动画结束后又回到了原来的状态,因为它根本就没动过。
这个同样也可以解释为什么在动画移动过程中,我们为何不能对其进行任何操作。
所以在我们完成layer动画之后,最好将我们的layer属性设置为我们最终状态的属性,然后将presentation layer 移除掉。
UIView中目前最常用的动画方法应该就是这个方法了
+(void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ nullable)(BOOL finished))completion ;
稍微复杂点的方法还是使用CALayer调用CAAnimation的API更为方便。
补充
- drawRect:
UIView的方法 没有默认实现
如果UIView检测到- drawRect:方法被调用,它会为视图分配一个寄宿图,这个寄宿图的尺寸等于视图大小乘以 contentsScale的值
- 会造成CPU资源和内存的浪费
- setNeedsDisplay
- drawRect:方法中利用CoreGraphics 去绘制一个寄宿图,然后被缓存起来直到它需要被更新,更新通常使用- setNeedsDisplay这个方法
CALayerDelegate
可以利用这个代理方法实现一些绘图,不必实现-displayLayer:和-drawLayer:inContext:来绘制寄宿图。通常方法是实现 UIView的- drawRect:方法,UIView就会帮你做完剩下的工作,包括在需要重绘的时候调用- display方法
CALayer的子类
核心动画相关类的继承和简介
CAShapeLayer
CAShapeLayer是一个通过矢量图形而不是bitmap(位图)来挥之的图层子类。
你指定诸如颜色和线宽等属性,用CAPath来定义想绘制的图形,最后CAShapeLayer就自动渲染出来了。
优点
1.渲染快速。CAShapeLayer使用了硬件加速,绘制同一个图形会比用Core Graphics快很多。
2.高效使用内存。一个CAShapeLayer不需要像CALayer一样创建一个寄宿图,所以无论有多大,都不会占用大多的内存。
3.不会被图层边界裁剪掉。
4.不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图普通图层一样变得像素化。
创建一个CGPath
CAShapeLayer可以用来绘制所有能够通过CGPath来表示的形状。这个形状不一定要闭合,图层路径也不一定要不可破的,事实上你可以在一个图层上绘制好几个不同的形状。
你可以控制一些属性比如
lineWith(线宽,用点表示单位)、lineCap(线条结尾的样子)、和lineJoin(线条之间的结合点的样子)。
CAShapeLayer属性时CGPathRef类型,当时我们用 UIBezierPath 帮助类创建了图层路径,这样我们就不用考虑释放CGPath了。
🌰 一个火柴人
- (void)viewDidLoad { [superviewDidLoad];//create pathUIBezierPath*path = [[UIBezierPathalloc] init]; [path moveToPoint:CGPointMake(175,100)]; [path addArcWithCenter:CGPointMake(150,100) radius:25startAngle:0endAngle:2*M_PI clockwise:YES]; [path moveToPoint:CGPointMake(150,125)]; [path addLineToPoint:CGPointMake(150,175)]; [path addLineToPoint:CGPointMake(125,225)]; [path moveToPoint:CGPointMake(150,175)]; [path addLineToPoint:CGPointMake(175,225)]; [path moveToPoint:CGPointMake(100,150)]; [path addLineToPoint:CGPointMake(200,150)];//create shape layerCAShapeLayer*shapeLayer = [CAShapeLayerlayer]; shapeLayer.strokeColor = [UIColorredColor].CGColor; shapeLayer.fillColor = [UIColorclearColor].CGColor; shapeLayer.lineWidth =5; shapeLayer.lineJoin = kCALineJoinRound; shapeLayer.lineCap = kCALineCapRound; shapeLayer.path = path.CGPath;//add it to our view[self.containerView.layer addSublayer:shapeLayer]; }
圆角
我们创建一个圆角矩形其实就是人工绘制单独的直线和弧度,但是事实上UIBezierPath有自动绘制圆角矩形的构造法。
🌰绘制一个有三个圆角一个直角的矩形
//define path parametersCGRect rect = CGRectMake(50, 50, 100, 100);CGSize radii = CGSizeMake(20, 20);UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft; //create pathUIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerR
我们可以通过这个图层路径绘制一个既有直角又有圆角的视图。
CATiledLayer
载入大图片可能会相当地慢,那些对你看上去比较方便的做法(在主线程调用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法) 将会阻塞你的界面,至少会引起动画卡顿现象。
CATiledLayer为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将它们单独的载入。
🌰iOS上小图片拼成大图
略。。。在iOS上要先有n多的小图片,然后通过CATiledLayer把图片放对应位置,实现显示大图片。 比如地图的实现。
核心方法-drawLayer:inContext:
使用CALayer加载图片千万注意使用
tileLayer.contentsScale = [UIScreen mainScreen].scale;
保证图片清晰
CAMediaTiming -- 图层时间
CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性集合,CALayer和CA Animation都实现了这个协议,所以时间可以背任何基于一个图层或者一段动画的类控制。
—这个协议中有很多关于时间的属性:开始时间,速度,次数。。。
->持续和重复
duration:一个动画的迭代时间 CFTimeInterval 类型<类似双精度浮点型>
repeatCount:动画重复的迭代次数
⚠️这两个值默认值都是0,分别代表0.25s和1次
->相对时间
beginTIme:指定了动画开始之前的延迟。
speed:是一个时间的倍数,默认1。
timeoffset:对于一个持续1s的动画来说,设置timeoffset为0.5意味着动画将从一半的地方开始。
🌰 <1s动画 speed = 2,time offset = 0.5 -> 动画变成:时长0.5s,从0.5s开始 >
fillmode
removeOnCompletion被设置为NO的动画将在动画结束的时候仍然保持之前的状态。
动画开始之前和动画结束之后,被设置的动画属性将会是什么值呢:
保持动画开始之前的那一帧,或者动画结束后的那一帧。
这就是所谓的填充 因为动画开始和结束的值来填充开始之前和结束之后的时间。
fillMode 是一个NSString类型
kCAFillModeForwards
kCAFillModeBackwards
kCAFillModeBoth
kCAFillModeRemoved 默认
⚠️需要把removeOnCompletion设置为NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。
CAMediaTimeingFunction 缓冲
+ timingFunctionWithName:
kCAMediaTimingFunctionLinear 快速开始然后匀速。
kCAMediaTimingFunctionEaseIn 慢慢加速后突然停止。
kCAMediaTimingFunctionEaseOut 全速开始后慢慢停止。
kCAMediaTimingFunctionEaseInEaseOut 慢慢开始然后慢慢减速。
kCAMediaTimingFunctionDefault
UIView的动画方法,kCAMediaTimingFunctionEaseInEaseOut是默认的,但当创建CAAnimation的时候,就需要手动设置了。
贝赛尔曲线
一个三次贝赛尔曲线由4个点来定义:
第一个和最后一个:代表曲线的起点和终点。
剩下的两个点:叫做控制点。<贝赛尔曲线的控制点其实是位于曲线之外的点,也就是说曲线并不一定要穿过他们。你可以把它们想象成吸引经过它们曲线的磁铁>
实际上它是一个很奇怪的函数,先加速-减速-加速。
CAMediaTimeingFunction
getControlPointAtIndex:values:可以用来检索曲线的点
使用它我们可以找到标准的缓冲的点,然后用UIBezierPath和CAShapeLayer把它画出来。
曲线的起点和终点始终是{0,0}和{1,1},于是我们只需要检索曲线的第二个和第三个点。
定时器动画
NSTimer
工作原理:iOS上的每一个线程都管理了一个NSRunloop,字面上看就是通过一个循环来完成一些任务列表。
但对主线程,这些任务包涵如下几项:
-处理触摸事件
-发送和接收网络数据包
-执行使用gcd的代码
-处理计时器行为
-屏幕绘制
当你设置一个NSTimer,它会被插入到当前任务列表,然后知道指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。
这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久完成就导致延迟很长一段时间。
屏幕重绘的频率是一秒六十次,但适合定时器行为一样,如果列表中上一个执行很长时间,它也会延迟。
这个延迟是一个随机值,于是就不能保证定时器精准的一秒执行六十次。
我们可以通过一些途径来优化
-我们可以用CADisplayLink 让更新频率严格控制在每次屏幕刷新之后。
-基于真实帧的持续时间而不失假设的更新频率来做动画。
-调整动画计时器的runloop模式,这样就不会被别的事情干扰。
隐式动画
Core Animation基于一个假设,说屏幕上的任何东西都是可以(或可能) 做动画。
动画并不需要你在Core Animation中手动打开,相反需要明确的关闭,否者它一只存在。
当你改变CALayer的一个可以做动画的属性,它并不能在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。
🌰 一个View上放一个CALayer,点击按钮改变layer的颜色<颜色是渐变过去的>
这就是隐式动画,之所以叫隐式动画是因为我们并没有指定任何的动画类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去动画。
CATransaction 管理事务的类
+begin 入栈
+commit 出栈
+setAnimationDuration: 设置当前动画的时间
+AnimationDuration 获取动画时间
Core Animation在每个RUNLOOP周期中自动开始一次新的事物。
RUNROOP:iOS中负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西。理解:涉及到任何主动或被动的操作改变都会唤醒run loop。
即使不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
UIView中的layer属性 并不存在 隐形动画
总结
1.UIView关联的图层禁用了隐形动画,对这种图层做动画的唯一方法就是使用IUView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖 -actionForLayer:forKey: 方法,或者直接创建一个显性动画。
2.对于独立的图层,我们可以通过实现图层的 -actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐形动画
显式动画
animationDidStop: finished:
代码demo
属性动画
基础动画
CABasicAnimation*animation = [CABasicAnimationanimation]; animation.keyPath =@"backgroundColor";animation.toValue = (__bridgeid)color.CGColor;animation.delegate =self;//apply animation to layer[self.colorLayer addAnimation:animation forKey:nil];
关键帧路径动画
CAKeyframeAnimation*animation = [CAKeyframeAnimationanimation];animation.keyPath =@"position";animation.duration =4.0;animation.path = bezierPath.CGPath;animation.rotationMode = kCAAnimationRotateAuto;[shipLayer addAnimation:animation forKey:nil];
动画组
CABasicAnimation和CAKeyframeAnimation仅仅是作用于单独的属性。
CAAnimationGroup可以把这些动画组合在一起
过渡
属性动画只对图层的可动画属性属性起作用
所以如果要改变一个不能动画的属性(比如图片),或者从层级关系中添加或者移除图层,属性动画将不起作用。
过渡:过渡并不像属性动画那样平滑的在两个值之间做动画,而是影响到整个图层的变化
。过渡动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。
CATransition:CAAnimation子类
CATransition有个type和subtype来识别变换效果,类型 NSString
typekCATransitionFade//默认 淡入淡出效果kCATransitionMoveIn//新图片滑动进入,直接覆盖旧的图片kCATransitionPush//边缘的一侧进来,把旧的图层从另一侧推出去kCATransitionReveal//旧图片滑出去,来显示新图片subtypekCATransitionFromRightkCATransitionFromLeftkCATransitionFromTopkCATransitionFromBottom
在想:控制器之间的过场动画是不是能用这个实现。
仿射变换
UIView可以通过transform属性做变换,transform是一个CGAffineTransform类型
CGAffineTransform 是用矩阵相乘的方法实现仿射变换
CGAffineTransformMakeRotation(CGFloat angle) 旋转
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy) 缩放
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty) 位移
CALayer 同样又个transform属性,transform的属性是CATransform3D
CALayer对应UIView的transform属性叫做affineTransform
混合变换
当操纵一个变换的时候,初始生成一个什么都不做的变换很重要-也就是创建一个CGAffineTransform类型的空值。<CGAffineTransformIdentity>
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle) CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy) CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
最后,如果需要混合两个已经存在的矩阵,就可以使用下面方法,在两个基础上新建一个变换
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
🌰❌
先缩小50%-再旋转30度-最后向右移动200像素
- (void)viewDidLoad{ [superviewDidLoad];//createa new transform CGAffineTransform transform = CGAffineTransformIdentity;//scaleby50%transform = CGAffineTransformScale(transform,0.5,0.5);//rotateby30degreestransform = CGAffineTransformRotate(transform, M_PI /180.0*30.0);//translateby200points transform = CGAffineTransformTranslate(transform,200,0);//applytransform to layerself.layerView.layer.affineTransform = transform; }
结果并不是预期的结果:这意味着变换的顺序会影响最终的结果,也就是说旋转之后的平移和平移之后的的旋转结果可能不同
CG - Core Graphics 框架 :严格意义上说是2D绘图API
CGAffineTransform 仅仅对2D变换有效
3D变换
CALayer的transform属性是CATransform3D类型,就能实现3D空间内移动或者旋转
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
想象x、y、z向旋转,缩放、位移的效果
🌰
绕Y轴做45度角的旋转
- (void)viewDidLoad{ [superviewDidLoad];//rotate the layer 45 degrees along the Y axisCATransform3Dtransform =CATransform3DMakeRotation(M_PI_4,0,1,0);self.layerView.layer.transform = transform;}
sublayerTransform:CALayer的属性,可以对所有子视图操作