×

3. Anatomy of a Constraint(约束详解)@Auto Layout Guide(自动布局指南)

96
fever105
2017.09.28 09:36* 字数 5163

翻译@Auto Layout Guide(自动布局指南)


Getting Started(新手上路)

Anatomy of a Constraint(约束详解)

布局所需的约束通过关系等式表示。一个等式表示一条约束。我们的目标就是定义有且仅有唯一解的等式。

下图列举了一个等式:

图5

其所表示的约束规定红色视图的前边(leading)位于蓝色视图后边(trailing)后方8pt的位置。等式的组成部分如下:

  • Item 1(元素1)。等式的第一个元素——这里是红色视图。元素1不能为空。元素必须是视图(view)或布局参照(layout guide)。

    (译者:其实,还有一种对象可以参与约束的创建:UILayoutSupport。但苹果提供了两种创建约束的原生API,关系等式以及VFL,其只能参与后者。而且,VFL由于不易用,已经几乎被放弃。🤔)

  • Attribute 1(属性1):元素1被约束的属性——这里是红色视图的前边(leading)。

  • Relationship(关系):等式左边和右边的关系,有三种:等于(equal),大于等于(greater than or equal),小于等于(less than or equal)。这里是左边与右边相等。

  • Multiplier(系数):属性2的值会乘以这个浮点数——这里是1.0。

  • Item 2(元素2):等式的第二个元素——这里是蓝色视图。与元素1不同,可以为空。

  • Attribute 2(属性2):元素2被约束的属性——这里是蓝色视图的后边(trailing)。如果元素2为空,此处则为Not an Attribute

  • Constant(常量):浮点数偏移量——这里是8.0。其和属性2的值相加。

大多数约束定义界面上两个元素之间的关系。元素可以视图,也可以是布局参照(layout guide)。约束还可以表示同一个元素两个属性之间的关系。例如,设定视图宽高比。也可以为视图的宽高设置常量:此时,等式中第二个元素为空,第二个属性为Not An Attribute,系数为0.0。

Auto Layout Attributes(可约束的属性)

自动布局中,属性表示可约束的元素特征。总的来说,包括元素的四边(上下,前后),宽高,以及水平和垂直方向上的中点。文本元素还会有一个或多个基准线属性。

图6

查看所有属性,详见枚举NSLayoutAttribute

注意

虽然OS X和iOS都使用这个枚举,但它们对某些值的解释不同。所以,查看文档时,要注意区分平台。

Sample Equations(样例等式)

使用不同元素和属性创建等式,能够表示表示不同约束。例如两个视图的间距,相对大小,让视图一边对齐,甚至视图本身的宽高比。然而,并非所有的属性都可以配对使用。

属性分为两类:表示尺寸的(如宽高)和表示位置的(如前后,左右)。尺寸属性定义元素大小,与位置无关。位置属性定义元素的相对位置,与尺寸无关。

根据上述区别,有如下规则:

  • 不能根据位置属性约束尺寸属性。
  • 不能为位置属性设置常量。
  • 不能对位置属性使用"无意义"的系数(即除了1.0以外的值)。
  • 不能根据垂直位置属性约束水平位置属性,或反之。
  • 不能根据前后位置,约束左右位置,或反之。

如果没有参照,仅设置元素的上边为常量20.0pt毫无意义。因此约束位置时,参照是必须的。例如,元素的上边位于父视图上边下方20.0pt处。然而,元素的高度为20.0pt就没问题。更多信息,详见章节Interpreting Values(数值解释)

代码3-1例举了一组表示常见约束的等式。

注意

本章所有等式都使用伪代码表示。要查看具体代码,详见章节代码创建约束自动布局手册

代码3-1 表示常见约束的等式

// 高度固定
View.height = 0.0 * NotAnAttribute + 40.0
 
// 两个按钮间距固定
Button_2.leading = 1.0 * Button_1.trailing + 8.0
 
// 对齐两个按钮的前边
Button_1.leading = 1.0 * Button_2.leading + 0.0
 
