CALayer、动画、frame

1.图层的绘制

图层,即CALayer的绘有两种实现方案:

  1. 自己绘制;
  2. 交给代理对象绘制。

1.1.自己绘制

图层自己绘制时,需要重写 -drawInContext 方法,具体步骤为:

  • 调用 CALayer 对象的 setNeedsDisplay 方法触发重绘;
  • 重写 drawInContext: 方法绘制新内容。

#示例1.1:

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
#import "CustomLayer.h"

@implementation CustomLayer

//重写drawInContext
- (void)drawInContext:(CGContextRef)ctx
{
CGContextSetLineWidth(ctx, 2);
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextSetStrokeColorWithColor(ctx, [UIColor whiteColor].CGColor);

//贝塞尔曲线画三角形
UIBezierPath *aPath = [UIBezierPath bezierPath];
aPath.lineWidth = 2.0; //设置线宽
aPath.lineCapStyle = kCGLineCapRound; //线条拐角
aPath.lineJoinStyle = kCGLineCapRound; //终点处理
[aPath moveToPoint:CGPointMake(100, 50)];
[aPath addLineToPoint:CGPointMake(150, 100)];
[aPath addLineToPoint:CGPointMake(50, 100)];
[aPath closePath];

CGContextAddPath(ctx, aPath.CGPath);
CGContextDrawPath(ctx, kCGPathFillStroke);
}
@end

将自定义 CustomLayer 添加到视图上,调用-setNeedsDisplay触发重绘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "CustomLayer.h"
@implementation ViewController

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

CustomLayer *layer = [[CustomLayer alloc] init];
layer.backgroundColor = [UIColor whiteColor].CGColor;
layer.frame = CGRectMake(100, 300, 100, 100);
[self.view.layer addSublayer:layer];
[layer setNeedsDisplay];//调用此方法触发重绘
}
@end

1.2.代理绘制

图层内部没有实现-drawInContext方法时,就需要将绘制任务代理给别的对象。默认情况下,CALayer的代理是其所属的视图。代理给别的对象时,此对象需要实现 CALayerDelegate。

  • 设置 CALayer 的 CALayerDelegate;
  • 调用 CALayer 的-setNeedsDisplay 触发绘制;
  • 在代理方法-drawLayer:inContext:的实现中绘制内容。

#示例1.2:

1
2
3
4
5
#import "CustomLayer.h"

@implementation CustomLayer
//这里不重写-drawInContext:方法
@end

将自定义CALayer添加到视图上,设置代理并调用-setNeedsDisplay触发重绘:

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
#import "CustomLayer.h"
@interface ViewController ()<CALayerDelegate>
@end

@implementation ViewController

- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

CustomLayer *layer = [[CustomLayer alloc] init];
layer.backgroundColor = [UIColor whiteColor].CGColor;
layer.frame = CGRectMake(100, 300, 100, 100);
layer.delegate = self;
[self.view.layer addSublayer:layer];
[layer setNeedsDisplay];//调用此方法触发重绘
}

//CALayerDelegate
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
CGContextAddEllipseInRect(ctx, CGRectMake(0, 0, 50, 50));
CGContextSetRGBFillColor(ctx, 1, 0, 1, 1);
CGContextDrawPath(ctx, kCGPathFillStroke);
}
@end

如果既重写了 CustomLayer 中的-drawInContext:,又实现了 CALayerDelegate,那么最终只会在-drawInContext中进行重绘。

2.视图的绘制

2.1.显示原理

通常,我们会使用UIView等控件来设置需要显示的内容,但实际上,视图本身并不负责内容的显示,甚至动画也不是在视图上执行的。从继承关系上来看,视图都直接或间接的继承自 UIResponder,从设计目标上来看视图的主要作用是处理用户交互。那么,显示和动画的工作由谁来处理呢?答案:CALayer!

CALayer
An object that manages image-based content and allows you to perform animations on that content.

视图内部默认有个.layer属性,这个CALayer对象正是视图显示和动画的幕后主角,视图的绘制本质上来说就是图层的绘制。

1
2
3
4
5
6
/* An object providing the contents of the layer, typically a CGImageRef,
* but may be something else. (For example, NSImage objects are
* supported on Mac OS X 10.6 and later.) Default value is nil.
* Animatable. */

@property(nullable, strong) id contents;

