因为最近在制作自己的独立产品,我学习了一些 iOS 平台的知识。在我的产品界面中,有一个侧边栏需要单独绘制自定义外观,所以这几天我就研究了一下 UIView 的绘制机理以及 Core Animation 中 CALayer 的相关的知识。在 CALayer 中有一个我比较困惑的地方:position 和 anchorPoint 之间的关系,以及它们对于动画、UIView 绘制的过程会作出何种影响。在看了一遍官方文档,搜索了一些相关博客文章、帖子后,我有了较为完整的理解。

position、anchorPoint 的定义及属性

要理解这两个属性,首先要理解 CALayer 中的坐标体系。在 CALayer 系统中,存在两种形式的坐标体系——基于 Point 的和基于 Unit 的:

Point-based coordinates: Point-based coordinates are used when specifying values that map directly to screen coordinates or must be specified relative to another layer.


Unit-based coordinates: Unit coordinates are used when the value should not be tied to screen coordinates because it is relative to some other value. For example, the layer’s anchorPoint property specifies a point relative to the bounds of the layer itself, which can change. You can think of the unit coordinates as specifying a percentage of the total possible value. Every coordinate in the unit coordinate space has a range of 0.0 to 1.0. For example, along the x-axis, the left edge is at the coordinate 0.0 and the right edge is at the coordinate 1.0.

Point-based coordinates 实际上就是 UIView 中的 bounds、center、frame 使用的坐标体系。Unit-based coordinates 中的坐标点、长度会根据屏幕、设备的不同,对应到不同的 Point-based coordinates 中的坐标点、长度。

下面正式介绍 position 和 anchorPoint。根据苹果的官方文档的描述,它们的定义如下:

The position property defines the location of the layer relative to its parent’s coordinate system.


The anchor point represents the point from which certain coordinates originate.

本质上,position 和 anchorPoint 都是其所属的 layer 上的同一个点,在 layer 上、以及 layer 所属的 UIView 对象上进行的动画会围绕该点进行。它们都是 CAPoint 对象,拥有 x、y 属性来表达不同轴上的位置。不同的地方在于,position 是基于 Point 单位,相对于 layer 的 super layer 的 bounds 来定义的;而 anchorPoint 则是基于 Unit 单位,相对于 layer 本身的 bounds 来定义的。尽管坐标单位和相对坐标系不一样,position 和 anchorPoint 最终都在屏幕上代表着同一个点。“最终一致”的意思是说,如果在下一次重绘之前,它们的属性被改变了的话,那么在重绘之后,layer 会被系统重新绘制调整位置后,保证它们还是在屏幕上的同样的一个点。之所以系统能够保证这一点,是因为系统可以调整 layer 的 frame.origin,从而保证在调整后,根据新的 frame.origin 配合 anchorPoint 计算出来的在 super layer bounds 中的点和 position 是一样的。

在默认情况下,独立的 CALayer 对象的 position 是 (0.0, 0.0),这点在 Apple 的 CALayer Reference 中有提到:

For new standalone layers, the default position is set to (0.0, 0.0).

独立的意思是说,该 CALayer 对象不从属于任何 UIView 对象。CALayer 对象的 anchorPoint 会被初始化为 (0.5, 0.5)。在从属于 UIView 对象的 CALayer 对象中,position 也会根据该 anchorPoint 计算出来的自己相应的值,而不再是 (0.0, 0.0):

The default value of this property (anchorPoint) is (0.5, 0.5), which represents the center of the layer’s bounds rectangle.

下面的代码段可以证明我的观点:

1
2
3
4
5
6
7
CALayer *standaloneLayer = [[CALayer alloc] init];
NSLog(@"Standalone layer's position: x is %f, y is %f.", standaloneLayer.position.x, standaloneLayer.position.y);
NSLog(@"Standalone layer's anchorPoint: x is %f, y is %f.", standaloneLayer.anchorPoint.x, standaloneLayer.anchorPoint.y);

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
NSLog(@"View's layer's position: x is %f, y is %f.", view.layer.position.x, view.layer.position.y);
NSLog(@"View's layer's anchorPoint: x is %f, y is %f.", view.layer.anchorPoint.x, view.layer.anchorPoint.y);

上述代码执行后的输出如下:

1
2
3
4
5
Standalone layer's position: x is 0.000000, y is 0.000000.
Standalone layer's anchorPoint: x is 0.500000, y is 0.500000.

View's layer's position: x is 100.000000, y is 100.000000.
View's layer's anchorPoint: x is 0.500000, y is 0.500000.

position 和 anchorPoint 的关系

改变 position 或 anchorPoint 都不会影响到另一方,但是都会影响其所属 layer 的 frame.origin 属性。这个设计尽管比较奇怪,但是却最终能够保证 position 和 anchorPoint 的一致性。

根据彻底理解 position 与 anchorPoint 这篇文章的观点,我们可以知道这三个属性之间的计算关系如下:

1
2
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;  
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;

在每次修改 position 或者 anchorPoint 后,系统都会根据上述公式来变更 CALayer 对象的 frame.origin,以保证两个属性的一致性。下面的代码段可以佐证我的观点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
NSLog(@"View's layer's frame.origin: x is %f, y is %f.", view.layer.frame.origin.x, view.layer.frame.origin.y);
NSLog(@"View's layer's position: x is %f, y is %f.", view.layer.position.x, view.layer.position.y);
NSLog(@"View's layer's anchorPoint: x is %f, y is %f.", view.layer.anchorPoint.x, view.layer.anchorPoint.y);

