本文部分翻译自苹果官方文档 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。位置属性用于指定项目相对于其他事物的位置。但是,它们没有表明物品的大小。
- 对于具体属性的值的解释,请参阅官方文档 值解释 章节。
二、 属性兼容
这些方程可用的各种属性使您可以创建许多不同类型的约束。您可以定义视图之间的空间、对齐视图的边缘、定义两个视图的相对大小,甚至定义视图的纵横比。但是,并非所有属性都兼容。这里主要有以下几条规则:
- 不能将尺寸属性限制为位置属性。否则会造成 crash 。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);
这样写是不符合规范的。这里圈重点,实际上是有场景需要这样做的。这条原则非绝对 - 对于位置属性,不能将垂直属性约束为水平属性。否则会造成 crash 。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);
}];
这里非常容易踩坑,一定要注意不是赋值,也可能会修改右侧的值以使得方程成立。
四、添加约束的视图
要将约束添加到哪个视图上,函数声明如下:
- (void)addConstraint:(NSLayoutConstraint *)constraint API_AVAILABLE(ios(6.0)); // This method will be deprecated in a future release and should be avoided. Instead, set NSLayoutConstraint's active property to YES.
- (void)addConstraints:(NSArray<__kindof NSLayoutConstraint *> *)constraints API_AVAILABLE(ios(6.0)); // This method will be deprecated in a future release and should be avoided. Instead use +[NSLayoutConstraint activateConstraints:].
- (void)removeConstraint:(NSLayoutConstraint *)constraint API_AVAILABLE(ios(6.0)); // This method will be deprecated in a future release and should be avoided. Instead set NSLayoutConstraint's active property to NO.
- (void)removeConstraints:(NSArray<__kindof NSLayoutConstraint *> *)constraints API_AVAILABLE(ios(6.0)); // This method will be deprecated in a future release and should be avoided. Instead use +[NSLayoutConstraint deactivateConstraints:].
这里可以看下官方文档的解释:
The constraint must involve only views that are within scope of the receiving view. Specifically, any views involved must be either the receiving view itself, or a subview of the receiving view. Constraints that are added to a view are said to be held by that view. The coordinate system used when evaluating the constraint is the coordinate system of the view that holds the constraint.
翻译如下:
约束必须只涉及receiver视图范围内的视图。具体来说,所涉及的视图必须是receiver视图本身,或者是receiver视图的子视图。添加到视图中的约束被视为由该视图持有。评估约束时使用的坐标系是持有该约束的视图的坐标系。
Masonry的封装中,会自动帮我们处理查找添加视图的逻辑。总结如下:
- view1和view2都存在,则查找2者最近的公共父视图。
[View_1 mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(View_2).offset(10); }];
- view2不存在且view1.attribute1是尺寸属性
[View_1 mas_makeConstraints:^(MASConstraintMaker *make) { make.height.equalTo(100); }];
- view2不存在且view1.attribute1不是尺寸属性
[View_1 mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(100); }]; // 这样写,实质等价于下面这种写法: [View_1 mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(View_1.superView).offset(100); }];
Masonry源码如下:
if (self.secondViewAttribute.view) {
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
NSAssert(closestCommonSuperview, @"couldn't find a common superview for %@ and %@", self.firstViewAttribute.view, self.secondViewAttribute.view);
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}
...
[self.installedView addConstraint:layoutConstraint];
苹果在iOS8后,已经不推荐使用addConstraint:
方法,而是直接使用layoutConstraint.active = YES/NO
,系统会自动帮你添加到最近的公共父视图上,省去了查找公共父视图的麻烦。
五、 探索
这里用1个示例,带大家探索约束添加到不同视图造成的影响。由于Masonry已经封装完添加约束视图的逻辑,且没有暴露接口修改,我们用系统原生Autolayout写法来演示。看下面的例子:
@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);
}];
//同理,我们将right的约束添加到superview上,注释掉Masonry的约束make.right.mas_equalTo(blueView).multipliedBy(0.5)
MASLayoutConstraint *layoutConstraint =
[MASLayoutConstraint constraintWithItem:blueSubView
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:blueView
attribute:NSLayoutAttributeRight
multiplier:0.5
constant:0];
layoutConstraint.priority = UILayoutPriorityRequired;
[superview addConstraint:layoutConstraint];
所以上述代码,效果分别如下:
综上所述,影响最终展示效果的,不仅仅是约束方程中的所有因素,还有约束被添加到的视图(坐标系)。