自打我混iOS圈以来,写UI就使用的是frame绝对布局。说是「绝对」,但在写的时候也已带着动态的思想了。比如,尽可能地用 autoResizingMask
。但是对于那种mask不能用的场景,在写布局时就像在做小学几何题,很是复杂。
在近期的项目中,尝试了Auto Layout,试着把自己的心得总结一下。
Auto Layout 简介
网上介绍Auto Layout的文章很多,有一点大家很少提到。就Auto Layout本身来说,它并不是什么新鲜的技术。Auto Layou系统是 Cassowary 算法的Cocoa实现。Cassowary是一个在二十世纪九十年代被发明,解析 线性 等式、不等式约束的一个算法。
开发者提供一系列的 布局规则 给Cocoa Auto Layout系统,它基于Cassowary算法,把规则转换成了View(s)的frame,完成了布局。这个所谓的「布局规则」,就是Auto Layout里的「约束」,Constraint。
前面说到,Cassowary算法解析的线性等式、不等式,因此,我们提供给Auto Layout的约束是线性的约束。可以简单把线性约束理解为用一次方程来描述的约束。
一次方程,它的基本形式(以等式为例)
y = kx + b
自然地,在Auto Layout中,一个约束该长什么样子:
view1的某个属性 = k * view2的某个属性 + b
如上面所示,一个约束描述了两个属性之间的关系,涉及到了7个量(view1、属性1、关系、view2、属性2、k、b)。
我们就是把这种形式的一条条的规则提供给Auto Layout,它综合了许许多多这样的规则,来完成布局。
「Hey,小奥,这个View要靠左显示,离它爹10像素吧!」「小奥,这个Label跟它上面的按钮垂直居中!」
怎么样?反正给我的感受是,frame布局是我们替机器思考,而Auto Layout,是为我们自己思考。
Constraint in Code
上文我们介绍了在逻辑中该怎么去表示一个「约束」。那落实到代码中,该怎么表示呢?
苹果给我们提供了这样一个类,用作代码中一个约束的抽象——NSLayoutConstraint
。
它有一个很长的构造方法,返回一个约束实例,这个方法的参数就是上面提到的7个量:
NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:view1 attribute:attr1
relatedBy:relation
toItem:view2 attribute:attr2
multiplier:k constant:b];
通过这个方法,我们构造一个又一个的 NSLayoutConstraint
实例,通过把这些实例add给合适的View的方式提供给Auto Layout System,就完成了代码层面的添加约束的过程。
Before starting
在你迫不及待的想上手之前,有两点务必记住。
- 代码上忘掉
frame
和autoresizingMask
- 父View当前的大小是不可靠的
对于第一点,由于Auto Layout System已经接管了frame的设置,如果你再来掺一脚,会有很多诡异的问题。注意我说的是在代码上忘掉 frame
,在思考某个特定View的约束时,还是要想到它的frame的概念的。回想我们的frame布局时代,一个frame:
{x, y, width, height}
有四个量,也就是说至少需要四个量才能确定一个View的布局,逻辑上想想确实是那么回事。那我在我们添加某个View的约束时,也至少需要四个约束,才能确定一个View的布局。
水平方向:我在哪?我多宽?
竖直方向:我在哪?我多高?
---- 一个View的独白
忘掉 autoresizingMask
同理。我们甚至 必须要 显式地设置translatesAutoresizingMaskIntoConstraints
来保证约束的正确解析。当你在Auto Layout里摸爬滚打,痛不欲生却无论如何也没有正确优雅的布局映入你眼帘的时候,一定要记得回过头来看看,是不是忘记把相关View的这个属性设置为 NO
了!
对于第二点,这是为了强迫你用动态的思维去思考该怎么描述约束。走出根据父View的bounds来设置子View的frame的时代吧,enjoy Auto Layout!
Add Constraints
接下来我们就开始计划着给View添加约束了。假设给了你一块地(一个View),你要给它的子View们添加约束,用Auto Layout进行布局,大致的思路如下:
- 给这些Views大致分分组。当子View很多时,没必要全把他们当儿子。合理地用一些Container View,把儿子变成孙子,达到简化约束的目的。
- 确定儿子们的布局依赖等级关系。举个例子,要想确定儿子A的位置,首先我得知道儿子B的位置。换句话说,只要儿子B的位置确定了,那么儿子A的位置就能确定。
- 找出那些只依赖父亲的儿子,也即依赖关系最顶层的儿子(一定存在),先添加它们的约束。
- 根据依赖关系层级,逐级添加约束
Example
假设我们要实现一个TableView,它的每个Cell中有个ImageView,显示一张图。大致效果如下图所示。
按着上面的思路,因为这个父亲(TableView Cell的contentView,注意使用Auto Layout布局TableView Cell一定要把儿子加在cotnentView上,不然在iOS 7、8上会有很诡异的问题)只有一个儿子,布局依赖关系也就很清楚了,开始构思约束。
Wait,在添加约束前,要先告知Auto Layout System ImageView和self.contentView的父子关系:
_imageView = [[UIImageView alloc] init];
_imageView.backgroundColor = [UIColor orangeColor];
_imageView.translatesAutoresizingMaskIntoConstraints = NO; // Don't forget !!
[self.contentView addSubview:_imageView];
OK,首先,这个ImageView的上、左、下距离它爹各20,约束如下:
NSLayoutConstraint *c1 = [NSLayoutConstraint constraintWithItem:_imageView attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.contentView attribute:NSLayoutAttributeLeft
multiplier:1 constant:20];
NSLayoutConstraint *c2 = [NSLayoutConstraint constraintWithItem:_imageView attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.contentView attribute:NSLayoutAttributeTop
multiplier:1 constant:20];
NSLayoutConstraint *c3 = [NSLayoutConstraint constraintWithItem:_imageView attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.contentView attribute:NSLayoutAttributeBottom
multiplier:1 constant:-20];
来分析一下。垂直方向,当 self.contentView
的高度确定后,由于我们指定了_imageView
的上下边距,则y和height都能被确定;水平方向上,我们指定了左边距,能确定x,但是width不能确定,因此我们还需要一个约束:
NSLayoutConstraint *c4 = [NSLayoutConstraint constraintWithItem:_imageView attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:_imageView attribute:NSLayoutAttributeHeight
multiplier:1 constant:0];
c4
告诉了Auto Layout System, _imageView
的长和宽相等。这样,它的约束就齐活了。
接着,这四个约束要给谁加。注意,每个约束要加载这个约束涉及到的两个View的最小父View 上,Forgiveness,这是我自己提出的概念。注意到 c1
, c2
, c3
涉及到的两个View是 self.contentView
和 _imageView
,其中 _imageView
是 self.contentView
的子View,因此约束就需要加给 self.contentView
。
[self.contentView addConstraints:@[c1, c2, c3]];
c4
涉及到的两个View是 _imageView
本身,约束加给它自己。
[_imageView addConstraint:c4];
运行起来,结果如下图。
Visual Format Language
继续上面的例子。或许你会抱怨,每个View都需要搞这么一发,得写多少代码啊!
苹果贴心地给我们搞了一套形象地表示约束的方法——Visual Format Language(VFL)。
上面的 c1
, c2
, c3
可以表示成:
CGFloat leftMargin = 20;
NSArray *cs = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(leftMargin)-[_imageView]"
options:0 metrics:@{@"leftMargin": @(leftMargin)}
views:NSDictionaryOfVariableBindings(_imageView)];
NSArray *cs1 = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-20-[_imageView]-20-|"
options:0 metrics:nil
views:NSDictionaryOfVariableBindings(_imageView)];
[self.contentView addConstraints:cs];
[self.contentView addConstraints:cs1];
要把VFL说完,得新写一篇文章了啊,篇幅限制,就不展开叙述了,具体请看: 官网文档 。
后续
如果认真看到这,那么相信你是真的入门了。Auto Layout的东西还有很多,值得你去花时间继续深入调研。
比如,每个约束还有优先级之分。当Auto Layout System发现你给它的约束有冲突,它会根据有冲突的约束的优先级进行仲裁。
还有一些其它的东西是入门之后需要细细体会的,最好自己写一些Demo。
- Frame vs. Alignment Rect
- Intrinsic Content Size 以及 Compression Resistance 和 Content Hugging
- Animation with Auto Layout
参考文献