CALayer 中有个contents属性,即图层的内容,是一个位图(CGImageRef),指向一片叫做 backing store(后备存储区)的缓存区。默认情况下,此属性的值为空,它会从后备存储中读取内容并显示。当我们通过视图展示内容时:

  • 使用视图及其子控件,设置好宽高、背景色等内容;
  • runloop即将休眠或退出时,开始视图的绘制,调用 drawRect 方法;
  • drawRect 内通过 CoreGraphics 在 CGContextRef 中绘制内容并保存为位图;
  • 绘制的位图被保存到由 CPU 开辟的一片叫 backing store 的缓存中;
  • 图层的contents会从 backing store 中读取内容;
  • 图层将contents中的位图交给GPU进行合成、转换和渲染;
  • GPU 将渲染结果放入帧缓冲区
  • 视频控制器按照 VSync 信号逐行读取帧缓冲区的数据,经过数模转换传递给显示器展示。

除了设置视图的宽高、背景色来展示内容外,我们也可以直接给 layer.contents 赋值,这种情况下,显示内容时不再从后备存储中读取内容,而是直接使用contents中的位图。

If you are using the layer to display a static image, you can set this property to the CGImageRef containing the image you want to display. Assigning a value to this property causes the layer to use your image rather than create a separate backing store.

2.2.drawRect

一般在自定义一个视图的绘制时,我们需要重写其-drawRect:方法。

#示例2.2:

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
#import "CustomView.h"
@implementation ViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

CustomView *view = [[CustomView alloc] init];
view.backgroundColor = [UIColor whiteColor];
view.frame = CGRectMake(100, 300, 100, 100);
[self.view addSubview:view];
}
@end

#import "CustomView.h"

@implementation CustomView
//自定义视图并重写drawRect
- (void)drawRect:(CGRect)rect
{
//边宽
CGFloat lineWidth = 3.0f;
//半径
CGFloat radius = (CGRectGetWidth(rect) - lineWidth * 2) / 2.0;
//圆心
CGPoint center = CGPointMake(CGRectGetWidth(rect)/2.0, CGRectGetHeight(rect) / 2.0);
//扇形起点
CGFloat startAngle = - M_PI_2;
//根据进度计算扇形结束位置
CGFloat endAngle = startAngle + 0.8 * M_PI * 2;
//根据起始点、原点、半径绘制弧线
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];

CGContextRef ref = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(ref, lineWidth);
CGContextSetFillColorWithColor(ref, [UIColor yellowColor].CGColor);
CGContextSetStrokeColorWithColor(ref, [UIColor blueColor].CGColor);
//从弧线结束为止绘制一条线段到圆心。这样系统会自动闭合图形,绘制一条从圆心到弧线起点的线段。
[path addLineToPoint:center];
CGContextAddPath(ref, path.CGPath);
CGContextDrawPath(ref, kCGPathFillStroke);
}
@end

2.3.调用堆栈

视图重绘

这是调用-drawRect时的堆栈图。当前 runloop 即将休眠或退出时,观察者回调函数中自动调用视图对象中 CALayer 的-display方法。下面是 CALayer.h 中对此方法的描述:

1
2
3
4
5
/* Reload the content of this layer. Calls the -drawInContext: method
* then updates the `contents' property of the layer. Typically this is
* not called directly. */

- (void)display;

-display用来更新图层的内容,其内部会继续调用-drawInContext:并更新contents中的内容。

1
2
3
4
5
6
7
8
9
10
/* Called via the -display method when the `contents' property is being
* updated. Default implementation does nothing. The context may be
* clipped to protect valid layer content. Subclasses that wish to find
* the actual region to draw can call CGContextGetClipBoundingBox(). */

- (void)drawInContext:(CGContextRef)ctx;

/* If defined, called by the default implementation of -drawInContext: */

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
1
2
Draws the layer’s content using the specified graphics context.The default implementation of this method does not do any drawing itself. If the layers delegate implements the drawLayer:inContext: method, that method is called to do the actual drawing.
Subclasses can override this method and use it to draw the layers content. When drawing, all coordinates should be specified in points in the logical coordinate space.

-drawInContext:是图层内绘制内容的地方。默认情况下方法体中什么都不做,如果图层的代理实现了 drawLayer:inContext: 代理方法,那么就由这个方法来实现真正的绘制。示例3中我们没有重写此方法,所以图层会将绘制任务代理给别人。

1
In iOS, if the layer is associated with a UIView object, this property must be set to the view that owns the layer.

