这里就要提到CALayer的两个实例方法presentationLayer和modelLayer:
@interface CALayer : NSObject <NSCoding, CAMediaTiming> ... /* 以下参考官方api注释 */ /* presentationLayer * 返回一个layer的拷贝,如果有任何活动动画时,包含当前状态的所有layer属性 * 实际上是逼近当前状态的近似值。 * 尝试以任何方式修改返回的结果都是未定义的。 * 返回值的sublayers 、mask、superlayer是当前layer的这些属性的presentationLayer */ - (nullable instancetype)presentationLayer; /* modelLayer * 对presentationLayer调用,返回当前模型值。 * 对非presentationLayer调用,返回本身。 * 在生成表示层的事务完成后调用此方法的结果未定义。 */ - (instancetype)modelLayer; ...
从注释不难看出,这个presentationLayer即是我们看到的屏幕上展示的状态,而modelLayer就是我们设置完立即生效的真实状态,我们动画开始后延迟0.1s分别打印layer,layer.presentationLayer,layer.modelLayer和layer.presentationLayer.modelLayer :
明显,layer.presentationLayer是动画当前状态的值,而layer.modelLayer 和 layer.presentationLayer.modelLayer 都是layer本身。
到这里,CALayer动画的原理基本清晰了,当有动画加入时,presentationLayer会不断的(从按某种插值或逼近得到的动画路径上)取值来进行展示,当动画结束被移除时则取modelLayer的状态展示。这也是为什么我们用CABasicAnimation时,设定当前值为fromValue时动画执行结束又会回到起点的原因,实际上动画结束并不是回到起点而是到了modelLayer的位置。
虽然我们可以使用fillMode控制它结束时保持状态,但这种方法在动画执行完之后并没有将动画从渲染树中移除(因为我们需要设置animation.removedOnCompletion = NO才能让fillMode生效)。如果我们想让动画停在终点,更合理的办法是一开始就将layer设置成终点状态,其实前文提到的UIView的block动画就是这么做的。
如果我们一开始就将layer设置成终点状态再加入动画,会不会造成动画在终点位置闪一下呢?其实是不会的,因为我们看到的实际上是presentationLayer,而我们修改layer的属性,presentationLayer是不会立即改变的:
MyTestView *view = [[MyTestView alloc]initWithFrame:CGRectMake(200, 200, 100, 100)]; [self.view addSubview:view]; view.center = CGPointMake(1000, 1000); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((1/60) * NSEC_PER_SEC)), dispatchQueue, ^{ NSLog(@"presentationLayer %@ y %f",view.layer.presentationLayer, view.layer.presentationLayer.position.y); NSLog(@"layer.modelLayer %@ y %f",view.layer.modelLayer,view.layer.modelLayer.position.y); }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((1/20) * NSEC_PER_SEC)), dispatchQueue, ^{ NSLog(@"presentationLayer %@ y %f",view.layer.presentationLayer, view.layer.presentationLayer.position.y); NSLog(@"layer.modelLayer %@ y %f",view.layer.modelLayer,view.layer.modelLayer.position.y); });
在上面代码中我们改变view的center,modelLayer是立即改变的因为它就是layer本身。但presentationLayer是没有变的,我们尝试延迟一定时间再去取presentationLayer,发现它是在一个很短的时间之后才发生变化的,这个时间跟具体设备的屏幕刷新频率有关。
也就是说我们给layer设置属性后,当下次屏幕刷新时,presentationLayer才会获取新值进行绘制。因为我们不可能对每一次属性修改都进行一次绘制,而是将这些修改保存在model层,当下次屏幕刷新时再统一取model层的值重绘。
如果我们添加了动画,并将modelLayer设置到终点位置,下次屏幕刷新时,presentationLayer会优先从动画中取值来绘制,所以并不会造成在终点位置闪一下。
小结一下
- UIView持有一个CALayer负责展示,view是这个layer的delegate。改变view的属性实际上是在改变它持有的layer的属性,layer属性发生改变时会调用代理方法actionForLayer: forKey: 来得知此次变化是否需要动画。对同一个属性叠加动画会从当前展示状态开始叠加并最终停在modelLayer的真实位置。
- CALayer内部控制两个属性presentationLayer和modelLayer,modelLayer为当前layer真实的状态,presentationLayer为当前layer在屏幕上展示的状态。presentationLayer会在每次屏幕刷新时更新状态,如果有动画则根据动画获取当前状态进行绘制,动画移除后则取modelLayer的状态。
初探CALayer属性
CALayer和UIView的区别
- 1.UIView是UIKit的(只能iOS使用),CALayer是QuartzCore的(iOS和mac os通用)
- 2.UIView继承UIResponder,CALayer继承NSObject,UIView比CALayer多了一个事件处理的功能,也就是说,CALayer不能处理用户的触摸事件,而UIView可以
- 3.UIView来自CALayer,是CALayer的高层实现和封装,UIView的所有特性来源于CALayer支持
- 4.CABasicAnimation,CAAnimation,CAKeyframeAnimation等动画类都需要加到CALayer上
其实UIView之所以能显示在屏幕上,完全是因为它内部的一个图层,在创建UIView对象时,UIView内部会自动创建一个图层(即CALayer对象),通过UIView的layer属性可以访问这个层。
@property(nonatomic,readonly,retain) CALayer *layer;
当UIView需要显示到屏幕上时,会调用drawRect:方法进行绘图,并且会将所有内容绘制在自己的图层上,绘图完毕后,系统会将图层拷贝到屏幕上,于是就完成了UIView的显示。
换句话说,UIView本身不具备显示的功能,是它内部的层才有显示功能
CALayer属性表
使用CALayer的Mask实现注水动画效果
Core Animation一直是iOS比较有意思的一个主题,使用Core Animation可以实现非常平滑的炫酷动画。Core animtion的API是较高级的封装,使用便捷,使得我们免于自己使用OpenGL实现动画。
下面主要介绍如何使用CALayer的mask实现一个双向注水动画
了解CALayer的mask
@property(strong) CALayer *mask;
mask实际上layer内容的一个遮罩。
如果把mask设置为透明的,实际看到的layer是完全透明的,也就是说只有mask的内容不透明的部分和layer叠加。
实现思路:设计的思路参考基于Core Animation的KTV歌词视图的平滑实现 (http://www.iwangke.me/2014/10/06/how-to-implement-a-core-animation-based-60-fps-ktv-lyrics-view/)
flow 在View上重叠放置两个UIImageView: grayHead&greenHead,默认greenHead会遮挡住grayHead。
为greenHead设置一个mask,这个mask不是普通的mask,它由两个subLayer:maskLayerUp maskLayerDown组成。
默认情况下,subLayer都显示在mask内容之外,此时mask实际上透明的,由此greenHead也是透明的。
现在我们希望greenHead从左上角和右下角慢慢显示内容,那么我们只需要从两个方向为greenHead填充内容就可以了.
创建mask遮罩
- (CALayer *)greenHeadMaskLayer { CALayer *mask = [CALayer layer]; mask.frame = self.greenHead.bounds; self.maskLayerUp = [CAShapeLayer layer]; self.maskLayerUp.bounds = CGRectMake(0, 0, 30.0f, 30.0f); self.maskLayerUp.fillColor = [UIColor greenColor].CGColor; // Any color but clear will be OK self.maskLayerUp.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(15.0f, 15.0f) radius:15.0f startAngle:0 endAngle:2*M_PI clockwise:YES].CGPath; self.maskLayerUp.opacity = 0.8f; self.maskLayerUp.position = CGPointMake(-5.0f, -5.0f); [mask addSublayer:self.maskLayerUp]; self.maskLayerDown = [CAShapeLayer layer]; self.maskLayerDown.bounds = CGRectMake(0, 0, 30.0f, 30.0f); self.maskLayerDown.fillColor = [UIColor greenColor].CGColor; // Any color but clear will be OK self.maskLayerDown.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(15.0f, 15.0f) radius:15.0f startAngle:0 endAngle:2*M_PI clockwise:YES].CGPath; self.maskLayerDown.position = CGPointMake(35.0f, 35.0f); [mask addSublayer:self.maskLayerDown]; return mask; }
做夹角动画
- (void)startGreenHeadAnimation { CABasicAnimation *downAnimation = [CABasicAnimation animationWithKeyPath:@"position"]; downAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(-5.0f, -5.0f)]; downAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(10.0f, 10.0f)]; downAnimation.duration = duration; [self.maskLayerUp addAnimation:downAnimation forKey:@"downAnimation"]; CABasicAnimation *upAnimation = [CABasicAnimation animationWithKeyPath:@"position"]; upAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(35.0f, 35.0f)]; upAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(20.0f, 20.0f)]; upAnimation.duration = duration; [self.maskLayerDown addAnimation:upAnimation forKey:@"upAnimation"]; }
小结一下:
- CALayer提供另外一种操作UI的手段,虽然它提供的API比UIView较底层,但它能提供更加丰富的功能和更高的性能(CALayer的动画是在专门的线程渲染的)。涉及到复杂且性能要求高的UI界面,CALayer的作用就比较明显了,比如AsyncDisplayKit。
- 其实也能看出CALayer的一个用处,通常我们处理圆角时会直接去修改CALayer的cornerRadius,但这种做法性能比较差,尤其是放在列表里的时候,现在我们有了mask,这样我们可以直接改变layer的mask,而不会影响到图形渲染的性能。
多说一点
为啥有了CALayer了还要UIView
UIView继承自UIResponder,主要特点是可以响应触摸事件。而CALayer实际的图层内容管理。大家干的的事情不一样,是两个东西,大家的存在互不影响,理所当然。
UILayer
假设有一个UIView和CALayer集合体UILayer这个UILayer是一个全能的Layer,可以负责管理显示内容,也能处理触摸事件 。
但由于iOS系统的更新,所以你要不断修改维护UILayer,比如iOS3.2版本增加手势识别、iOS4引入了Block语法、iOS6增加AutoLayout特性、iOS7的UI得改头换面,每次都要打开巨长的UILayer从头改到脚。这样的维护成本太高了。
所以,在这份理所当然的SDK的背后,蕴藏着大牛门几十年的设计智慧。当中应该能够看到很多门道。这次就UIView和CALayer来分析,就可以得出一些东西。
这方面的设计原则:
- 机制与策略分离
- 整体稳定
- 各司其职
- 减少暴露
机制与策略分离
Unix内核设计的一个主要思想是——提供(Mechanism)机制而不是策略(Policy)。编程问题都可以抽离出机制和策略部分。机制一旦实现,就会很少更改,但策略会经常得到优化。例如原子可以看做是机制,而各种原子的组成就是一种策略。
CALayer也可以看做是一种机制,提供图层绘制,你们可以翻开CALayer的头文件看看,基本上是没怎么变过的,而UIView可以看做是策略,变动很多。越是底层,越是机制,越是机制就越是稳定。机制与策略分离,可以使得需要修改的代码更少,特别是底层代码,这样可以提高系统的稳定性。
整体稳定
稳定给你的是什么感觉?坚固?不可形变?稳定其实就是不可变。一个系统不可变的东西越多,越是稳定。所以机制恰是满足这个不可变的因素的。构建一个系统有一个指导思想就是尽量抽取不可变的东西和可变的东西分离。水是成不了万丈高楼的,坚固的混凝土才可以。更少的修改,意味着更少的bug的几率。
各司其职
即使能力再大也不能把说有事情都干了,万一哪一天不行了呢,那就是突然什么都不能干了。所以仅仅是基于分散风险原则也不应该出现全能类。各司其职,相互合作,把可控粒度降到最低,这样也可以是系统更稳定,更易修改。
减少暴露
接口应该面向大众的,按照八二原则,其实20%的接口就可以满足80%的需求,剩下的80%应该隐藏在背后。因为漏的少总是安全的,不是吗。剩下的80%专家接口可以隐藏与深层次。比如UIView遮蔽了大部分的CALayer接口,抽取构造出更易用的frame和动画实现,这样上手更容易。
以上文章整理自:https://blog.csdn.net/zmmzxxx/article/details/74276077#一calayer简介