美文网首页
iOS 自动布局 AutoLayout 详解

iOS 自动布局 AutoLayout 详解

作者: whlpkk | 来源:发表于2022-07-27 17:01 被阅读0次

    本文部分翻译自苹果官方文档 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。位置属性用于指定项目相对于其他事物的位置。但是,它们没有表明物品的大小。
      • 对于具体属性的值的解释,请参阅官方文档 值解释 章节。

    思考

    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 的中间。效果如下:

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

    相关文章

      网友评论

          本文标题:iOS 自动布局 AutoLayout 详解

          本文链接:https://www.haomeiwen.com/subject/ozmzirtx.html