图层 CALayer 对象 简介 iOS中,我们能看得见的东西,基本上都是 UIView,比如一个按钮,一个文本标签。而 UIView 之所以能显示在屏幕上,完全时因为它内部的一个图层。
在创建UIView对象时,内部会自动创建图层 CALayer 对象,通过 UIView 的 layer 属性可以访问这个图层对象。
当 UIView 需要显示到屏幕上时,会调用 drawRect: 方法进行绘图,并且会将所有内部内容绘制在自己的图层上,绘图之后,系统会将图层拷贝到屏幕上,于是就完成了 UIView 的显示。
iOS7 以前需要导入框架,QuartzCore.framework
QuartzCore 框架和 CoreGraphics 框架是可以跨平台使用的,在 iOS 和 Mac OS X 上都能使用。
CALayer 属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @property CGRect bounds;@property CGPoint position;@property CGPoint anchorPoint;@property CGColorRef backgroundColor;@property (retain ) id contents;@property CGFloat borderWidth; @property (nullable ) CGColorRef borderColor; self .purpleView.layer.borderColor = [UIColor greenColor].CGColor;@property CGFloat cornerRadius; self .purpleView.layer.masksToBounds = NO ; @property (nullable ) CGColorRef shadowColor; @property (nonatomic ) CGSize shadowOffset; @property float shadowOpacity; @property CATransform3D transform; self .purpleView.layer.transform = CATransform3DMakeRotation (M_PI_4, 1 , 1 , 0 ); self .purpleView.layer.transform = CATransform3DMakeScale (1.5 , 0.5 , 0 ); [self .purpleView.layer setValue:@(M_PI_2) forKeyPath:@"transform.rotation" ]; [self .purpleView.layer setValue:[NSValue valueWithCATransform3D:CATransform3DMakeScale (0.5 , 2 , 0 )] forKeyPath:@"transform" ]; [self .purpleView.layer setValue:@(0.5 ) forKeyPath:@"transform.scale.y" ];
只要是继承了 UIView,都有图层属性,因此 imageView,button 都可以设置以上属性。
在设置 imageView 的圆角时,由于 imageView 内部还有一个用于显示 图片的子层(UIView 内部只有一个主图层),因此我们改变 imageView 的 Layer时,改变的是主层的圆角。子层显示在主层上,因此图片并没有圆角效果,只有再加一句代码:
1 imageView.layer.masksToBounds = NO ;
告诉 imageView 超出主层的部分全部减掉。才会显示圆角效果。
CALayer 使用注意点: UIView 可以通过 subViews 属性访问所有的子视图,CALayer 也可以通过 subLayers 属性访问所有的子层
UIView 可以通过 superView 属性访问父视图,CALayer 也可以通过 superLayer 访问父层。
如果两个 UIView 是父子关系,那么它们内部的 CALayer 也是父子关系
图层的手动创建 1 2 3 4 5 6 7 8 CALayer *layer = [CALayer layer];layer.frame = CGRectMake (50 , 50 , 200 , 200 ); layer.backgroundColor = [UIColor redColor].CGColor; layer.contents = (id )[UIImage imageNamed:@"阿狸头像" ].CGImage; [self .view.layer addSublayer:layer];
自定义图层的内容 方法一:自己写一个类继承 CALayer 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #import "MyLayer.h" @implementation MyLayer - (void )drawInContext:(CGContextRef )ctx { CGContextSetRGBFillColor (ctx, 1 , 0 , 0 , 1 ); CGContextAddEllipseInRect (ctx, CGRectMake (0 , 0 , 50 , 50 )); CGContextFillPath (ctx); } @end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 - (void )viewDidLoad { [super viewDidLoad]; self .purpleView.hidden = YES ; MyLayer *layer = [MyLayer layer]; layer.bounds = CGRectMake (0 , 0 , 100 , 100 ); layer.position = CGPointZero ; layer.anchorPoint = CGPointZero ; layer.backgroundColor = [UIColor blueColor].CGColor; [layer setNeedsDisplay]; [self .view.layer addSublayer:layer]; }
方法二:实现代理方法 CALayer 的代理是没有协议的,这说明任何对象都可以当 CALayer 代理。而代理方法就在 NSObject 中。当图层需要显示的时候,会调用代理的绘制方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 - (void )viewDidLoad { [super viewDidLoad]; self .purpleView.hidden = YES ; CALayer *layer = [CALayer layer]; layer.bounds = CGRectMake (0 , 0 , 100 , 100 ); layer.position = CGPointZero ; layer.anchorPoint = CGPointZero ; layer.delegate = self ; layer.backgroundColor = [UIColor blueColor].CGColor; [layer setNeedsDisplay]; [self .view.layer addSublayer:layer]; } #pragma mark - CALayer 的代理方法 - (void )drawLayer:(CALayer *)layer inContext:(CGContextRef )ctx { CGContextSetRGBFillColor (ctx, 1 , 0 , 0 , 1 ); CGContextAddEllipseInRect (ctx, CGRectMake (0 , 0 , 50 , 50 )); CGContextFillPath (ctx); }
CALayer 上动画的暂停和恢复 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #pragma mark 暂停CALayer的动画 - (void )pauseLayer:(CALayer *)layer { CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime () fromLayer:nil ]; layer.speed = 0.0 ; layer.timeOffset = pausedTime; } #pragma mark 恢复CALayer的动画 -(void )resumeLayer:(CALayer *)layer { CFTimeInterval pausedTime = layer.timeOffset; layer.speed = 1.0 ; layer.timeOffset = 0.0 ; layer.beginTime = 0.0 ; CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime () fromLayer:nil ] - pausedTime; layer.beginTime = timeSincePause; }
UIView 能够显示内容的原理 首先 view.layer.delegate
== view
,一个 view 内部的图层的代理就是这个 view。view.layer
会准备一个 Layer Graphics Contex(图层类型上下文)
这时要显示内容时会调用 view.layer.delegate
的 -drawLayer:inContext: 方法将图层上的东西绘制出来,而 drawLayer:inContext: 方法内部又回调用 -drawRect:
方法。
因此 view 就可以在 -drawRect:
方法中实现绘图代码,所有东西最终都绘制到 view.layer 上面,系统再将 view.layer 的内容拷贝到屏幕,于是完成了 view 的显示。
隐式动画 每一个 UIView 内部都默认关联着一个 CALayer,我们可以称这个 Layer 为 Root Layer(根层)
所有的非 Root Layer,就是那些手动创建的 CALayer 对象,都存在着隐式动画
当我们对于非 Root Layer 的部分属性进行修改时,默认会自动产生一些动画效果。这些属性称为 Animatable Properties(可动画属性)比如:
bounds:用于设置 CALayer 的宽度和高度,修改这个属性会产生缩放动画
backgroundColor:用于设置 CALayer 的背景色,修改这个属性会产生背景色的渐变动画
position:用于设置 CALayer 的位置,修改这个属性会产生平移动画
代码如下:
首先,需要新建图层,因为只有非根层才有隐式动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 - (void )viewDidLoad { [super viewDidLoad]; self .purpleView.hidden = YES ; CALayer *layer = [CALayer layer]; layer.bounds = CGRectMake (0 , 0 , 100 , 100 ); layer.backgroundColor = [UIColor redColor].CGColor; layer.position = CGPointZero ; layer.anchorPoint = CGPointZero ; [self .view.layer addSublayer:layer]; self .layer = layer; }
当我们点击屏幕时候要做一些事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 - (void )touchesBegan:(NSSet <UITouch *> *)touches withEvent:(UIEvent *)event { self .layer.backgroundColor = [UIColor blueColor].CGColor; self .layer.opacity = 0.5 ; self .layer.position = CGPointMake (100 , 100 ); [CATransaction begin]; [CATransaction setDisableActions:YES ]; self .layer.position = CGPointMake (100 , 100 ); self .layer.opacity = 0.5 ; [CATransaction commit]; }
Core Animation Core Animation简介:
Core Animation 可以用在 Mac OS X 和 iOS 平台。 动画执行过程都是在后台操作,不会阻塞主线程。 Core Animation 直接作用在 CALayer 上,并非 UIView。
使用步骤:
需要 QuartzCore.framework 框架 <QuartzCore/QuartzCore.h>
初始化一个CAAnimation 对象,设置动画相关属性。
通过调用 CALayer 的 addAnimation:forKey:
方法增加 CAAnimation 对象到 CALayer 中,这样就能开始执行动画了,key 为该动画的标识符,删除动画时会用到。
通过调用 CALayer 的 removeAnimationForKey:
方法可以停止 CALayer 中的动画。
CAAnimation 内部结构 CAAnimation 是抽象类
其实,一般情况下,我们使用的比较多的是CAAnimation的子类,因此,先大致看看CAAnimation的继承结构:
黑线代表继承,黑色文字代表类名,白色文字代表属性。其中CAMediaTiming是一个协议(protocol)。
动画代理方法 property delegate;
1 2 3 4 5 6 7 @interface NSObject (CAAnimationDelegate )- (void )animationDidStart:(CAAnimation *)anim; - (void )animationDidStop:(CAAnimation *)anim finished:(BOOL )flag; @end
CAAnimation 的常用属性
1 2 3 4 5 6 7 8 9 10 11 12 13 animation.startProgress = 0 ; animation.endProgress = 0.6 ; animation.duration = 2.0 ; animation.removedOnCompletion = NO ; animation.fillMode = kCAFillModeForwards; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
fillMode 动画执行完毕后的绘制模式
kCAFillModeForwards 当动画结束后,layer会一直保持着动画最新的状态
kCAFillModeBackwards 与 kCAFillModeForwards 相对应,表示只要一开始执行动画(-addAnimation: 方法),便会让 layer 立即进入初始位置(fromValue)
kCAFillModeRemoved 这个是默认值,动画开始前和动画结束后,动画对layer都没有影响,动画结束后,layer会恢复到之前的状态
kCAFillModeBoth 动画加入后开始之前,layer便处于动画初始状态,动画结束后layer保持动画最后的状态。
==其中 transform 属性传入的是 CATransform3D==
timingFunction 属性可选的值有:
kCAMediaTimingFunctionLinear(线性):匀速,给你一个相对静态的感觉
kCAMediaTimingFunctionEaseIn(渐进):动画缓慢进入,然后加速离开
kCAMediaTimingFunctionEaseOut(渐出):动画全速进入,然后减速的到达目的地
kCAMediaTimingFunctionEaseInEaseOut(渐进渐出):动画缓慢的进入,中间加速,然后减速的到达目的地。这个是默认的动画行为。
CAAnimation 的子类们 CATransition(transition 有过渡的意思)(重要) 作用:转场动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 CATransition *animation = [CATransition animation]; animation.type = @"moveIn" ; animation.subtype = kCATransitionFromBottom; animation.duration = 0.5 ; animation.startProgress = 0 ; animation.endProgress = 0.6 ; [self .imageView.layer addAnimation:animation forKey:nil ];
CABasicAnimation基本动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 CABasicAnimation *animation = [CABasicAnimation animation]; animation.keyPath = @"position" ; animation.fromValue = [NSValue valueWithCGPoint:CGPointMake (0 , 0 )]; animation.toValue = [NSValue valueWithCGPoint:CGPointMake (200 , 300 )]; animation.byValue; animation.duration = 2.0 ; animation.removedOnCompletion = NO ; animation.fillMode = kCAFillModeForwards; [self .layer addAnimation:animation forKey:nil ];
CAKeyframeAnimation帧动画 可以做一些列连续顺畅的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];animation.keyPath = @"position" ; CGMutablePathRef path = CGPathCreateMutable (); CGPathAddEllipseInRect (path, NULL , CGRectMake (100 , 100 , 200 , 200 )); animation.path = path; CGPathRelease (path); animation.duration = 2.0 ; animation.removedOnCompletion = NO ; animation.fillMode = kCAFillModeForwards; animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; [self .purpleView.layer addAnimation:animation forKey:nil ];
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position" ; NSValue *v1 = [NSValue valueWithCGPoint:CGPointZero ];NSValue *v2 = [NSValue valueWithCGPoint:CGPointMake (100 , 100 )];NSValue *v3 = [NSValue valueWithCGPoint:CGPointMake (0 , 0 )];NSValue *v4 = [NSValue valueWithCGPoint:CGPointMake (200 , 300 )];NSValue *v5 = [NSValue valueWithCGPoint:CGPointMake (300 , 200 )];NSValue *v6 = [NSValue valueWithCGPoint:CGPointMake (0 , 0 )]; animation.values = @[v1, v2, v3, v4, v5, v6]; animation.duration = 2.0 ; animation.removedOnCompletion = NO ; animation.fillMode = kCAFillModeForwards; [self .purpleView.layer addAnimation:animation forKey:nil ];
CAAnimationGroup 动画组,可以让很多对象一起执行动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 CAAnimationGroup *group = [CAAnimationGroup animation];CABasicAnimation *scale = [CABasicAnimation animation];scale.keyPath = @"transform.scale" ; scale.toValue = @0.5 ; CABasicAnimation *rotation = [CABasicAnimation animation];rotation.keyPath = @"transform.rotation" ; rotation.toValue = @(arc4random_uniform(M_PI)); CABasicAnimation *position = [CABasicAnimation animation];position.keyPath = @"position" ; position.toValue = [NSValue valueWithCGPoint:CGPointMake (arc4random_uniform(200 ), arc4random_uniform(200 ))]; group.animations = @[scale,rotation,position]; [self .purpleView.layer addAnimation:group forKey:nil ];
UIView animation 图层动画都是假象,动画执行过程中,图层的 position 属性一直没有变过
开发中用的比较多的不是图层动画,除非是转场动画 。一般开发中用的比较多的是 UIView 动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + (void )animateWithDuration:(NSTimeInterval )duration delay:(NSTimeInterval )delay options:(UIViewAnimationOptions )options animations:(void (^)(void ))animations completion:(void (^ __nullable )(BOOL finished))completion NS_AVAILABLE_IOS (4 _0); + (void )animateWithDuration:(NSTimeInterval )duration animations:(void (^)(void ))animations completion:(void (^ __nullable )(BOOL finished))completion NS_AVAILABLE_IOS (4 _0); + (void )animateWithDuration:(NSTimeInterval )duration animations:(void (^)(void ))animations NS_AVAILABLE_IOS (4 _0); + (void )animateWithDuration:(NSTimeInterval )duration delay:(NSTimeInterval )delay usingSpringWithDamping:(CGFloat )dampingRatio initialSpringVelocity:(CGFloat )velocity options:(UIViewAnimationOptions )options animations:(void (^)(void ))animations completion:(void (^ __nullable )(BOOL finished))completion NS_AVAILABLE_IOS (7 _0); + (void )transitionWithView:(UIView *)view duration:(NSTimeInterval )duration options:(UIViewAnimationOptions )options animations:(void (^ __nullable )(void ))animations completion:(void (^ __nullable )(BOOL finished))completion NS_AVAILABLE_IOS (4 _0); + (void )transitionFromView:(UIView *)fromView toView:(UIView *)toView duration:(NSTimeInterval )duration options:(UIViewAnimationOptions )options completion:(void (^ __nullable )(BOOL finished))completion NS_AVAILABLE_IOS (4 _0); + (void )performSystemAnimation:(UISystemAnimation )animation onViews:(NSArray <__kindof UIView *> *)views options:(UIViewAnimationOptions )options animations:(void (^ __nullable )(void ))parallelAnimations completion:(void (^ __nullable )(BOOL finished))completion NS_AVAILABLE_IOS (7 _0);
CADisplayLink 简介 所在框架 CADisplayLink 和其它 CoreAnimation 类一样,都是在 QuartzCore.framework 里。
功能 CADisplayLink 最主要的特征是能提供一个周期性的调用我们赋给它的 selector 的机制,从这点上看它很像定时器 NSTimer。
使用方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 - (void )startDisplayLink { self .displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector (handleDisplayLink:)]; [self .displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode ]; } - (void )handleDisplayLink:(CADisplayLink *)displayLink { } - (void )stopDisplayLink { [self .displayLink invalidate]; self .displayLink = nil ; }
当把 CADisplayLink 对象 add 到 runloop 中后,selector 就能被周期性调用,类似于 NSTimer 被启动了;
执行 invalidate 操作时, CADisplayLink 对象就会从 runloop 中移除,selector 调用也随即停止,类似于 NSTimer 的 invalidate 方法。
特性 下面结合 NSTimer 来介绍 CADisplayLink,与 NSTimer 不同的地方有:
原理 CADisplayLink 是一个能让我们以和屏幕刷新率同步的频率将特定的内容画到屏幕上的定时器类。 CADisplayLink 以特定模式注册到 runloop 后, 每当屏幕显示内容刷新结束的时候,runloop 就会向 CADisplayLink 指定的 target 发送一次指定的 selector 消息, CADisplayLink 类对应的 selector 就会被调用一次。
NSTimer 以指定的模式注册到 runloop 后,每当设定的周期时间到达后,runloop 会向指定的 target 发送一次指定的 selector 消息。
周期设置方式 iOS设备的屏幕刷新频率(FPS)是60Hz,因此 CADisplayLink的selector 默认调用周期是每秒60次,这个周期可以通过 frameInterval 属性设置, CADisplayLink 的 selector 每秒调用次数 = 60 / frameInterval。比如当 frameInterval 设为 2,每秒调用就变成 30 次。因此, CADisplayLink 周期的设置方式略显不便。
精确度 iOS设备的屏幕刷新频率是固定的,CADisplayLink 在正常情况下会在每次刷新结束都被调用,精确度相当高。
NSTimer 的精确度就显得低了点,比如 NSTimer 的触发时间到的时候,runloop 如果在忙于别的调用,触发时间就会推迟到下一个 runloop 周期。更有甚者,在 OS X v10.9 以后为了尽量避免在 NSTimer 触发时间到了而去中断当前处理的任务,NSTimer 新增了 tolerance 属性,让用户可以设置可以容忍的触发的时间范围。
使用场合 从原理上不难看出, CADisplayLink 使用场合相对专一, 适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。
NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。
重要属性 下面不完整的列出了 CADisplayLink 的几个重要属性:
frameInterval 可读可写的 NSInteger 型值,标识间隔多少帧调用一次 selector 方法,默认值是1 ,即每帧都调用一次。
官方文档中强调,当该值被设定小于1时,结果是不可预知的。
duration 只读的 CFTimeInterval 值,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在 target 的 selector 被首次调用以后才会被赋值。selector 的调用间隔时间计算方式是:时间 = duration × frameInterval。
现存的 iOS 设备屏幕的 FPS 都是60Hz,这一点可以从 CADisplayLink 的 duration 属性看出来。duration 的值都是0.166666…,即1/60。尽管如此,我们并没法确定苹果不会改变 FPS ,如果以后某一天将 FPS 提升到了120Hz了怎么办呢?这时,你设置了 frameInterval 属性值为2期望每秒刷新30次,却发现每秒刷新了60次,结果可想而知,出于安全考虑,还是先根据 duration 判断屏幕的 FPS 再去使用 CADisplayLink 。
timestamp 只读的 CFTimeInterval 值,表示屏幕显示的上一帧的时间戳,这个属性通常被target 用来计算下一帧中应该显示的内容。 打印 timestamp 值,其样式类似于:
179699.631584
虽然名为时间戳,但这和常见的 unix 时间戳差异很大,事实上这是 CoreAnimation 使用的时间格式。每个 CALayer 都有一个本地时间(CALayer 本地时间的具体作用会在后续文章中说明),可以获取当前 CALayer 的本地时间并打印:
1 2 CFTimeInterval localLayerTime = [myLayer convertTime:CACurrentMediaTime () fromLayer:nil ];NSLog ("localLayerTime:%f" ,localLayerTime);
注意 iOS并不能保证能以每秒60次的频率调用回调方法,这取决于:
如果CPU忙于其它计算,就没法保证以60HZ执行屏幕的绘制动作,导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。
如果执行回调时间大于重绘每帧的间隔时间,就会导致跳过若干次回调调用机会,这取决于执行时间长短。
参考文档
官方文档
https://developer.apple.com/library/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/Reference/Reference.html#//apple_ref/doc/uid/TP40009031-CH1-DontLinkElementID_1
官方使用CADisplayLink播放视频的例子
https://developer.apple.com/library/ios/samplecode/AVBasicVideoOutput/Introduction/Intro.html#//apple_ref/doc/uid/DTS40013109
本文代码连结:CAAnimation