view.layer.anchorPoint = CGPointMake(0, 0);
NSLog(@"View's layer's frame.origin: x is %f, y is %f.", view.layer.frame.origin.x, view.layer.frame.origin.y);
NSLog(@"View's layer's position: x is %f, y is %f.", view.layer.position.x, view.layer.position.y);
NSLog(@"View's layer's anchorPoint: x is %f, y is %f.", view.layer.anchorPoint.x, view.layer.anchorPoint.y);

view.layer.position = CGPointMake(50, 50);
NSLog(@"View's layer's frame.origin: x is %f, y is %f.", view.layer.frame.origin.x, view.layer.frame.origin.y);
NSLog(@"View's layer's position: x is %f, y is %f.", view.layer.position.x, view.layer.position.y);
NSLog(@"View's layer's anchorPoint: x is %f, y is %f.", view.layer.anchorPoint.x, view.layer.anchorPoint.y);

上述代码段的输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
View's layer's frame.origin: x is 0.000000, y is 0.000000.
View's layer's position: x is 100.000000, y is 100.000000.
View's layer's anchorPoint: x is 0.500000, y is 0.500000.

View's layer's frame.origin: x is 100.000000, y is 100.000000.
View's layer's position: x is 100.000000, y is 100.000000.
View's layer's anchorPoint: x is 0.000000, y is 0.000000.

View's layer's frame.origin: x is 50.000000, y is 50.000000.
View's layer's position: x is 50.000000, y is 50.000000.
View's layer's anchorPoint: x is 0.000000, y is 0.000000.

我们可以看到在上述代码执行的过程中,position 和 anchorPoint 都没有被系统改变,只有 frame.origin 被系统所改变。而且,在 frame.origin 改变后,position 和 anchorPoint 在 super layer 的坐标系中又重新指代同一个点了。

更直观的例子

进一步地,我们可以在更形象的例子中去展示这个过程。

下面这个例子演示了一个绿色 UIView 被缩放的过程。在这个例子中,我们可以看到,动画发生的位置总是围绕着 anchorPoint/position 进行,而且系统会在绘制的过程中保证最终两点的重合——这表现为,在动画发生前绿色方块的位置被重置了。例子的运行效果如下:

例子的代码如下:

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
//
// ViewController.m
// FacebookPopPlay
//
// Created by winiex on 14/11/10.
// Copyright (c) 2014年 winiex. All rights reserved.
//

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) UIView *greenView;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.greenView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 180, 180)];
self.greenView.backgroundColor = [UIColor greenColor];

//点击一次后,动画开始
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onTapOnce)];
recognizer.numberOfTapsRequired = 1;
[self.greenView addGestureRecognizer:recognizer];
[self.view addSubview:self.greenView];
}

- (void)onTapOnce {
NSLog(@"anchorPoint: x is %f, y is %f", self.greenView.layer.anchorPoint.x, self.greenView.layer.anchorPoint.y);
NSLog(@"position: x is %f, y is %f", self.greenView.layer.position.x, self.greenView.layer.position.y);
NSLog(@"frame.origin: x is %f, y is %f", self.greenView.layer.frame.origin.x, self.greenView.layer.frame.origin.y);
//动画执行的相关代码
self.greenView.layer.anchorPoint = CGPointMake(0, 0);

//如果加上下面这行代码的话,greenView 就不会在动画开始前“跳跃了”
//self.greenView.layer.position = CGPointMake(100, 100);

NSLog(@"anchorPoint: x is %f, y is %f", self.greenView.layer.anchorPoint.x, self.greenView.layer.anchorPoint.y);
NSLog(@"position: x is %f, y is %f", self.greenView.layer.position.x, self.greenView.layer.position.y);
NSLog(@"frame.origin: x is %f, y is %f", self.greenView.layer.frame.origin.x, self.greenView.layer.frame.origin.y);

[UIView animateWithDuration:2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.greenView.transform = CGAffineTransformMakeScale(0.1, 0.1);
} completion:^(BOOL finished) {

}];
}

@end

例子的控制台输出结果为:

1
2
3
4
5
6
anchorPoint: x is 0.500000, y is 0.500000
position: x is 190.000000, y is 190.000000
frame.origin: x is 100.000000, y is 100.000000
anchorPoint: x is 0.000000, y is 0.000000
position: x is 190.000000, y is 190.000000
frame.origin: x is 190.000000, y is 190.000000

当我们加上“self.greenView.layer.position = CGPointMake(100, 100);”这一行代码后,例子最终运行的效果如下:

动画的效果一下子变的自然了。而且,很明显,绿色方块的形变是根据新的 anchorPoint/position 在进行。

小问题

在下图中,黑块是绿块的 sublayer,长、宽都是绿块的 1/4,其 position 处在绿块的正中心。怎样调整黑块的 anchorPoint 来完成下图的布局呢?

总结

根据前文的描述,对于 CALayer 对象的 position 和 anchorPoint 的相关知识总结如下:

  1. position 和 anchorPoint 都是其所属的 layer 上的同一个点;
  2. position 针对 CALayer 对象的 super layer 而言,anchorPoint 针对 CALayer 对象本身而言;
  3. position 使用基于 Point 的坐标系,anchorPoint 使用基于 Unit 的坐标系;
  4. 系统通过变更 CALayer 对象的 frame.origin 来保证 position 与 anchorPoint 为同一个点。