// 两个按钮等宽 
Button_1.width = 1.0 * Button_2.width + 0.0
 
// 视图中心与父视图对齐
View.centerX = 1.0 * Superview.centerX + 0.0
View.centerY = 1.0 * Superview.centerY + 0.0
 
// 视图宽高比固定
View.height = 2.0 * View.width + 0.0

Equality, Not Assignment(相等,而非赋值)

注意,等式两端是相等关系,而非赋值。

解析时,系统不会将右边的值赋给左边。而是像解方程一样,计算等式成立时属性1和属性2的值。这意味着元素的顺序无关紧要。例如,代码3-2和代码3-1表示的约束其实一样。

代码3-2 颠倒等式中元素的顺序

// 两个按钮间距固定
Button_1.trailing = 1.0 * Button_2.leading - 8.0
 
// 对齐两个按钮的前边
Button_2.leading = 1.0 * Button_1.leading + 0.0
 
// 两个按钮等宽 
Button_2.width = 1.0 * Button.width + 0.0
 
// 视图中心与父视图对齐
Superview.centerX = 1.0 * View.centerX + 0.0
Superview.centerY = 1.0 * View.centerY + 0.0
 
// 视图宽高比固定
View.width = 0.5 * View.height + 0.0

注意

调整元素顺序的同时,也需要调整系数和常量的值。例如,常量从8.0变为-8.0;系数从2.0变为0.5。常量为0.0和系数为1.0时不需调整。

使用自动布局,一个问题可以有很多种解决方式。理想情况下,选择最能够体现我们意图的方式。然而,不同开发者对问题的理解不同,选择也会不同。不管怎样,选定一种,并坚持使用,能够避免很多问题。例如,本文遵循下述规则:

  1. 系数最好为整数,而非分数。
  2. 常量最好为正数,而非负数。
  3. 如果可能,(添加约束时)元素应该按照布局中的顺序出现:自上至下,自前至后。

Creating Nonambiguous, Satisfiable Layouts(创建明确,可满足的约束)

自动布局的关键在于表示约束的等式有且仅有唯一解。模糊的(non-ambiguous)约束有一个以上解。无法满足的(unsatisfiable)约束没有解。

总的来说,视图的尺寸和位置都必须得到约束。假设父视图的尺寸已经确定(例如,父视图是iOS中一个场景(scene)的根视图),那么要创建一个明确,可满足的布局,每个子视图的每个方向上各需要两条约束。不过,至于使用哪些约束,选择很多。例如,下面的三个布局都是明确,可满足的(只显示水平方向上的约束)。

图7
  • 第一个布局相对于父视图前边约束视图前边,视图固定宽度。据此,视图后边的位置得以确定。
  • 第二个布局相对于父视图前边约束视图前边,父视图后边约束视图后边。根据父视图的宽度,视图宽度得以确定。
  • 第三个布局相对于父视图前边约束视图前边,视图中心点与父视图中心点对齐。根据父视图的宽度,视图宽度及后边得以确定。

注意,每个布局中,子视图都有两条水平约束。完整定义了视图宽度和水平位置:即水平方向上的布局是明确,可满足的。然而,它们的效果并不相同。想象一下,如果父视图的尺寸改变,视图尺寸和位置会如何变化。

第一个布局,视图宽度不变。大多数时候,这并非期望的效果。实际上,一般不能将视图尺寸写死。自动布局是为创建动态布局而生的,如果写死,它就是去了意义。

也许很难一眼看出,但第二个和第三个布局的效果相同:不论父视图尺寸如何变化,视图与其的左右间距不变。然而,从使用上讲,这两个布局又不一样。第二个布局可能更好理解,但第三个布局可能更好用,特别需要中心对齐多个视图时。如前所述,我们要根据效果选择布局方式。

看一个更复杂的例子。假设iPhone上有两个视图要并排显示。它们同宽,且四周都留有空隙。即使设备旋转,布局不变。

下图显示设备在垂直和水平方向时的效果:

图8

至于如何添加约束,下图是一个直观的解决方案:

图9

约束如下:

// 垂直约束
Red.top = 1.0 * Superview.top + 20.0
Superview.bottom = 1.0 * Red.bottom + 20.0
Blue.top = 1.0 * Superview.top + 20.0
Superview.bottom = 1.0 * Blue.bottom + 20.0
 
// 水平约束
Red.leading = 1.0 * Superview.leading + 20.0
Blue.leading = 1.0 * Red.trailing + 8.0
Superview.trailing = 1.0 * Blue.trailing + 20.0
Red.width = 1.0 * Blue.width + 0.0

根据之前的规则,我们要布局两个视图,意味着4个水平约束,4个垂直约束。虽然(这种推测)并不绝对,但能够让我们迅速上手。更重要的是,只有这样两个视图的尺寸和位置同时得到定义,从而创建明确,可满足的布局。缺少任意一条约束,布局有歧义;添加额外约束,则可能产生冲突。

当然,答案不只有一个。下图中的也可以:

图10

没有参照父视图,而是根据红色视图约束蓝色视图的上下边。伪代码如下:

// 垂直约束
Red.top = 1.0 * Superview.top + 20.0
Superview.bottom = 1.0 * Red.bottom + 20.0
Red.top = 1.0 * Blue.top + 0.0
Red.bottom = 1.0 * Blue.bottom + 0.0
 
// 水平约束
Red.leading = 1.0 * Superview.leading + 20.0
Blue.leading = 1.0 * Red.trailing + 8.0
Superview.trailing = 1.0 * Blue.trailing + 20.0
Red.width = 1.0 * Blue.width + 0.0

仍然是两个视图,仍然是每个方向四条约束。布局仍然是明确,可满足的。

哪个更好?

两种布局方式都OK。但哪个更好呢?

不好意思,我们无法评判孰优孰劣。因为它们各有优缺点。

对于第一种方式,如果移除其中一个子视图,对布局影响较小。视图被移出视图结构时,与其相关的所有约束也会被移除。如果移除红色视图,蓝色视图就只剩三条约束,只需再添加一条即可。而第二种方式,红色视图的移除意味着蓝色视图只剩下一条约束。

另一方面,对于第一种方式,假设需要对齐所有子视图的上下边,必须保证所有约束的偏移量相同。修改一处,其他地方也要修改。

Constraint Inequalities(不等关系)

目前为止,我们所接触的约束都通过相等关系表示。然而,也可以使用不等关系:即关系的种类可以是相等(equal),大于等于(greater than or equal to),和小于等于(less than or equal to)。

举个例子,通过不等关系约束视图尺寸的最大值和最小值(代码 3-3)。

代码 3-3 视图尺寸的最大值和最小值

// 设置最小宽度
View.width >= 0.0 * NotAnAttribute + 40.0
 
// 设置最大宽度
View.width <= 0.0 * NotAnAttribute + 280.0

之前我们说过,视图每个方向上只需两条约束,但使用不等关系表示的约束不在此列。两个不等约束可以代替一个相等约束,见代码3-4。

代码 3-4 两个不等约束代替一个相等约束

// 一个相等约束
Blue.leading = 1.0 * Red.trailing + 8.0
 
// 可以被两个不等约束替代
Blue.leading >= 1.0 * Red.trailing + 8.0
Blue.leading <= 1.0 * Red.trailing + 8.0

然而这种替代并不总是成立。例如,代码3-3中的不等约束只限制了视图宽度——并没有定义宽度的具体值。因此,视图宽度依然需要约束,但这个值必须在不等约束的范围内。

Constraint Priorities(约束优先级)

约束的默认优先级是必要(reqiured),即最高级。计算布局时,所有约束都必须被满足,否则就会出错。无法满足的约束,其信息会被输出至控制台;并被系统忽略,我们称之为"打破约束"。随后,布局重新计算。更多信息,详见章节Unsatisfiable Layouts(无法满足的约束)