默认情况下,视图中根layer的代理是视图对象本身,所以绘制任务会通过代理转交给layer所属的视图。从堆栈图来看,drawLayer:inContext:代理方法内自动调用了视图的 drawRect 方法。也就是说,本来需要由 CALayer 来完成的绘制任务,通过代理转交给了视图的 drawRect,这也正是我们重写视图 drawRect 方法所要做的事情。

调用流程梳理如下:

  1. 将视图添加到界面或者调用视图对象的-setNeedsDisplay方法,触发重绘;
  2. 视图将绘制任务交给内部的.layer处理;
  3. layer 调用 -display 方法;
  4. -display 继续调用-drawInContext绘制内容;
  5. 因为没有重写-drawInContext,CALayer将绘制任务通过代理交给layer所属的视图;
  6. 视图实现-drawLayer:inContext:代理方法,并调用自己的-drarRect:绘制内容;

有个小知识点,-drawLayer:inContext:代理方法最后有个参数ctx,即绘图的上下文。

1
2
3
4
5
6
7
8
9
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context {
UIGraphicsPushContext(context);

CGRect bounds;
bounds = CGContextGetClipBoundingBox(context);
[self drawRect:bounds];

UIGraphicsPopContext();
}

这是博主thinkq的一篇 文章 中帖出来的 drawLayer:inContext: 的实现代码,方法内将layer传递过来的 CGContextRef 压入绘图上下文栈顶。我们在视图的 drawRect 中通过 UIGraphicsGetCurrentContext() 获取的正是此上下文,最后 drawRect 中的绘制任务都会保存在此ctx中。CGContextRef 是 per-thread的,进入子线程中通过 UIGraphicsGetCurrentContext() 获取栈顶的上下文会为空。这时,就需要通过 UIGraphicsBeginImageContext() 创建一个基于位图的上下文并将其设为当前上下文进行绘图。

另外,通过重写视图的 drawRect 方法,使用 CoreGraphic 来绘制内容,这会消耗很大一部分内存,尤其是当你需要不断重绘时。所以,可以考虑使用 CAShapeLayer 来绘制。CAShapeLayer 通过矢量图形而非 bitmap 来绘制图层,所以更节省内存、渲染速度也更快。

2.3.绘制与线程

视图的显示流程被分为绘制渲染两个阶段。绘制的工作是由CPU来处理的,视图的 drawRect 方法或者图层的 drawInContext 方法内,绘制任务默认都是在主线程进行的,当使用 CoreGraphics绘图时,如果内容比较复杂则会导致CPU性能瓶颈从而造成卡顿现象。渲染的工作是由GPU来完成的,当需要渲染的图片过大、有离屏渲染等情况时,渲染不能及时完成,也会造成卡顿现象。所以有时我们需要在异步线程中执行绘图任务,再回到主线程将绘制结果返回给视图或CALayer。在使用 tableviewCell 这种控件时,避免离屏渲染问题。

#示例2.3:

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
- (UIImage*)drawdrawAsynchronousInRect:(CGRect)rect
{
CGFloat scale = [[UIScreen mainScreen] scale];

rect.size.width *= scale;
rect.size.height *= scale;

UIGraphicsBeginImageContext(rect.size);

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextScaleCTM(context, scale, scale);

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return [UIImage imageWithCGImage:image.CGImage scale:scale orientation:UIImageOrientationUp];
}

//调用示例
UIImageView *imageView;
NSOperationQueue *drawQueue = [[NSOperationQueue alloc] init];
[drawQueue addOperationWithBlock:^{
//异步队列中绘图
UIImage *image = [self drawdrawAsynchronousInRect:rect];
//回到主线程设置图片
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[imageView setImage:image];
}];
}];

3.图层与动画

3.1.隐式动画

当我们修改了图层的可动画属性时,属性并不会立刻显示最终结果,而是有一个平滑的过渡效果,持续0.25秒,这就是图层的隐式动画。这个过程中,我们没有明确指定动画的类型,仅仅是改变了某个属性。

#示例3.1.1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import "CustomLayer.h"
@interface ViewController ()
@property (nonatomic, strong) CustomLayer *mSublayer;
@end

@implementation ViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

_mSublayer = [[CustomLayer alloc] init];
_mSublayer.backgroundColor = [UIColor whiteColor].CGColor;
_mSublayer.frame = CGRectMake(100, 300, 100, 100);
[self.view.layer addSublayer:_mSublayer];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGAffineTransform transform = _mSublayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
_mSublayer.affineTransform = transform;
}

示例中我们只是对刚添加的图层的affineTransform属性值做了修改,运行点击界面后图层会有一个0.25秒的旋转动画。

