UIView 的 frame 原理

0x0. 什么是 frame

来看下官方定义:

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

从这句话中可以提取出三个关键信息: 矩形位置大小

也就是说,不管 view 的形状如何,frame 都一定是矩形的。而且,只要 frame 发生了变化,那么 view 的位置或者大小就一定会发生变化。 看似是一句废话,但这句话对接下来的 frame 分析却有一定的帮助。

0x1. frame 的计算方式

// demo1
self.myView = [[UIView alloc] init]; 
self.myView.layer.anchorPoint = CGPointMake(0, 0);
self.myView.frame = CGRectMake(12, 14, 120, 80); 
self.myView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.myView];
1
2
3
4
5
6
// demo2
self.myView = [[UIView alloc] init]; 
self.myView.frame = CGRectMake(12, 14, 120, 80);
self.myView.layer.anchorPoint = CGPointMake(0, 0); 
self.myView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.myView];
1
2
3
4
5
6

仔细观察下这段代码的不同之处。

没错,就是设置 frame 和 anchorPoint 的先后顺序不一样而已。运行后效果图如下:

视图位置不同,说明 frame 是不同的。demo1 为{12, 14, 120, 80},而 demo2 因为在设置完 frame 后改变了 anchorPoint,导致 frame 变成了{72, 54, 120, 80},所以视图位置向右下方偏移。

通过断点调试,可以知道-[UIView setFrame:]内部调用了以下三个方法:

  • -[CALayer setFrame:]
  • -[CALayer setPosition:]
  • -[CALayer setBounds:]
self.myView = [[MyView alloc] init]; 
self.myView.frame = CGRectMake(12, 14, 120, 80); 
self.myView.backgroundColor = [UIColor redColor];
[backView addSubview:self.myView];
1
2
3
4

运行上述代码,在-[CALayer setPosition:]断住,看下相关逻辑:

上图中d8(双精度浮点寄存器) 中的值是 width,d14中的值是 anchorPoint.x,d11中的值是 origin.x,d9中的值是 height,d15中的值是 anchorPoint.y,d10中的值是 origin.y。根据这些值,计算出 position 后,作为 setPosition: 方法的参数。

通过fmadd指令,可以清楚地看到 position、anchorPoint、frame 三者之间的关系:

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

经过一系列测试验证后,发现该等式是成立的。那么对于上述 demo2 为什么改变了 anchorPoint 而 frame 发生变化也就不言而喻了。

frame.origin.x = position.x - anchorPoint.x * frame.size.width 
               = 72 - 0 * 120
               = 72;

frame.origin.y = position.y - anchorPoint.y * frame.size.height
               = 54 - 0 * 80
               = 54;
1
2
3
4
5
6
7

0x2. 带有 transform 的 frame

文章开头提到,frame 表示的是一个矩形框架。那么对 view 应用仿射变换后,frame 则是刚好能包裹住视图的那个矩形。

myView.frame = CGRectMake(50, 50, 100, 100);
myView.transform = CGAffineTransformMakeRotation(M_PI_4); // 顺时针旋转45°
1
2

此时,view 的 bounds 依然是{0, 0, 100, 100},但 frame 却变成了{29.29, 29.29, 141.42, 141.42}

frame.width 和 frame.height 刚好等于 bounds.width 与 bounds.height 平方和的平方根。套用下公式:

// 当然系统可能并非通过这样方式来计算 frame.width 和 frame.height,总感觉有点low
frame.size.width =(100^2 + 100^2) = 141.42; 
frame.size.height =(100^2 + 100^2) = 141.42;
frame.origin.x = 100 - 0.5 * 141.42 = 29.29; 
frame.origin.y = 100 - 0.5 * 141.42 = 29.29; // 锚点默认值为0.5
1
2
3
4
5

0x3. 小扩展

0x3.1 获取 view 的 frame

通过断点调试可以看出,-[UIView frame]方法内部实现仅仅是调用了-[CALayer frame]

也就是说,当获取 view 的 frame 时,其实是返回了 layer 的 frame。

0x3.2 view.center 和 layer.position

view 的center属性跟 layer 的position属性是相对应的,两者是同步变化的。-[UIView setCenter:]方法内部会调用-[CALayer setPosition:],而-[UIView center]其实就是返回了 layer 的position。(只想说,汇编面前没有秘密)

有个值得注意的点,就是 view.center 只有两种情况下会发生变化

  1. 直接改变了 view.center (或改变 layer.position)
  2. 直接改变了 view.frame (或改变 layer.frame)

记住这点,套用公式便可知道下列代码的输出结果了:

self.myView = [[MyView alloc] init];
self.myView.frame = CGRectMake(12, 14, 120, 80);  
// position.x = 12 + 0.5 * 120 = 72
// position.y = 14 + 0.5 * 80 = 54

self.myView.layer.anchorPoint = CGPointMake(0, 0);
// anchorPoint.x = 0
// anchorPoint.y = 0

self.myView.bounds = CGRectMake(0, 0, 100, 60);
// frame.size.width = 100
// frame.size.height = 60
// frame.origin.x = 72 - 0 * 100 = 72 
// frame.origin.y = 54 - 0 * 60 = 54

self.myView.backgroundColor = [UIColor redColor];
[backView addSubview:self.myView];

NSLog(@"frame: %@ center: %@ ", NSStringFromCGRect(self.myView.frame), NSStringFromCGPoint(self.myView.center));
// frame: {72, 54, 100, 60}  center: {72, 54}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
最后更新: 2/1/2021, 5:39:58 PM