优先级的范围从1到1000,小于1000是可选约束;1000代表必要约束。

计算布局时,系统会按照优先级从高到低满足约束:对于无法满足的可选约束,会跳过(译者:对于无法满足的必要约束,意味着冲突,则会打破)

没有被满足的可选约束仍然可以影响布局。如果布局因为跳过可选约束而产生歧义,那么系统会基于这个约束调整布局,选择最接近的值。因此,它更像是一种“趋势”。(译者:例如,有可选约束a == b,即使其无法被满足,系统也会尽量将abs(a - b)的值最小化。🤔)

可选约束经常和不等关系一起使用。例如,代码3-4中的不等约束可以有不同优先级。大于等于的约束优先级为必要(1000),小于等于的约束优先级较低(250)。这意味着它们的间距至少为8.0pt;如果存在其他高优先级约束,则它们的间距可以拉大,但在满足这些高优先级约束的前提下,系统又会尽量缩小二者的间距至8.0pt。

注意

优先级不一定必须是1000。事实上,系统预设了四档:低(250),中(500),高(750)以及必要(1000)。有时,为了防止优先级相同所产生的冲突,可以微调。但如果我们需要为每个约束的优先级设置具体值,那么布局方式很可能有问题。

更多关于iOS预设优先级的信息,详见枚举UILayoutPriority。至于OS X,详见布局优先级常量。

Intrinsic Content Size(固有尺寸)

目前为止,示例中视图的尺寸和位置都被显性约束。然而,某些视图,根据其内容,天生有一个尺寸,称之为固有尺寸。例如,按钮的固有尺寸等于标题的尺寸加上到四周的边距。(译者:这里指的是按钮只有标题,没有图片的情况🤔)

不是所有视图都有固有尺寸。即便有,固有尺寸也可能只限定视图的宽,或者高。表3-1是一些例子:

表 3-1 常见视图的固有尺寸

视图 固有尺寸
UIView和NSView 没有
滑块控件 只限定宽度(iOS)
根据类型不同,分别限定宽,高或全部(OS X)
标签,按钮,开关和文本框 同时限定宽高
文本视图(text view)和图像视图(image view) 根据内容变化

固有尺寸基于视图的当前内容产生:对于标签和按钮,同文本和字体有关;对于其他视图,计算方式会很复杂。例如,空的图像视图没有固有尺寸;一旦添加图片,就等同于图片尺寸。

文本视图的固有尺寸会根据文本内容,能否滚动,及其他约束而改变。例如,允许滚动,没有固有尺寸;不允许,默认为不自动折行的文本内容的大小。假设文本不含回车,那么其尺寸就是其作为一行文字显示时所占的空间。如果定义了文本视图的宽度,那么其固有尺寸就是在特定宽度下完整显示文本所需的高度。

系统通过每个方向上两条约束来表示固有尺寸:内缩(content hugging)和外扩(content compression resistance)(译者:宽度两条,高度两条🤔)。内缩代表尺寸向内收缩的趋势,使得视图紧紧包裹内容;外扩代表尺寸向外伸展的趋势,使得视图边缘不会裁剪内容。

图11

代码3-5通过不等式关系表示这四条约束。其中IntrinsicWidthIntrinsicHeight分别代表固有尺寸宽高。

代表 3-5 外扩和内缩与固有尺寸一起形成约束,决定视图的尺寸

// 外扩
View.height >= 0.0 * NotAnAttribute + IntrinsicHeight
View.width >= 0.0 * NotAnAttribute + IntrinsicWidth
 
// 内缩
View.height <= 0.0 * NotAnAttribute + IntrinsicHeight
View.width <= 0.0 * NotAnAttribute + IntrinsicWidth

这些各有自己的优先级。默认,内缩优先级为250,外扩优先级为750。因此,视图更倾向于外扩,而非内缩。对于大多数视图来说,这是可以接受的结果。例如,按钮可以随意拉伸放大,但如果缩小,内容就可能被剪裁。需要注意的是,界面编辑器(Interface Builder)有时会自动修改这些优先级,防止冲突发生。更多信息,详见章节Setting Content-Hugging and Compression-Resistance Priorities(设置内缩和外扩优先级)