隐式动画的背后其实是Core Animation在默默处理动画事务:当我们修改了图层的可动画属性时,Core Animation 会通过事务将我们的属性变化包裹起来,即包含在CATransactionbegincommit中;事务被保存在一个栈结构中,当隐式动画被提交之后,它就被保存在栈顶的事务中;runloop的循环中会自动开始栈顶的这个事务,从而执行动画过程。

我们可以试着将上面执行动画部分的代码稍作修改,加入事务的语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//创建事务
[CATransaction begin];
//自定义动画时长
[CATransaction setAnimationDuration:2.0f];
//自定义动画结束后的处理逻辑
[CATransaction setCompletionBlock:^{
_mSublayer.backgroundColor = [UIColor redColor].CGColor;
}];
CGAffineTransform transform = _mSublayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
_mSublayer.affineTransform = transform;
//[CATransaction commit];
}

运行后可以看到,如果不commit,动画根本不会执行~另外,可以看到可变属性的动画时长和动画后的处理语句都可以自定义,这跟下面两种UIView的动画方式类似:

1
2
3
4
5
6
7
8
//1
[UIView beginAnimations:@"x" context:nil];
//animation here
[UIView commitAnimations];
//2
[UIView animateWithDuration:2.0f animations:^{
//animation here
}];

实际上这两种方式的内部都是通过事务来实现的,动画的创建和提交分别调用了CATransactionbegincommit

需要注意的是,隐式动画只在我们直接创建的图层上有效,UIView中根图的隐式动画默认是关闭的,对UIView中根图层做动画时,不会有动画效果:

#示例3.1.2:

1
2
3
4
5
6
7
8
9
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
[CATransaction begin];
[CATransaction setAnimationDuration:2.0f];
CGAffineTransform transform = self.view.layer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
//修改 self.view 中根图层的可变属性
self.view.layer.affineTransform = transform;
[CATransaction commit];
}

示例中我们对self.view.layer.affineTransform做了修改,并且通过事务设置了动画时长为2秒,但运行后可以看到并没有动画效果,而是直接变到目标值。

那么,CALayer 是如何确定自己是否执行隐式动画的呢?我们对#示例3.1.1中的代码稍作修改:

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
#import "CustomLayer.h"
@interface ViewController ()<CALayerDelegate>//修改1:声明图层代理
@property (nonatomic, strong) CustomLayer *mSublayer;
@end

@implementation ViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

_mSublayer = [[CustomLayer alloc] init];
//修改2:指定图层的代理为当前VC
_mSublayer.delegate = self;
_mSublayer.backgroundColor = [UIColor whiteColor].CGColor;
_mSublayer.frame = CGRectMake(100, 300, 100, 100);
[self.view.layer addSublayer:_mSublayer];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
CGAffineTransform transform = _mSublayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
_mSublayer.affineTransform = transform;
}

//修改3:实现代理
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event{
NSLog(@"++++属性:%@",event);
return nil;
}

示例中有三处修改,都是关于CALayerDelegate的。运行后点击界面触发隐式动画,下面是动画触发时的堆栈信息:

actionForLayer

可以看到,动画提交后,CALayer 调用了其actionForKey方法,试图返回一个实现了CAAction协议的对象,参数event对应的正是我们修改的可动画属性。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/** Action methods. **/

/* An "action" is an object that responds to an "event" via the
* CAAction protocol (see below). Events are named using standard
* dot-separated key paths. Each layer defines a mapping from event key
* paths to action objects. Events are posted by looking up the action
* object associated with the key path and sending it the method
* defined by the CAAction protocol.
*
* When an action object is invoked it receives three parameters: the
* key path naming the event, the object on which the event happened
* (i.e. the layer), and optionally a dictionary of named arguments
* specific to each event.
*
* To provide implicit animations for layer properties, an event with
* the same name as each property is posted whenever the value of the
* property is modified. A suitable CAAnimation object is associated by
* default with each implicit event (CAAnimation implements the action
* protocol).
*
* The layer class also defines the following events that are not
* linked directly to properties:
*
* onOrderIn
* Invoked when the layer is made visible, i.e. either its
* superlayer becomes visible, or it's added as a sublayer of a
* visible layer
*
* onOrderOut
* Invoked when the layer becomes non-visible. */

/* Returns the default action object associated with the event named by
* the string 'event'. The default implementation returns a suitable
* animation object for events posted by animatable properties, nil
* otherwise. */

