基础概念
iOS图形架构
核心动画是 iOS 和 MacOS 上的图形渲染和动画基础结构,用于为应用的视图和其他视觉元素设置动画。
核心动画位于 APPKit 和 UIKit 之下,且集成于 Cocoa 和 Cocoa Touch 的视图工作流之中。
核心动画自己也具有一些接口,可以扩展视图所展现的功能,并可以更好的控制应用的动画。下图是iOS图形处理架构图:
UIView与CALayer的关系与区别
UIView:用户交互、界面展示。真正绘制的界面的类是Layer。CALayer属于QuartzCore框架(跨平台的)。mac os:交互上面:APPKIT,通过鼠标键盘交互,NSView。相同点:同一种绘制方式(CALayer)。不同点:不同交互方式。UIView封装了layer
frame,background:layer对应的属性。
总结:UIView负责处理用户交互,CALayer负责绘制内容,每个View都有一个Layer。我们访问和设置的这些负责显示的属性实际上就是访问和设置了Layer对应的属性,只不过UIView把它封装了起来。
iOS 为什么要基于 UIView 和 CAlayer 提供2个平行的层级关系?
1、 职责分离:UIView 处理 UI 交互,CALayer 处理内容绘制和动画;
2、 代码公用:在 iOS 和 macOS 2个平台上,事件和UI 交互有许多不同点,基于触控和鼠标键盘交互有本质的区别;故 针对不同的平台,UI 交互这些代码做不同的处理,而内容绘制和动画这些代码可以复用。
UIView和Layer图层属性概念
UIView/CALayer 3个重要的属性:
- frame:相对于父级的外部坐标
- bounds:内部坐标
- center/position:相当于初始anchorPoint(锚点概念在下面)在父级Layer中的位置。anchorPoint改变,center/position不会跟着改变。
UIView & CALayer 的坐标系
frame是一个虚拟属性,根据bounds,position,transform计算得来, 其中任一值发生变化,frame都会改变。
当 对Layer进行变换操作时,比如缩放或旋转,frame 代表的是覆盖在 Layer 旋转之后的整个轴对齐的矩形区域,既 frame 和 bounds 的宽高不再一致。
锚点:anchorPoint
可以认为是用来移动 Layer 的把柄。
默认居于 Layer 的中心点,可改变。示例如下:
实际应用,通过改变锚点,使 Layer 围绕其的一个端点旋转,如指针钟表。
显式动画/隐式动画
隐式动画
当改变CALayer的可动画属性时,它会从原先的值平滑过渡到新的值。默认时长为0.25s。即不指定任何动画类型,在改变CALayer的属性时,Core Animation来决定如何并且何时去做动画。UIView的Layer不能做隐式动画。
Core Animation是如何判断动画类型和持续时间的呢?
实际上动画执行的时间取决于当前事务的设置,动画类型取决于图层行为。
事务
Core Animation用来包含一些列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立即发生变化。而是当事务提交的时候开始使用一个动画过渡到新值。
事务使用CATransaction来进行管理。此类无实例,使用+begin和+commit来入栈和出栈。当改变图层属性值时,任何可动画的图层属性都会被加入到栈顶的事务,可以通过+setAnimationDUration:和+ animationDuration来设置/获取值(默认0.25s)。
Core Animation 在每个Runloop周期中自动开始一次新的事务,即使不显式的使用[CATransaction begin]开始一次事务,任何在一次Runloop循环中属性的改变都会被集中起来,然后做一次0.25s的动画。
图层行为
改变属性时,CALayer自动应用的动画称作行为(action),当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。实际步骤如下:
1、图层首先检测它是否有委托,并且事发后实现CALayerDelegate协议指定的-actionForLayer: forKey方法。如果有,直接调用并返回结果。
2、如果没有委托,或委托未实现-actionForLayer: forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
3、如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
4、最后,如果style中也找不到对应的行为,那么图层将会直接调用定义了每个属性标准行为的-defaultActionForKey:方法。
为什么UIView关联的CALayer隐式动画被禁用呢?
UIView对它关联的CALayer都扮演了一个委托的角色,并实现了 actionForLayer:forKey:方法。当不在一个动画块的实现中,其返回nil,此时将不会有动画发生;在动画块的范围之内,则返回一个非空值。如下:
//test layer action when outside of animation block
NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//begin animation block
[UIView beginAnimations:nil context:nil];
//test layer action when inside of animation block
NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//end animation block
[UIView commitAnimations];
//output:
$ LayerTest[21215:c07] Outside:
$ LayerTest[21215:c07] Inside:
另外禁用隐式动画的方法:
- setDisableActions: 用来对所有属性打开或关闭隐式动画。
[CATransaction setDisableActions:YES];
通过设置actions字典自定义动画行为
//create sublayer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
//add a custom action
CATransition *transition = [CATransition animation];
transition.type = kCATransitionPush;
transition.subtype = kCATransitionFromLeft;
self.colorLayer.actions = @{@"backgroundColor": transition};
//add it to our view
[self.layerView.layer addSublayer:self.colorLayer];
显式动画
对一些属性做指定的自定义动画,或者创建非线性动画,比如沿着任意一条曲线移动,这样的动画叫做显示动画。显示动画分类与继承关系如下:
属性动画 CAPropertyAnimation
首先我们来探讨一下属性动画。属性动画作用于图层的某个单一属性,并指定了它的一个目标值,或者一连串将要做动画的值。属性动画分为两种:基础动画和关键帧动画。
基础动画 CABasicAnimation
动画其实就是一段时间内发生的改变,最简单的形式就是从一个值改变到另一个值,这也是CABasicAnimation最主要的功能。
CAPropertyAnimation通过指定动画的keyPath作用于一个单一属性,CAAnimation通常应用于一个指定的CALayer,于是这里指的也就是一个图层的keyPath了。实际上它是一个关键路径(一些用点表示法可以在层级关系中指向任意嵌套的对象),而不仅仅是一个属性的名称,因为这意味着动画不仅可以作用于图层本身的属性,而且还包含了它的子成员的属性,甚至是一些虚拟的属性(后面会详细解释)。
CABasicAnimation继承于CAPropertyAnimation,并添加了如下属性:
id fromValue
id toValue
id byValue
从命名就可以得到很好的解释:fromValue代表了动画开始之前属性的值,toValue代表了动画结束之后的值,byValue代表了动画执行过程中改变的值。
fromValue,toValue和byValue属性可以用很多种方式来组合,但为了防止冲突,不能一次性同时指定这三个值。例如,如果指定了fromValue等于2,toValue等于4,byValue等于3,那么Core Animation就不知道结果到底是4(toValue)还是5(fromValue + byValue)了。他们的用法在CABasicAnimation头文件中已经描述的很清楚了,所以在这里就不重复了。总的说来,就是只需要指定toValue或者byValue,剩下的值都可以通过上下文自动计算出来。
下面我们通过CABasicAnimation来改变图层的位置:
CABasicAnimation *anim = [CABasicAnimation animation];
// CABasicAnimation属性
anim.toValue = @400;
// 父类CAPropertyAnimation属性
anim.keyPath = @"position.y";
// 基类CAAnimation
// 基类CAAnimation 结束后不移除动画
anim.removedOnCompletion = NO;
// 协议CAMediaTiming添加的属性 kCAFillModeForwards保持动画最后的状态
anim.fillMode = kCAFillModeForwards;
anim.delegate = self;
[_greenView.layer addAnimation:anim forKey:nil];
这里需要注意的是必须将动画的属性removedOnCompletion设置为NO,即不移除动画,fillMode设置为kCAFillModeForwards,即保持最后的状态,否则动画完后会回复到之前的状态。
我们给greenView添加手势,如下:
UITapGestureRecognizer *greenViewGes = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(greenViewGesClick:)];
[self.greenView addGestureRecognizer:greenViewGes];
- (void)greenViewGesClick:(UITapGestureRecognizer *)greenViewGes{
NSLog(@"触摸了greenView");
}
当动画完毕后我们去点击图层在界面上呈现的位置,会发现没有反应,反而在点击图层原来的位置上是,手势触摸有反应。
这是为什么呢?这就涉及到CALayer的呈现层和模型层的知识了。
呈现层和模型层(presentationLayer和modelLayer)
CALayer有两个图层,一个是modelLayer(存储和读取),代表layer实际的坐标值,一个是presentationLayer,代表了layer做动画时实际呈现的位置。上述动画中,_greenView.layer虽然做动画到了别的位置,但实际位置并未改变,所以我们在点击动画的位置没有反应,而点击原来的位置反而有反应。
如果想让你做动画的图层相应用户的输入,可以使用-hitTest:来判断指定的图层是否被触摸.通过判断[_greenView.layer.presentationLayer hitTest:point])是否有值来判断触摸点是否在presentationLayer上。
关键帧动画
和CABasicAnimation类似,CAKeyframeAnimation是另一种UIKit没有暴露出来但功能强大的类,CAKeyframeAnimation同样是CAPropertyAnimation的一个子类,它依然作用于单一的一个属性,和CABasicAnimation不一样的是,它不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。
关键帧起源于传动动画,意思是指主导的动画在显著改变发⽣时 重绘当前帧(也就是关键帧),每帧之间剩下的绘制(可以通过 关键帧推算出)将由熟练的艺术家来完成。
CAKeyframeAnimation也是同样的道理:你提供了显著的帧,然后Core Animation在每帧之间进行插入。
示例如下:
CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
anim.keyPath = @"transform.rotation";
anim.repeatDuration = MAXFLOAT;
// 和下面三句代码等价
// anim.values = @[@angleToRadians(-6),@angleToRadians(6),@angleToRadians(-6)];
anim.values = @[@angleToRadians(-6),@angleToRadians(6)];
// 动画反转
anim.autoreverses = YES;
// 加速2倍
anim.speed = 2;
[self.iconView.layer addAnimation:anim forKey:nil];
也可以通过CGPath指定动画路径。
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(50, 200)];
[path addCurveToPoint:CGPointMake(300, 200) controlPoint1:CGPointMake(180, 100) controlPoint2:CGPointMake(200, 300)];
// 绘制小车的layer
CALayer *carLayer = [CALayer layer];
carLayer.contents = (__bridge id)[UIImage imageNamed:@"car.png"].CGImage;
carLayer.frame = CGRectMake(50-36, 200-36, 36, 36);
carLayer.anchorPoint = CGPointMake(0.5, 0.8);
[self.view.layer addSublayer:carLayer];
// 绘制曲线的layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = path.CGPath;
shapeLayer.fillColor = nil;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:shapeLayer];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
anim.keyPath = @"position";
// 设置曲线
anim.path = path.CGPath;
anim.duration = 4.0f;
anim.removedOnCompletion = NO;
anim.fillMode = kCAFillModeForwards;
// 跟着曲线的方向移动
anim.rotationMode = kCAAnimationRotateAuto;
[carLayer addAnimation:anim forKey:nil];
补充 CAShapeLayer
CAShapeLayer 是一个通过矢量图形而不是 bitmap 来绘制的图层子类。通过制定诸如颜色和线宽等属性,用 CGPath 来定义想要绘制的图形。
相比⽤Core Graphics直接向原始的CALayer 的内容中绘制路径,其优点如下:
- 渲染快速。其使用了硬件加速,绘制同一图形比 Core Craphics 快得多。
- 高效使用内存。不需要向 CALayer 一样创建一个寄宿图,无论多大,也不会占用太多的内存。
- 不会被图层边界裁掉。⼀个 CAShapeLayer 可以在边界之 外绘制
- 不会出现像素化。当对 CAShapeLayer 做3D变换时,它不像⼀个有寄宿图的普通图层⼀样变得像素化。
动画组 CAAnimationGroup
CAAnimationGroup, 是另⼀个继承于 CAAnimation 的⼦类, 它添加了⼀个 animations 数组的属性,⽤来组合别的动画。示例如下:
// 曲线
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(50, 200)];
[path addCurveToPoint:CGPointMake(300, 200) controlPoint1:CGPointMake(180, 100) controlPoint2:CGPointMake(200, 300)];
//需要添加在layer上
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = path.CGPath;
shapeLayer.fillColor = nil;
shapeLayer.strokeColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:shapeLayer];
CALayer *colorLayer = [CALayer layer];
colorLayer.frame = CGRectMake(0, 0, 60, 60);
colorLayer.position = CGPointMake(50, 200);
colorLayer.backgroundColor = [UIColor blueColor].CGColor;
[self.view.layer addSublayer:colorLayer];
// 过山车的动画
CAKeyframeAnimation *anim = [CAKeyframeAnimation animation];
anim.keyPath = @"position";
anim.path = path.CGPath;
// anim.duration = 4.0f;
// anim.removedOnCompletion = NO;
// anim.fillMode = kCAFillModeForwards;
// anim.rotationMode = kCAAnimationRotateAuto;
// [colorLayer addAnimation:anim forKey:nil];
// 改变大小
CABasicAnimation *sizeAnim = [CABasicAnimation animation];
sizeAnim.keyPath = @"transform.scale";
sizeAnim.toValue = @.5;
// sizeAnim.duration = 4.0;
// sizeAnim.fillMode = kCAFillModeForwards;
// sizeAnim.removedOnCompletion = NO;
//
// [colorLayer addAnimation:sizeAnim forKey:nil];
// 修改颜色
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
UIColor *color = [UIColor colorWithRed:red green:green blue:blue alpha:1];
CABasicAnimation *colorAnim = [CABasicAnimation animation];
colorAnim.keyPath = @"backgroundColor";
colorAnim.toValue = (id)color.CGColor;
// colorAnim.duration = 4.0f;
// colorAnim.fillMode = kCAFillModeForwards;
// colorAnim.removedOnCompletion = NO;
// [colorLayer addAnimation:colorAnim forKey:nil];
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = @[anim, sizeAnim, colorAnim];
group.duration = 4.0f;
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[colorLayer addAnimation:group forKey:nil];
效果如下:
转场动画 CATransition
CATransition,对不可动画属性或整个图层做动画处理。
转场动画并不像属性动画那么平滑的在两个值之间做动画,而是影响到整个图层的变化。转场动画首先展示之前的图层外观,然后通过一个交换过渡到新的外观。
为了创建一个转场动画,我们使用CATransition,同样是一个CAAnimation的子类,和别的子类不同,CAAnimation有一个type和subtype来标识变换效果。
type,NSString类型,公开的值有如下四种:
- kCATransitionFade,新图层键入,默认值。
- kCATransitionMoveIn,新图层顶部滑动进入。
- kCATransitionPush,新图层把老图层退总,自己进入。
- kCATransitionReveal,原始图片划出来显示新的图层。
除此之外还有些系统未公开的转场动画类型,如下图:
subtype, NSString 类型,值如下:
- kCATransitionFromRight
- kCATransitionFromLeft
- kCATransitionFromTop
- kCATransitionFromBottom
示例如下,对图片替换加入转换效果:
NSString *imgName = _imgs[_index];
_imageView.image = [UIImage imageNamed:imgName];
CATransition *anim = [CATransition animation];
anim.type = @"suckEffect";
//anim.type = kCATransitionPush;
anim.subtype = kCATransitionFromLeft;
anim.duration = .5;
// anim.startProgress = .2;
// anim.endProgress = .5;
[_imageView.layer addAnimation:anim forKey:nil];
动画效果如下:
动画过程中取消动画
移除某一属性的动画:
- (void)removeAnimationForKey:(NSString *)key;
或者移除所有动画:
- (void)removeAllAnimations;
动画⼀旦被移除,图层的外观就⽴刻更新到当前的模型图层的值。⼀般说来,动画在结束之后被⾃动移除,除⾮设置 removedOnCompletion 为 NO ,如果你设置动画在结束之后不被⾃动移除,那么当它不需要的时候你要⼿动移除它;否则它会⼀直存在于内存中,直到图层被销毁。
CAMediaTiming 协议
定义了一段动画内用来控制逝去时间的属性的集合, CALayer 和 CAAnimation 都实现了这个协议,所以时间可以被任意基于图层或动画的类控制。
- beginTime,开始之前的延迟时间
- timeOffset,时间偏移值
- repeatCount,动画重复次数, 不可和 repeatDuration 同时使用
- repeatDuration,动画重复总时间,不可和 repeatCount 同时使用
- duration,CFTimeInterval, 动画时长
- speed,动画速度,默认1.0,取值0~n,一个时间的倍数, 当等于0时相当于暂停。
- autoreverses,动画完成后是否反向动画到原始值
- fillMode,动画完成后,动画值的填充模式
- kCAFillModeForwards,动画完成后保持动画结束状态
- kCAFillModeBackwards,动画开始前立即进入动画初始状态
- kCAFillModeBoth, kCAFillModeForwards | kCAFillModeBackwards
- kCAFillModeRemoved,动画完车后移除动画结束状态
动画速度
CAMediaTimingFunction , Core Animation 内嵌的一系列标准函数:
- kCAMediaTimingFunctionLinear
- kCAMediaTimingFunctionEaseIn 慢慢加速,全速停止
- kCAMediaTimingFunctionEaseOut 全速开始,慢慢减速停止
- kCAMediaTimingFunctionEaseInEaseOut
- kCAMediaTimingFunctionDefault ,类似kCAMediaTimingFunctionEaseInEaseOut,但缓冲速度略慢
通过 +setAnimationTimingFunction 或 +timingFUnctionWithName 来使用。
自定义缓冲函数
通过+functionWithControlPoints::::来使用:
[CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
文章中Demo地址:https://github.com/huangmoxianganquan/CoreAnimationExample
参考文章:
1、核心动画一览
2、CoreAnimation4-隐式动画和显式动画