iOS 自动布局 AutoLayout 详解

本文部分翻译自苹果官方文档 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 的值中。

小结:

  1. 大多数约束定义了我们用户界面中两个 Item 之间的关系。这些Item可以代表 view 或 layout guide。
  2. 约束还可以定义单个 Item 的两个不同属性之间的关系,例如,设置项目的高度和宽度之间的纵横比。
  3. 您还可以为 Item 的高度或宽度分配常量值。使用常量值时,第二项留空,第二个属性设置为 Not An Attribute,乘数设置为 0.0
  4. 有关属性的完整列表,请参阅 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的封装中,会自动帮我们处理查找添加视图的逻辑。总结如下:

  1. view1和view2都存在,则查找2者最近的公共父视图。
    [View_1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(View_2).offset(10);
    }];
    
  2. view2不存在且view1.attribute1是尺寸属性
    [View_1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.height.equalTo(100);
    }];
    
  3. 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];

所以上述代码,效果分别如下:


综上所述,影响最终展示效果的,不仅仅是约束方程中的所有因素,还有约束被添加到的视图(坐标系)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容