+ (nullable id<CAAction>)defaultActionForKey:(NSString *)event;

/* Returns the action object associated with the event named by the
* string 'event'. The default implementation searches for an action
* object in the following places:
*
* 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
*
* If any of these steps results in a non-nil action object, the
* following steps are ignored. If the final result is an instance of
* NSNull, it is converted to `nil'. */

- (nullable id<CAAction>)actionForKey:(NSString *)event;

/* A dictionary mapping keys to objects implementing the CAAction
* protocol. Default value is nil. */

@property(nullable, copy) NSDictionary<NSString *, id<CAAction>> *actions;

@protocol CALayerDelegate <NSObject>
//...略

/* If defined, called by the default implementation of the
* -actionForKey: method. Should return an object implementing the
* CAAction protocol. May return 'nil' if the delegate doesn't specify
* a behavior for the current event. Returning the null object (i.e.
* '[NSNull null]') explicitly forces no further search. (I.e. the
* +defaultActionForKey: method will not be called.) */

- (nullable id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
@end

CAAction可以视为图层隐式动画的行为。动画提交后图层会通过-actionForKey:方法寻找一个CAAction对象,寻找的过程如下:

  • 先尝试通过 CALayerDelegate 的 -actionForLayer:forKey: 代理方法返回一个 CAAction 对象;
  • 如果代理方法返回 null,则停止寻找,属性的修改不执行任何动画,直接更新到目标值;
  • 如果代理方法返回 nil,则继续从 layer 的 actions 字典中查找 CAAction 对象;
  • 如果 actions 字典中没有包含对应的属性,则图层继续在它的 style 字典中搜索属性名;
  • 如果在 style 里也找不到,则从 +defaultActionForKey: 方法中返回属性对应的默认行为对象;

上面的搜索过程中,-actionForLayer:forKey:如果返回null对象,则不执行动画;如果返回nil则会执行隐式动画。如果返回CAAction协议对象,则由 Core Animation 创建对应的动画。

上面提到的对视图的根图层的可动画属性进行修改时,并不会触发隐式动画,实际上就是因为视图默认是其根图层的代理,-actionForLayer:forKey:返回了一个null对象。你可以尝试让当前 VC 实现self.view.layer的代理并返回nil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController ()<CALayerDelegate>
@end

@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//设置代理
self.view.layer.delegate = self;
[CATransaction begin];
[CATransaction setAnimationDuration:2.0f];
CGAffineTransform transform = self.view.layer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
//修改 self.view 中根图层的可变属性
self.view.layer.affineTransform = transform;
[CATransaction commit];
}

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event{
NSLog(@"++++属性:%@",event);
return nil;
}
@end

再运行并点击界面后可以看到,self.view.layer这次执行了隐式动画!

3.2.图层树

和视图一样图层也有树形结构,称为“图层树”,即展示树模型树

  • 展示树
1
- (instancetype)presentationLayer;

Returns a copy of the presentation layer object that represents the state of the layer as it currently appears onscreen.

Discussion
The layer object returned by this method provides a close approximation of the layer that is currently being displayed onscreen. While an animation is in progress, you can retrieve this object and use it to get the current values for those animations.
The sublayers, mask, and superlayer properties of the returned layer return the corresponding objects from the presentation tree (not the model tree). This pattern also applies to any read-only layer methods. For example, the hitTest: method of the returned object queries the layer objects in the presentation tree.

当 layer 处于动画状态时,展示树会返回一个当前正在展示中的 layer,其属性值都是当前运动状态中对应的值,动画过程中会不断变化。

注意:展示树只有当图层展示在界面上之后才会有值,在此之前调用此树会返回nil

  • 模型树
1
- (instancetype)modelLayer;

Returns the model layer object associated with the receiver, if any.

Discussion
Calling this method on a layer in the presentation tree returns the corresponding layer object in the model tree. This method returns a value only when a transaction involving changes to the presentation layer is in progress. If no transaction is in progress, the results of calling this method are undefined.

呈现图层上调用–modelLayer将会返回它正在呈现所依赖的 CALayer。通常在一个图层上调用-modelLayer会返回self

关于这两个树的应用,可以移步到事件响应者链 & 传递链这篇文章中的抢红包的示例~

4.位置相关属性

4.1.frame

The frame rectangle, which describes the view’s location and size in its superview’s coordinate system.

frame,表示视图在其父视图坐标系中的位置和大小。这里的位置frame.origin(x,y)即视图的左上角,它参照的是父视图的坐标系原点。改变frame的值只会影响到视图本身,其子视图的相对位置不变。

