本文部分翻译自苹果官方文档 Anatomy of a Constraint。
一、简述
什么是自动布局
自动布局,定义为一系列线性方程。每个约束代表一个方程。你的目标是声明一系列方程,它们只有一个可能的解。
示例方程如下所示:
此约束规定红色视图的前沿必须在蓝色视图的后沿之后 8.0 pt。它的方程部分如下:
- Item1:等式中的第一项——在本例中为红色视图。该项必须是 view 或 layout guide 。
- Attribute 1:要约束在第一项上的属性——在本例中,是红色视图的 leading 。
- Relationship:他左右两边的关系。该关系可以具有以下三个值之一:==、>=、<=。
- Multiplier:属性 2 的值乘以这个浮点数。在本例中,乘数为 1.0。
- Item 2:等式中的第二项——在本例中为蓝色视图。与第一项不同,此项可以留空(当且仅当约束属性是width 或 height)。
- Attribute 2:要约束在第二个项目上的属性——在本例中,是蓝色视图的 trailing 。如果第二项留空,则这必须是 Not an Attribute。
- Constant:一个恒定的浮点偏移量——在本例中为 8.0。该值被添加到属性 2 的值中。
小结:
- 大多数约束定义了我们用户界面中两个 Item 之间的关系。这些Item可以代表 view 或 layout guide。
- 约束还可以定义单个 Item 的两个不同属性之间的关系,例如,设置项目的高度和宽度之间的纵横比。
- 您还可以为 Item 的高度或宽度分配常量值。使用常量值时,第二项留空,第二个属性设置为 Not An Attribute,乘数设置为 0.0
- 有关属性的完整列表,请参阅 NSLayoutAttribute 枚举。属性总共分为两大类:
- 尺寸属性。例如,高度和宽度。尺寸属性用于指定项目的大小,而没有任何位置指示。
- 位置属性。例如,Leading、Left 和 Top。位置属性用于指定项目相对于其他事物的位置。但是,它们没有表明物品的大小。
- 对于具体属性的值的解释,请参阅官方文档 值解释 章节。
思考
UIView *blueView = UIView.new;
blueView.backgroundColor = UIColor.blueColor;
blueView.layer.borderColor = UIColor.blackColor.CGColor;
blueView.layer.borderWidth = 2;
[superview addSubview:blueView];
UIView *blueSubView = UIView.new;
blueSubView.backgroundColor = UIColor.redColor;
blueSubView.layer.borderColor = UIColor.blackColor.CGColor;
blueSubView.layer.borderWidth = 2;
[blueView addSubview:blueSubView];
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(0);
make.left.equalTo(superview).offset(20);
make.width.equalTo(200);
make.height.equalTo(200);
}];
[blueSubView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(blueView.mas_top);
make.left.equalTo(blueView.mas_left);
make.width.equalTo(100);
make.height.equalTo(100);
}];
疑问:如上文代码,我们使用Masonry,设置一个子视图(blueSubView)对齐父视图(blueView)的 top 和 left 。这里 blueView 的 mas_left ,究竟是怎么计算的?如果简单理解为 blueView.frame.origin.x,那么此例中, blueView 在 superview 的坐标系下,x 应该为20。则 blueSubView 的 left 应该被设置为20?我们当然知道,最终的展示样式,blueSubView.frame.origin.x 的值为0,不为20,为什么?这里方程等式的坐标系,究竟是 superview 的坐标系?blueView 的坐标系?blueSubView 的坐标系?亦或是整个window或屏幕的坐标系?
带着上述疑问,我们继续向后学习。
二、 属性兼容
这些方程可用的各种属性使您可以创建许多不同类型的约束。您可以定义视图之间的空间、对齐视图的边缘、定义两个视图的相对大小,甚至定义视图的纵横比。但是,并非所有属性都兼容。这里主要有以下几条规则:
- 不能将尺寸属性限制为位置属性。eg:
make.width.equalTo(blueView.mas_left);
这样写是不符合规范的。这里同样留一个疑问,如果这样写了,会有什么效果?为什么? - 不能将常量值分配给位置属性。否则会造成 crash 。这里 Masonry 自动帮我们处理了,如果不是尺寸属性且没有赋值 secondViewAttribute,自动帮我们处理为 self.firstViewAttribute.view.superview 和 firstLayoutAttribute。eg:
make.top.equalTo(10) 等价于 make.top.equalTo(superView.mas_top).offset(10)
。 - 不能将非恒等乘数(即1.0 以外的值)与位置属性一起使用。eg:
make.left.equalTo(blueView.mas_left).multipliedBy(2);
这样写是不符合规范的。疑问,如果这样写了,会有什么效果?熟悉 Masonry 的同学应该知道,Masonry 可以处理视图数组,固定宽高不固定间隔来设置约束,里面的实现,make.right.equalTo(tempSuperView).multipliedBy(i/((CGFloat)self.count-1)).with.offset(offset);
这里就使用了非恒定乘数,这里的效果是怎样的?为什么会有这种效果? - 对于位置属性,不能将垂直属性约束为水平属性。eg :
make.left.equalTo(blueView.mas_top);
- 对于位置属性,不能将 leading 或 trailing 属性限制为 left 或 right 属性。eg :
make.leading.equalTo(blueView.mas_left);
。- Leading、trailing:对于从左到右的布局方向,值会随着您向右移动而增加。对于从右到左的布局方向,值会随着您向左移动而增加。换言之,会随着语言的阅读方向,自动适应。当语音为从右向左读时,Leading在右边,trailing在左边,且从右向左增大。
- Left、Right:当您向右移动时,值会增加。苹果官方推荐使用 Leading、trailing,不要使用 Left、Right。这个在做国际版等多语言适配时有显著效果。
这里又抛出了几个疑问,我们继续向后学习。
三、 方程相等
请务必注意, 约束方程中显示的等式表示相等,而不是赋值。
当 Auto Layout 求解这些方程时,它不只是将右侧的值分配给左侧。相反,它计算属性 1 和属性 2 的值以使关系成立。这意味着我们通常可以自由地重新排序等式中的项目。例如:
Button_2.leading = 1.0 * Button_1.trailing + 8.0 //等价于下面的写法
Button_1.trailing = 1.0 * Button_2.leading - 8.0
// 上述方程,用 Masonry 写出来,代码如下,这两行代码的作用是等价的,换句话说,只要写任意一个即可,不需要写两遍。
[Button_2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(Button_1.mas_trailing).offset(8);
}];
[Button_1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.trailing.equalTo(Button_2.mas_leading).offset(-8);
}];
View_1.height = 2.0 * View_2.height + 10.0 //等价于
View_2.height = 0.5 * View_1.height - 5.0
// 同样用 Masonry 写出来,代码如下,原理同上,只要写任意一个即可,不需要写两遍。
[View_1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.equalTo(View_2.mas_height).multipliedBy(2).offset(10);
}];
[View_2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.equalTo(View_1.mas_height).multipliedBy(0.5).offset(-5);
// make.height.equalTo(View_1.mas_height).dividedBy(2).offset(-5);
}];
四、添加约束的视图
如上图的示例方程中,被约束的 item1(视图1) 和 item2(视图2),究竟应该将约束加在哪个 View 上?Masonry 中,当我们调用 [view mas_makeConstraints:] 方法时,约束是被添加在view上的吗?思考下面的代码:
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:blueSubView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:blueView
attribute:NSLayoutAttributeLeft
multiplier:2
constant:0];
layoutConstraint.priority = UILayoutPriorityRequired;
// 下面的3行代码,会将 layoutConstraint 加在不同的 view 上,会有什么不同的效果?
[blueView addConstraint:layoutConstraint];
// [blueSubView addConstraint:layoutConstraint];
// [superview addConstraint:layoutConstraint];
[blueSubView mas_makeConstraints:^(MASConstraintMaker *make) {
// 下面这段代码生成的约束,Masonry会帮我们自动添加在哪个View上?blueSubView 或 blueView 或 其他view?
make.height.equalTo(blueView.mas_height).multipliedBy(0.5).offset(-5);
// 下面这个约束,和上面的约束,会添加在同一个View上吗?
make.width.equalTo(otherView.mas_width).multipliedBy(0.5).offset(-5);
}];
五、 总结
到此为止,小伙伴们是否已经被上文提出的疑问搅得一头雾水?现在就一一回答。先看下面的代码
@implementation MASExampleBasicView
- (id)init {
self = [super init];
if (!self) return nil;
UIView *blueView = UIView.new;
blueView.backgroundColor = UIColor.blueColor;
blueView.layer.borderColor = UIColor.blackColor.CGColor;
blueView.layer.borderWidth = 2;
[self addSubview:blueView];
UIView *blueSubView = UIView.new;
blueSubView.backgroundColor = UIColor.redColor;
blueSubView.layer.borderColor = UIColor.blackColor.CGColor;
blueSubView.layer.borderWidth = 2;
[blueView addSubview:blueSubView];
UIView *superview = self;
int padding = 30;
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(30);
make.left.mas_equalTo(superview).offset(padding);
make.width.mas_equalTo(200);
make.height.mas_equalTo(200);
}];
[blueSubView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(0);
// make.left.mas_equalTo(blueView).multipliedBy(2); //这里这条约束的写法,等价于下面的系统调用
make.width.mas_equalTo(100);
make.height.mas_equalTo(100);
}];
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:blueSubView
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:blueView
attribute:NSLayoutAttributeLeft
multiplier:2
constant:0];
layoutConstraint.priority = UILayoutPriorityRequired;
// [superview addConstraint:layoutConstraint];
[blueView addConstraint:layoutConstraint];
return self;
}
@end
如上文代码所示,Masonry的逻辑,会将约束添加在 view1 和 view2 所在视图层级中,最近的父视图层级上。上文中, view1是 blueSubView 、view2是 blueView,因为 blueSubView 就是 blueView 的子视图,所以他们两个最近的父视图层级,就是 blueView。所以 Masonry 会将约束添加到 blueView 上,即等价于下面的系统调用api。
我们修改上述代码,将 [blueView addConstraint:layoutConstraint];
注释掉,改为 [superview addConstraint:layoutConstraint];
也就是说将约束修改为添加到 superview 上。这两种方案,效果分别如下
将上述的约束,抽象为方程,即为:
blueSubView.left = blueView.left * 2 + 0
这里我们可以看到,方程没有变化,添加约束的视图变化,效果也跟随产生了变化。这是因为,添加约束的视图,决定了方程计算时,值的坐标系。
- 当我们将约束添加在 blueView 上时,此时 blueView 的 left,相对于他自身的坐标系,是0,所以这里不论 multiplier 设置为几,结果都是0,所以 blueSubView 的 left 永远都是0,坐标系同样是 blueView 的坐标系。表现出的现象,就是红色视图永远都紧贴蓝色视图。
- 当我们将约束添加在 superview 上时,此时 blueView 的 left,相对于 superview 的坐标系,是30。根据方程,计算可得 blueSubView 的 left 是60,坐标系同样是 superview 的坐标系,然后再切换回 blueSubView 的父视图(也就是 blueView)的坐标系,即为30。表现出的现象,就是红色视图左侧到蓝色视图左侧为30。
这也就解释了, 为什么 make.left.mas_equalTo(blueView).multipliedBy(2);
这里,我们将 multiplier 设置为任意值,都不影响展示的结果。但是,这里考虑,如果将 left 替换为 right,效果是怎样?这里注意,right 和 left 不同,即使是在自身的坐标系,right 是 view 自身的宽度,也就是 width 。
将约束修改为如下代码:
[blueSubView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(0);
make.right.mas_equalTo(blueView).multipliedBy(0.5);
make.width.mas_equalTo(50);
make.height.mas_equalTo(100);
}];
所以上述代码,会将 blueSubView 的 right,设置到 blueView 的中间。效果如下:
综上所述,影响最终展示效果的,不仅仅是约束方程中的所有因素,还有约束被添加到的视图(坐标系)。