布局时尽量使用固有尺寸。如此,布局能够动态响应视图内容的变化,同时也削减了约束的数量;但我们可能需要手动管理内缩和外扩优先级。固有尺寸的使用规则如下:

  • 需要拉伸多个视图填满空间时,所有内缩优先级都一样,则布局歧义。因为系统不知道该拉伸哪个视图。

    举个例子:当标签和文本框在一起时,通常应该拉伸文本框,标签保持固有尺寸不变。为此,需要设置文本框的水平内缩优先级低于标签。

    这样的组合是如此常见,以至于界面编辑器遇到这种情况时会自动处理:标签的内缩优先级被设为251。而代码布局时,需要手动修改。

  • 无背景色(透明)的视图(例如按钮和标签)尺寸失真时(即不再是固有尺寸),会造成诡异的布局。问题可能很难发现,因为我们只观察到文字的位置不正确。增加内缩优先级,可以防止这种情况发生。

  • 基准线约束只在视图的高度是其固有高度时有效。垂直方向上压缩或拉伸后,基准线约束无法对齐。(译者:事实上我从未用过这个约束,所以这里的理解可能有误。🤔)

  • 某些视图,如开关(switch),最好只使用固有尺寸。通过适当调整外扩和内缩优先级,防止变形。

  • 尽量避免最高优先级(即"必要")。错误的视图尺寸总比造成约束冲突好。如果必须使用固有尺寸,可以将优先级设置为999。如此一来,视图既保持了固有尺寸,也给意外情况留下回旋余地:例如,可现实的空间比预想的过大或过小。

Intrinsic Content Size Versus Fitting Size(固有尺寸 vs. 契合尺寸)

(译者:契合寸可以通过UIView的方法systemLayoutSizeFittingSize:获取。🤔)

对于自动布局来说,固有尺寸是输入。系统根据固有尺寸创建约束,定义视图尺寸,计算布局。

而契合尺寸是输出:即根据视图现有约束计算出的尺寸。如果视图使用约束布局内容,那么内容的尺寸就是它的契合尺寸。

以堆叠视图为例:没有额外约束时,系统会根据其内容和属性计算尺寸。从很多方面看,堆叠视图似乎有固有尺寸:例如,只需定义位置,布局就算完整。但实际上,堆叠视图的尺寸是自动布局计算的结果(输出),而非计算的依据(输入)。因此,修改它的外扩和内缩优先级无效,因为固有尺寸不存在。

如果需要根据其他视图调整堆叠视图的契合尺寸(这个视图不属于堆叠视图),要么创建约束,要么修改内容的外扩和内缩优先级。

Interpreting Values(数值解释)

自动布局中数值的单位是点(points)。然而,值的意义会根据不同属性和布局方向有不同的解释。

属性 数值含义 备注
高 / 宽(Height/Width) 视图尺寸 可以是常量,也可以相对于其他宽高定义。不能为负。
上边 / 下边 / 基准线(Top/Bottom/Baseline) 越靠近屏幕下方,值越大 相对于垂直中心,上边,下边和基准线定义。
前边 / 后边(Leading/Trailing) 越靠近后边,值越大。阅读方向自左至右时,越靠右值越大;相反,越靠左数值越大。 相对于前边,后边和水平中心定义。
左边 / 右边(Left/Right) 越靠右值越大。 相对于左边,右边和水平中心定义。
尽量使用前后,而非左右,以便布局根据阅读方向做出调整。
默认,阅读方向同设备当前语言一致。设置视图属性semanticContentAttribute,以规定视图内容是否需要根据阅读方向翻转。
水平 / 垂直中心(Center X/Center Y) 数值的含义取决所参照的属性 水平中心可以相对于水平中心,前后,左右定义。
垂直中心可以相对于垂直中心,上下,基准线定义。
Web note ad 1