iOS-CAlayer

2019-11-07  本文已影响0人  Imkata
  1. 为什么要有CAlayer?
    ① 在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKit和UIView,但是Mac OS有AppKit和NSView的原因。他们功能上很相似,但是在实现上有着显著的区别。
    ② 绘图,布局和动画,相比之下就是类似Mac笔记本和桌面系列一样应用于iPhone和iPad触屏的概念。把这种功能的逻辑分开并应用到独立的Core Animation框架,苹果就能够在iOS和Mac OS之间共享代码,使得对苹果自己的OS开发团队和第三方开发者去开发两个平台的应用更加便捷

  2. QuartzCore = CoreAnimation
    这个框架的名称感觉不是很清晰,但是看它的头文件可以发现,它其实就是CoreAnimation,这个框架的头文件只包含了CoreAnimation.h,他里面的类大部分都是CA开头的,是动画库。

  3. CoreGraphics
    CoreGraphics 是图形库,并且CoreAnimation中大量用到CoreGraphics中的类,原因是显然的,实现动画自然要用到图形库中的东西。

  4. CALayer是定义在QuartzCore框架中的,CGImageRef、CGColorRef两种数据类型是定义在CoreGraphics框架中的,UIColor、UIImage是定义在UIKit框架中的。

  5. 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;
  1. 用来设置CALayer在父层中的位置
  2. 以父层的左上角为原点(0, 0)

② anchorPoint

@property CGPoint anchorPoint;
  1. 称为“定位点”、“锚点”
  2. 决定着CALayer身上的哪个点会在position属性所指的位置
  3. 以自己的左上角为原点(0, 0)
  4. 它的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).png

viewOne.layer.anchorPoint = CGPointMake(1.f, 1.f);

(1,1).png

viewOne.layer.anchorPoint = CGPointMake(1.f, 0.f);

(1,0).png
  1. anchorPoint是锚点,它的值范围为(0~1),是相对于layer本身的坐标
  2. view的旋转是围绕着锚点来旋转的
  3. postion是相对于父层的坐标,position和anchorPoint实际上指的是同一个点
  4. postion或者anchorPoint的修改,可以改变view的布局
  5. 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的坐标:

  1. 先调用layer层的setFrame,后调用position,最后调用bounds(通过打印的次序以及注释掉 [super setFrame:frame] 得出结论)
  2. 修改frame,本质是修改layer层的position和bounds
  3. 推导出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

验证了上面所说。

二. 非根层的隐式动画

  1. 每一个UIView内部都默认关联着一个CALayer,我们可用称这个Layer为Root Layer(根层)。
  2. 所有的非Root Layer,也就是手动创建的CALayer对象,都存在着隐式动画
  3. 什么是隐式动画?
    当对非Root Layer的部分属性进行修改时,默认会自动产生一些动画效果,这些动画效果就叫隐式动画,而这些属性称为Animatable Properties(可动画属性)
  4. 列举几个常见的Animatable Properties:
    bounds:用于设置CALayer的宽度和高度,修改这个属性会产生缩放动画
    backgroundColor:用于设置CALayer的背景色,修改这个属性会产生背景色的渐变动画
    position:用于设置CALayer的位置,修改这个属性会产生平移动画
其他更多Animatable Properties可根据属性注释是否有Animatable或者参考官方文档,如下: Animatable.png

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都能实现相同的显示效果,那究竟该选择谁好呢?

总结:

  1. 其实,对比CALayer,UIView多了一个事件处理的功能,也就是说,CALayer不能处理用户的触摸事件,而UIView可以
  2. 所以,在选择的过程中,需要考虑到实际的情况,如果显示出来的东西需要跟用户进行交互的话,用UIView;如果不需要跟用户进行交互,用UIView或者CALayer都可以
  3. 当然,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>"
)
  1. UIView可以通过subviews属性访问所有的子视图,类似地,CALayer也可以通过sublayers属性访问所有的子层。
  2. 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>"
)

可以发现:

  1. 如果一个控件是另外一个控件的子控件,那么这个控件的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

注意:

  1. 在自定义layer中 drawInContext:方法不会自己调用,只能自己通过setNeedDisplay方法调用。
  2. 在自定义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显示详细过程:

  1. 当UIView需要显示时,它内部的层会准备好一个CGContextRef(图形上下文),然后调用delegate的drawLayer:inContext:方法,并且传入已经准备好的CGContextRef对象。
  2. 而在drawLayer:inContext:方法中通过[super drawLayer:inContext:]又会调用UIView的drawRect:方法。
  3. 平时在drawRect:中通过UIGraphicsGetCurrentContext()获取的就是由层传入的CGContextRef对象,在drawRect:中完成的所有绘图都会填入层的CGContextRef中,然后被拷贝至屏幕。
通过打断点,观察以下方法调用栈,可以验证我们的说法: 方法调用顺序.png

drawRect方法是UIView进行绘制的方法,没有必要不要调用drawRect方法,因为获取图形上线文,需要消耗更多内存。

调用drawRect: 调用drawRect.png 没调用drawRect: 没调用drawRect.png

六. layer层的模型树和呈现树

  1. layer层有三层树结构:modelLayer Tree(模型树)、presentationLayer Tree(呈现树)、Render Tree(渲染树)。
  2. modelLayer Tree用来存储数据和值的(怎么验证),presentationLayer Tree用来展示的。
  3. 做动画的时候是对presentLayer操作,modelLayer还是保持原来的值,可以通过点击事件,来进行确认。

模型树:是用来存储数据的、响应事件
呈现树:用来根据模型树的数据,展示
呈现树:是对模型树的copy

先看结论:如果你添加了一个动画,那么此时呈现树的数据来自于动画,而不是模型树,当动画结束之后,才又会根据模型树的数据展示。如果动画结束了,但是动画不移除,那么呈现树的数据依然来自于动画。

引申:一个runloop循环的时候,会提交一个CATransaction事务,让呈现树根据数据进行展示。

当然,你也可以显性写出CATransaction提交数据展示。

下面验证以上的说法:

  1. 验证呈现树来自动画

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 )>
  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;
}

Demo地址:CALayer
Demo地址:高级动画1

上一篇 下一篇

猜你喜欢

热点阅读