iOS-CAlayer
-
为什么要有CAlayer?
① 在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKit和UIView,但是Mac OS有AppKit和NSView的原因。他们功能上很相似,但是在实现上有着显著的区别。
② 绘图,布局和动画,相比之下就是类似Mac笔记本和桌面系列一样应用于iPhone和iPad触屏的概念。把这种功能的逻辑分开并应用到独立的Core Animation框架,苹果就能够在iOS和Mac OS之间共享代码,使得对苹果自己的OS开发团队和第三方开发者去开发两个平台的应用更加便捷。 -
QuartzCore = CoreAnimation
这个框架的名称感觉不是很清晰,但是看它的头文件可以发现,它其实就是CoreAnimation,这个框架的头文件只包含了CoreAnimation.h,他里面的类大部分都是CA开头的,是动画库。 -
CoreGraphics
CoreGraphics 是图形库,并且CoreAnimation中大量用到CoreGraphics中的类,原因是显然的,实现动画自然要用到图形库中的东西。 -
CALayer是定义在QuartzCore框架中的,CGImageRef、CGColorRef两种数据类型是定义在CoreGraphics框架中的,UIColor、UIImage是定义在UIKit框架中的。
-
QuartzCore框架和CoreGraphics框架都是可以跨平台使用的,在iOS和Mac OS上都能使用,但是UIKit只能在iOS中使用,因此,为了保证可移植性,QuartzCore不能使用UIImage、UIColor,只能使用CGImageRef、CGColorRef,不过很多情况下,可以通过UIKit对象的特定方法,得到CoreGraphics对象,比如UIImage的CGImage方法可以返回一个CGImageRef。
一. 关于CAlayer
在iOS中,你能看得见摸得着的东西基本上都是UIView,比如一个按钮、一个文本标签、一个文本输入框、一个图标等等,这些都是UIView。
其实UIView之所以能显示在屏幕上,完全是因为它内部的一个图层,在创建UIView对象时,UIView内部会自动创建一个图层(即CALayer对象),通过UIView的layer属性可以访问这个层。
@property(nonatomic,readonly,retain) CALayer *layer;
当UIView需要显示到屏幕上时,会调用drawRect:方法进行绘图,并且会将所有内容绘制在自己的图层上,绘图完毕后,系统会将图层拷贝到屏幕上,于是就完成了UIView的显示。
换句话说,UIView本身不具备显示的功能,拥有显示功能的是它内部的图层。
CAlayer的一些基本属性:
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 150, 150)];
view.backgroundColor = [UIColor yellowColor];
view.layer.borderWidth = 3;
view.layer.borderColor = [UIColor redColor].CGColor;
view.layer.cornerRadius = 30;
//contents是id类型,可以接受内容,上面的实例让layer显示一张图片,仔细观察可以发现四个圆角的部分露了一个角出来。
//那是因为设置的image不是展示在主图层上的,而是显示在子图层上的。可以通过设置一个范围,设置超出主图层的部分把它给剪切掉。
view.layer.contents=(id)[UIImage imageNamed:@"me"].CGImage;
//可以通过view.layer.sublayers 访问所有子层
//UI框架中使用的方法
//view.clipsToBounds=YES;
//把超过根层以外的东西都给裁剪掉(UIview自带的层,我们称它是根层)
view.layer.masksToBounds = YES;
//设置阴影,不光需要设置阴影颜色,还应该设置阴影的偏移位和透明度。
//因为如果不设置偏移位的话,那么阴影和layer完全重叠,且默认透明度为0(即完全透明)。
//如果设置了超过主图层的部分减掉,则设置阴影不会有显示效果。
//设置阴影的颜色
view.layer.shadowColor=[UIColor blackColor].CGColor;
//设置阴影的偏移量,如果为正数,则代表为往右边偏移
view.layer.shadowOffset=CGSizeMake(15, 5);
//设置阴影的透明度(0~1之间,0表示完全透明)
view.layer.shadowOpacity=0.6;
//设置阴影的模糊的半径
view.layer.shadowRadius = 5;
关于形变属性(transform):
//通过UIView设置(2D效果)
view.transform = CGAffineTransformMakeTranslation(0, -100);
//通过layer来设置(3D效果,x,y,z三个方向)
view.layer.transform=CATransform3DMakeTranslation(100, 20, 0);
//通过KVC来设置,通过KVC一般是做快速旋转,平移,缩放 - 更多关于KVC可以赋值的属性请参考官方文档
NSValue *v=[NSValue valueWithCATransform3D:CATransform3DMakeTranslation(100, 20, 0)];
[view.layer setValue:v forKeyPath:@"transform"];
//如果是只需要设置在某一个方向上的移动,可以参考下面的代码
//在x的方向上向左移动100
[view.layer setValue:@(-100) forKeyPath:@"transform.translation.x"];
//旋转一个弧度
view.layer.transform = CATransform3DMakeRotation(M_PI_4, 1, 1, 0.5);
1. position和anchorPoint 位置和锚点
这是CALayer的两个非常重要的属性
① position
@property CGPoint position;
- 用来设置CALayer在父层中的位置
- 以父层的左上角为原点(0, 0)
② anchorPoint
@property CGPoint anchorPoint;
- 称为“定位点”、“锚点”
- 决定着CALayer身上的哪个点会在position属性所指的位置
- 以自己的左上角为原点(0, 0)
- 它的x、y取值范围都是0~1,默认值为(0.5, 0.5)
举例:
灰色view为参照物,position是(200,200) bounds是(100,100),只修改红色view的锚点,观察区别,代码如下:
//参照物viewTwo
viewTwo = [[UIView alloc] init];
viewTwo.layer.position = CGPointMake( 200.f, 200.f);
viewTwo.layer.bounds = CGRectMake(0.f, 0.f, 100.f, 100.f);
//设置完position和bounds、anchorPoint(默认)就相当于设置了frame
viewTwo.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:viewTwo];
//进行试验的viewOne
viewOne = [[EOCView alloc] init];
viewOne.backgroundColor = [UIColor redColor];
[self.view addSubview:viewOne];
viewOne.layer.position = CGPointMake( 200.f, 200.f);
viewOne.layer.anchorPoint = CGPointMake(1.f, 0.f); //默认值为0.5 0.5
viewOne.layer.bounds = CGRectMake(0.f, 0.f, 100.f, 100.f);
NSLog(@"position %@ anchorPoint %@ ", NSStringFromCGPoint(viewOne.layer.position), NSStringFromCGPoint(viewOne.layer.anchorPoint));
viewOne.layer.anchorPoint = CGPointMake(0.f, 0.f);
(0,0).pngviewOne.layer.anchorPoint = CGPointMake(1.f, 1.f);
(1,1).pngviewOne.layer.anchorPoint = CGPointMake(1.f, 0.f);
(1,0).png- anchorPoint是锚点,它的值范围为(0~1),是相对于layer本身的坐标
- view的旋转是围绕着锚点来旋转的
- postion是相对于父层的坐标,position和anchorPoint实际上指的是同一个点
- postion或者anchorPoint的修改,可以改变view的布局
- position和bounds和anchorPoint的值定下来,这个view的布局信息就定下来了
总结:
知道position的值,把anchorPoint和position对齐,就是view的布局。
上面我们说了,旋转是围绕锚点旋转的,最经典的例子是秒针的旋转,代码如下:
//锚点的运用
- (void)anchorPointCase {
//我们的layer或者view,你对它进行旋转的时候,它是围绕着锚点来旋转的
//我写一个秒针,我让秒针来动
CALayer *secondLayer = [CALayer layer];
secondLayer.bounds = CGRectMake(0.f, 0.f, 10.f, 40.f);
secondLayer.position = CGPointMake(50.f, 50.f);
secondLayer.anchorPoint = CGPointMake(0.5f, 1.0f);
secondLayer.backgroundColor = [UIColor lightGrayColor].CGColor;
[viewOne.layer addSublayer:secondLayer];
//开启一个定时器
static int currentSecond = 0;
[NSTimer scheduledTimerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
currentSecond++;
secondLayer.transform = CATransform3DMakeRotation(currentSecond*(M_PI*2)/60, 0.f, 0.f, 1.f);
}];
}
效果图:(其实它在转,我懒得截动图)
秒针.png
2. 设置frame的本质
为了探究,我们自定义View自定义layer,View使用我们的layer,点击空白设置fram,查看方法调用顺序:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
viewOne = [[EOCView alloc] init];
viewOne.backgroundColor = [UIColor redColor];
[self.view addSubview:viewOne];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//修改viewOne的frame:
//1、先调用setFrame,后调用position,最后调用bounds (通过打印的次序 以及 注释掉 [super setFrame:frame] 得出结论)
//2、修改frame,本质是修改layer层的position和bounds
viewOne.frame = CGRectMake(100.f, 200.f, 100.f, 100.f);
}
自定义layer代码:
#import "EOCLayer.h"
@implementation EOCLayer
- (void)setFrame:(CGRect)frame {
NSLog(@"%s", __func__);
[super setFrame:frame];
}
- (void)setBounds:(CGRect)bounds {
NSLog(@"%s", __func__);
[super setBounds:bounds];
}
- (void)setPosition:(CGPoint)position {
NSLog(@"%s", __func__);
[super setPosition:position];
}
@end
结果:
[42590:12178660] -[EOCLayer setFrame:]
[42590:12178660] -[EOCLayer setPosition:]
[42590:12178660] -[EOCLayer setBounds:]
修改viewOne的坐标:
- 先调用layer层的setFrame,后调用position,最后调用bounds(通过打印的次序以及注释掉 [super setFrame:frame] 得出结论)
- 修改frame,本质是修改layer层的position和bounds
- 推导出frame的计算公式(不包含旋转角度)
frame.origin.x = position.x - anchorPoint.x*width
frame.origin.y = position.y - anchorPoint.y*height
frame.width
frame.height
下面我们验证结论:
UIView *viewThree = [[UIView alloc] init];
viewThree.backgroundColor = [UIColor yellowColor];
//内部会设置position、bounds
viewThree.frame = CGRectMake(100.f, 300.f, 200.f, 200.f);
//viewThree.layer.anchorPoint = CGPointMake(0, 0);
NSLog(@"position %@", NSStringFromCGPoint(viewThree.layer.position)); //{100, 300}、{200, 400}
可以发现frame设置之后,无论锚点是多少,打印都是:
position {200, 400}
验证了:给View设置frame,其实内部是设置layer的position和bounds(没打印肯定也是不变),虽然position位置不变,但是当我们改变锚点的时候,View的布局还是会改变的。如下:
锚点(0.5,0.5).png 锚点(0,0).png验证了上面所说。
二. 非根层的隐式动画
- 每一个UIView内部都默认关联着一个CALayer,我们可用称这个Layer为Root Layer(根层)。
- 所有的非Root Layer,也就是手动创建的CALayer对象,都存在着隐式动画。
- 什么是隐式动画?
当对非Root Layer的部分属性进行修改时,默认会自动产生一些动画效果,这些动画效果就叫隐式动画,而这些属性称为Animatable Properties(可动画属性)。 - 列举几个常见的Animatable Properties:
bounds:用于设置CALayer的宽度和高度,修改这个属性会产生缩放动画
backgroundColor:用于设置CALayer的背景色,修改这个属性会产生背景色的渐变动画
position:用于设置CALayer的位置,修改这个属性会产生平移动画
1. 隐式动画验证
创建一个自定义View和自定义layer,点击空白分别改变View颜色和layer颜色,代码如下:
#import "EOCLayerAnimationViewController.h"
#import "EOCLayerThree.h"
#import "EOCViewThree.h"
@interface EOCLayerAnimationViewController () {
EOCViewThree *viewThree;
EOCLayerThree *layerThree;
}
@end
@implementation EOCLayerAnimationViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
viewThree = [[EOCViewThree alloc] initWithFrame:CGRectMake(100.f, 100.f, 200.f, 200.f)];
viewThree.backgroundColor = [UIColor redColor];
[self.view addSubview:viewThree];
layerThree = [EOCLayerThree layer];
layerThree.frame = CGRectMake(100.f, 400.f, 200.f, 200.f);
layerThree.backgroundColor = [UIColor redColor].CGColor;
//layerThree.delegate = viewThree;
[self.view.layer addSublayer:layerThree];
}
//view层属性的修改,没有动画;自定义的layer层属性修改,有隐式动画
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//修改viewOne的backgroundColor 这里加一个UIView animation的block
UIColor *color = [UIColor colorWithRed:arc4random_uniform(256.f)/255.f green:arc4random_uniform(256.f)/255.f blue:arc4random_uniform(256.f)/255.f alpha:1.f];
viewThree.backgroundColor = color;
//修改View的背景其实就是修改主layer的背景,如下,都没有隐式动画
//viewThree.layer.backgroundColor = color.CGColor;
//layer层隐式动画增加时间
[CATransaction setAnimationDuration:6.f];
//关闭掉隐式动画
//[CATransaction setDisableActions:YES];
//修改layerOne的backgroundColor
layerThree.backgroundColor = color.CGColor;
}
@end
点击空白,效果图如下:
隐式动画.gif
可以发现,点击空白,修改view的背景色(其实就是修改view.layer.backgroundColor,就是修改主layer的背景)没动画,修改自定义layer的背景色有动画。
2. 为什么修改自定义layer层的属性会有隐式动画?
首先我们自定义layer,自定义View重写他们的一些方法如下:
自定义layer:
#import "EOCLayerThree.h"
#import <UIKit/UIKit.h>
@implementation EOCLayerThree
- (nullable id<CAAction>)actionForKey:(NSString *)event {
NSLog(@"%s", __func__);
return [super actionForKey:event];
}
+ (id)defaultActionForKey:(NSString *)key {
NSLog(@"%s", __func__);
return [super defaultActionForKey:key];
}
- (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key {
NSLog(@"%s", __func__);
NSLog(@"anim: %@ key: %@", anim, key);
[super addAnimation:anim forKey:key];
}
@end
自定义View:
#import "EOCViewThree.h"
#import "EOCLayerThree.h"
@implementation EOCViewThree
//layer层的actionForKey:event这个方法内部
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
NSLog(@"%s", __func__);
//return nil; //还会往下执行,就有动画了
return [super actionForLayer:layer forKey:event]; //[NSNull null] 不会往下执行了, 系统把他拦截了,就没动画了
}
@end
其实给自定义layer修改属性的时候会调用layer的actionForKey:event方法
actionForKey:event这个方法内部实现逻辑:
以下4个步骤,如果步骤返回nil,那么继续执行;如果非空,那么就是非空的值,停止执行
1. if defined, call the delegate method -actionForLayer:forKey:
2. look in the layer's `actions' dictionary
3. look in any `actions' dictionaries in the `style' hierarchy
4. call +defaultActionForKey: on the layer's class
在actionForKey:event方法上打断点,po,结果都为nil
(lldb) po self.actions
nil
(lldb) po self.style
nil
当点击空白给layer修改属性后,po
(lldb) po [super actionForKey:event]
[46158:12441869] +[EOCLayerThree defaultActionForKey:]
<CABasicAnimation:0x600002f20020; fillMode = backwards; timingFunction = default; keyPath = backgroundColor; fromValue = <CGColor 0x600000b7c840> [<CGColorSpace 0x600000b748a0> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 0 0 1 )>
会发现actionForKey方法最后返回了一个CABasicAnimation对象
方法的打印顺序如下:
[46158:12441869] -[EOCLayerThree actionForKey:]
[46158:12441869] +[EOCLayerThree defaultActionForKey:]
[46158:12441869] -[EOCLayerThree addAnimation:forKey:]
[46158:12441869] anim: <CABasicAnimation: 0x600002f281e0> key: backgroundColor
总结:
点击空白处,给layer修改属性,会调用actionForKey方法,actionForKey内部会做上面那些事,因为actions和style都是nil,所以会调用defaultActionForKey,最后会返回一个CABasicAnimation,最后再调用addAnimation,把动画添加上去,所以就有了动画。
3. 为什么修改View的属性不会有隐式动画?
其实这个和View的actionForLayer: forKey:方法有关,上面我们说过
actionForKey内部会调用这个方法,当我们需改View的颜色的时候,系统内部还是修改view.layer的颜色,所以也会调用这个方法,但是在这个方法返回了[NSNull null],就被拦截了,所以actionForKey内部的其他方法都不会调用了,所以改View的属性不会有隐式动画。
如果想要修改View的属性也会有隐式动画,可以在这个actionForLayer: forKey:方法返回nil,并且在view的颜色设置之前,动画时间设置3秒[CATransaction setAnimationDuration:3.f];这样就有动画了,如下:
//layer层的actionForKey:event这个方法内部
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
NSLog(@"%s", __func__);
//return nil; //还会往下执行,就有动画了
return [super actionForLayer:layer forKey:event]; //[NSNull null] 不会往下执行了,系统把他拦截了,就没动画了
}
效果图:
View也有隐式动画.gif
三. layer层的使用
添加一个显示图片的图层
- (void)viewDidLoad
{
[super viewDidLoad];
//创建一个layer
CALayer *Mylayer=[CALayer layer];
//设置layer的属性
Mylayer.bounds=CGRectMake(100, 100, 100, 100);
Mylayer.position=CGPointMake(100, 100);
//设置需要显示的图片
Mylayer.contents=(id)[UIImage imageNamed:@"me"].CGImage;
//设置圆角半径为10
Mylayer.cornerRadius=10;
//如果设置了图片,那么需要设置这个属性为YES才能显示圆角效果
Mylayer.masksToBounds=YES;
//设置边框
Mylayer.borderWidth=3;
Mylayer.borderColor=[UIColor brownColor].CGColor;
//把layer添加到界面上
[self.view.layer addSublayer:Mylayer];
}
运行后,效果图如下:
添加图片.png
可以发现,添加背景色或者添加图片不仅可以通过添加层来实现,还可以通过添加UIView来实现。
既然CALayer和UIView都能实现相同的显示效果,那究竟该选择谁好呢?
总结:
- 其实,对比CALayer,UIView多了一个事件处理的功能,也就是说,CALayer不能处理用户的触摸事件,而UIView可以。
- 所以,在选择的过程中,需要考虑到实际的情况,如果显示出来的东西需要跟用户进行交互的话,用UIView;如果不需要跟用户进行交互,用UIView或者CALayer都可以。
- 当然,CALayer的性能会高一些,因为它少了事件处理的功能,更加轻量级。
四. 关于layer的子层
- (void)viewDidLoad {
NSLog(@"star-%@",self.view.layer.sublayers);
CALayer *layer=[CALayer layer];
//2.设置layer的属性(一定要设置位置,颜色属性才能显示出来)
layer.backgroundColor=[UIColor brownColor].CGColor;
layer.bounds=CGRectMake(0, 0, 200, 200);
layer.position=CGPointMake(100, 100);
//3.把layer添加到界面上
[self.view.layer addSublayer:layer];
//添加btn按钮
// UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom];
// btn.frame = CGRectMake(100, 100, 100, 100);
// btn.backgroundColor = [UIColor redColor];
// [self.view addSubview:btn];
NSLog(@"end-%@",self.view.layer.sublayers);
}
打印结果:
2019-11-08 10:08:36.431372+0800 基础动画[8491:9093151] star-(null)
2019-11-08 10:08:36.431686+0800 基础动画[8491:9093151] end-(
"<CALayer: 0x60000327bfa0>"
)
- UIView可以通过subviews属性访问所有的子视图,类似地,CALayer也可以通过sublayers属性访问所有的子层。
- UIView可以通过superview属性访问父视图,类似地,CALayer也可以通过superlayer属性访问父层。
我们打开上面代码添加btn的注释,打印结果为:
2019-11-08 10:13:10.060028+0800 基础动画[8554:9097922] star-(null)
2019-11-08 10:13:10.066250+0800 基础动画[8554:9097922] end-(
"<CALayer: 0x6000036398e0>",
"<CALayer: 0x600003645f60>"
)
可以发现:
- 如果一个控件是另外一个控件的子控件,那么这个控件的layer也是另一个控件的子layer。这个和一个控制器的view成为另外一个控制器的子view,那么这个控制器也要是另外一个控制器的子控制器,是一个道理。
五. 如何在自定义layer中绘图
方式一:
以前想要在view中画东西,需要创建一个类,继承于UIView,然后重写它的drawRect:方法,在该方法中画图。
绘制图形的步骤:
(1)获取上下文
(2)绘制图形
(3)渲染图形
如果在layer上画东西,与上面的过程类似。
新建layer类,继承于CALayer,重写这个类的drawInContext:方法,代码如下:
在自定义layer中画个圆
//重写该方法,在该方法内绘制图形
-(void)drawInContext:(CGContextRef)ctx
{
//1.绘制图形
//画一个圆
CGContextAddEllipseInRect(ctx, CGRectMake(50, 50, 100, 100));
//设置属性(颜色)
//[[UIColor yellowColor]set];
CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
//2.渲染
CGContextFillPath(ctx);
}
添加到 self.view.layer 上
- (void)viewDidLoad {
//1.创建自定义的layer
mylayer *layer=[mylayer layer];
//2.设置layer的属性
layer.backgroundColor = [UIColor brownColor].CGColor;
layer.bounds = CGRectMake(0, 0, 200, 150);
layer.anchorPoint = CGPointZero;
layer.position = CGPointMake(100, 100);
layer.cornerRadius = 20;
layer.shadowColor = [UIColor blackColor].CGColor;
layer.shadowOffset = CGSizeMake(10, 20);
layer.shadowOpacity = 0.6;
[layer setNeedsDisplay];
//3.添加layer
[self.view.layer addSublayer:layer];
}
效果图:
layer中绘图.png
注意:
- 在自定义layer中 drawInContext:方法不会自己调用,只能自己通过setNeedDisplay方法调用。
- 在自定义view中 drawRect:方法会在view第一次显示的时候自动调用。
方式二:
设置CALayer的delegate,然后让delegate实现drawLayer:inContext:方法,当CALayer需要绘图时,会调用delegate的drawLayer:inContext:方法进行绘图。
- (void)viewDidLoad {
//1.创建自定义的layer
CALayer *layer=[CALayer layer];
//2.设置layer的属性
layer.backgroundColor=[UIColor brownColor].CGColor;
layer.bounds=CGRectMake(0, 0, 200, 150);
layer.anchorPoint=CGPointZero;
layer.position=CGPointMake(100, 100);
layer.cornerRadius=20;
layer.shadowColor=[UIColor blackColor].CGColor;
layer.shadowOffset=CGSizeMake(10, 20);
layer.shadowOpacity=0.6;
//设置代理
layer.delegate = self;
[layer setNeedsDisplay];
//3.添加layer
[self.view.layer addSublayer:layer];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
//1.绘制图形
//画一个圆
CGContextAddEllipseInRect(ctx, CGRectMake(50, 50, 100, 100));
//设置属性(颜色)
// [[UIColor yellowColor]set];
CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
//2.渲染
CGContextFillPath(ctx);
}
效果图如上👆
注意:不能再将某个UIView设置为CALayer的delegate,因为UIView对象已经是它内部根层的delegate,再次设置为其他层的delegate就会出问题。
和UIView中绘图的区别:
一般我们绘制view代码如下:
- (void)drawRect:(CGRect)rect
{
//1.获取上下文
CGContextRef ctx=UIGraphicsGetCurrentContext();
//2.绘制图形
CGContextAddEllipseInRect(ctx, CGRectMake(50, 50, 100, 100));
//设置属性(颜色)
// [[UIColor yellowColor]set];
CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
//3.渲染
CGContextFillPath(ctx);
}
添加:
- (void)viewDidLoad {
myView *myV = [[myView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
myV.backgroundColor = [UIColor yellowColor];
[self.view addSubview:myV];
}
效果图:
UIView绘制
UIView显示详细过程:
- 当UIView需要显示时,它内部的层会准备好一个CGContextRef(图形上下文),然后调用delegate的drawLayer:inContext:方法,并且传入已经准备好的CGContextRef对象。
- 而在drawLayer:inContext:方法中通过[super drawLayer:inContext:]又会调用UIView的drawRect:方法。
- 平时在drawRect:中通过UIGraphicsGetCurrentContext()获取的就是由层传入的CGContextRef对象,在drawRect:中完成的所有绘图都会填入层的CGContextRef中,然后被拷贝至屏幕。
drawRect方法是UIView进行绘制的方法,没有必要不要调用drawRect方法,因为获取图形上线文,需要消耗更多内存。
调用drawRect: 调用drawRect.png 没调用drawRect: 没调用drawRect.png六. layer层的模型树和呈现树
- layer层有三层树结构:modelLayer Tree(模型树)、presentationLayer Tree(呈现树)、Render Tree(渲染树)。
- modelLayer Tree用来存储数据和值的(怎么验证),presentationLayer Tree用来展示的。
- 做动画的时候是对presentLayer操作,modelLayer还是保持原来的值,可以通过点击事件,来进行确认。
模型树:是用来存储数据的、响应事件
呈现树:用来根据模型树的数据,展示
呈现树:是对模型树的copy
先看结论:如果你添加了一个动画,那么此时呈现树的数据来自于动画,而不是模型树,当动画结束之后,才又会根据模型树的数据展示。如果动画结束了,但是动画不移除,那么呈现树的数据依然来自于动画。
引申:一个runloop循环的时候,会提交一个CATransaction事务,让呈现树根据数据进行展示。
当然,你也可以显性写出CATransaction提交数据展示。
下面验证以上的说法:
- 验证呈现树来自动画
view.layer添加子layer打断点
self.caseLayer = [CALayer layer];
self.caseLayer.frame = CGRectMake(100.f, 100.f, 50.f, 50.f);
self.caseLayer.backgroundColor = [UIColor redColor].CGColor;
self.caseLayer.cornerRadius = 8.f;
self.caseLayer.masksToBounds = YES;
[self.view.layer addSublayer:self.caseLayer];
结果,呈现树没值,如下:
(lldb) po _caseLayer.modelLayer
<CALayer:0x6000018a2d60; position = CGPoint (125 125); bounds = CGRect (0 0; 50 50); allowsGroupOpacity = YES; backgroundColor = <CGColor 0x600003ce0360> [<CGColorSpace 0x600003cf9440> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 0 0 1 )>
(lldb) po _caseLayer.presentationLayer
nil
添加动画到layer:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position"];
anim.duration = 5.f;
anim.toValue = [NSValue valueWithCGPoint:CGPointMake(200.f, 200.f)];
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
[self.caseLayer addAnimation:anim forKey:nil];
}
重新打断点,发现呈现树有值,如果添加动画,验证了呈现树来自动画,如下:
(lldb) po _caseLayer.presentationLayer
<CALayer:0x6000018b2140; position = CGPoint (125 125); bounds = CGRect (0 0; 50 50); masksToBounds = YES; allowsGroupOpacity = YES; cornerRadius = 8; backgroundColor = <CGColor 0x600003ce0360> [<CGColorSpace 0x600003cf9440> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 0 0 1 )>
- 验证动画的时候模型树的值不变
下面通过点击事件验证
viewOne添加点击手势
viewOne = [[UIView alloc] init];
viewOne.backgroundColor = [UIColor redColor];
viewOne.layer.position = CGPointMake( 100.f, 100.f);
viewOne.layer.bounds = CGRectMake(0.f, 0.f, 100.f, 100.f);
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
[viewOne addGestureRecognizer:tapGesture];
[self.view addSubview:viewOne];
点击空白给ViewOne添加动画,动画完成不移除
- (void)tapAction {
NSLog(@"tapAction");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"position"];
anim.duration = 5.f;
anim.toValue = [NSValue valueWithCGPoint:CGPointMake(200.f, 200.f)];
anim.fillMode = kCAFillModeForwards;
anim.removedOnCompletion = NO;
[viewOne.layer addAnimation:anim forKey:nil];
}
效果图如下:
动画结束.png
灰色是原来的位置,红色是动画之后的位置。动画完成,点击红色不打印,点击灰色会打印,如下:
第三期VIP[45306:12357421] tapAction
验证了,动画不会改变模型树的位置。
七. 利用layer.mask获得不规则形状
大图: 大图.png 小图: 小图.png 想要得到如下效果图: 效果图.png代码:
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
imgView = [[UIImageView alloc] initWithFrame:CGRectMake(100.f, 100.f, 200.f, 200.f)];
imgView.image = [UIImage imageNamed:@"bg-mine.png"];
[self.view addSubview:imgView];
//通过layer.mask 来塑造一个不透明控件
UIImage *image = [UIImage imageNamed:@"bubble.png"];
CALayer *imageLayer = [CALayer layer];
imageLayer.contents = (__bridge id)image.CGImage;
imageLayer.frame = imgView.bounds;
imgView.layer.mask = imageLayer;
}