4.2.bounds

The bounds rectangle, which describes the view’s location and size in its own coordinate system.

bounds,表示视图在其自身坐标系中的位置和大小。默认情况下,bounds = (0, 0, frame.size.width, frame.size.heigth),即:

  • bounds.origin默认为 (0, 0);
  • bounds.size = frame.size;

其中bounds.origin(x,y)参照的是视图自身的坐标系原点,修改bounds.origin不会改变视图本身的位置,而是改变其自身的坐标系原点;因为子视图的位置参照的是父视图坐标系的原点,所以修改视图的bounds.origin最终影响的是其子视图的位置。具体表现是:

  • bounds.origin(x,y)相当于当前视图的坐标系原点与视图左上角之间带向量的间距;
  • 默认情况下,视图本身的坐标系原点与视图左上角重合,即bounds.origin(x,y) = (0, 0);
  • origin.x为正值时,间距在x轴上拉大,因为修改origin不影响视图本身的位置,所以视图左上角位置不变,只能让视图坐标系原点向左移动;此时子视图frame和bounds的数值都没变,但受父视图坐标系原点左移的影响,子视图本身也会跟着父视图的原点左移;
  • origin.y为正值时,间距在y轴上拉大,因为视图左上角不变,所以视图坐标系原点会向上移动;子视图frame和bounds都不变,但它的实际位置会跟着父视图的原点向上移;

#示例:

bounds

上图中,视图A是父视图,视图B是A的子视图。

  • 粉色背景中视图A.bounds.origin = (0,0);视图B.bounds.origin = (0, 0);
  • 绿色背景中视图A.bounds.origin被修改为(50,0),所以视图A自身的坐标系原点会向左移动50;
  • 视图B是视图A的子视图,所以视图B会跟着视图A的坐标系原点向左移动50;

结论:修改bounds.origin会影响到子视图参照的坐标系原点;修改bounds.size会影响到视图本身的大小;

4.3.anchorPoint

1
2
3
4
5
6
7
/* Defines the anchor point of the layer's bounds rect, as a point in
* normalized layer coordinates - '(0, 0)' is the bottom left corner of
* the bounds rect, '(1, 1)' is the top right corner. Defaults to
* '(0.5, 0.5)', i.e. the center of the bounds rect. Animatable. */

@property CGPoint anchorPoint;

翻译过来是锚点,这是一个CGPoint类型的属性,可以理解为在 layer 自身坐标系中的一个点。锚点在x轴和y轴上数值的变化范围均为[0~1],例如(0,0)表示 layer 的左上角,(0.5,0.5)表示 layer 的中心点,(1,1)表示 layer 的右下角。锚点的默认值为(0.5,0.5),即 layer 的中心点。直接修改锚点的值会触发隐式动画。

4.4.position

1
2
3
4
5
/* The position in the superlayer that the anchor point of the layer's
* bounds rect is aligned to. Defaults to the zero point. Animatable. */

@property CGPoint position;

此属性也是CGPoint类型,表示一个点,即 layer 的锚点在其父图层中对应的点,也就是说anchorPointposition两个点是重合的,只不过前者在自身坐标系统内,后者在父图层内。

为了方便理解,你可以将 layer 想象成一张便签纸,anchorPoint是用来固定便签纸的一枚图钉,图钉可以扎在便签纸自身范围内的任何位置上;position表示黑板上的一个点,便签纸会被图钉钉在黑板上,而钉在黑板上的这个点就是position

#示例:

bounds = (0, 0, 50, 50)position = (0, 0)anchorPoint = (0,0),效果如下:

效果图1

bounds = (0, 0, 50, 50)position = (0, 0)anchorPoint = (0.5,0.5),效果如下:

效果图2

bounds = (0, 0, 100, 100)position = (0, 0)anchorPoint = (0.5,0.5),效果如下:

效果图3

bounds = (0, 0, 50, 50)position = (0, 0)anchorPoint = (0,0)affineTransform = (M_PI / 4.0);效果如下:

效果图4

结论:CALayer 在其superlayer上的frame是由其自己的boundspositionanchorPointaffineTransform等属性共同决定的。


相关参考:

#©Apple-CALayer

#©AppleDev-CAShapelayer

#©thinkq-关于drawRect

#©catsmen-隐式动画和显式动画


CALayer、动画、frame
https://davidlii.cn/2017/12/29/draw.html
作者
Davidli
发布于
2017年12月29日
许可协议