Auto Layout Guide
本文翻译自苹果关于 Auto Layout 的 使用指南 第一章,如果有什么不正确和不确切的地方欢迎大家指正。话不多说,下面上翻译。
了解 Auto Layout
Auto Layout 是根据添加在视图上的约束,动态计算视图位置和大小的布局方式。举个🌰:你可以设置约束,让一个 Button 和一个 ImageView 有着相同的水平中心,同时 Button 的顶部距离 ImageView 的底部8个点。此时,如果 ImageView 的大小或者位置发生改变,那么 Button 的布局也会随着动态变化。
外部变化
当父视图的大小或者位置发生改变,就表示发生了外部变化。此时,就需要更新子视图的层次结构和布局来适应外部变化。下面列举几种常见的外部变化:
- 用户改变窗口大小(OS X)
- iPad 用户插入或者收起 Split View
- 设备旋转(iOS)
- 当前通话或录音条出现或者消失(iOS)
- 你想支持不同的大小的类(iOS)
- 你想支持不同屏幕大小
大部分的外部改变发生在程序运行的时候,而此时你的 APP 需要马上对此作出回应。即使屏幕尺寸在程序运行的时候一般不会发生改变,创建一个自适应视图能让你的 APP 很好的运行在 iPhone4s, iPhone6 Plus 甚至 iPad 上。Auto Layout 同时也是支持 iPad 上 Split View 和 Slide View 等关键组件。
内部变化
当用户界面中视图或者控制器的大小发生改变的时候内部变化就发生了。下面几种是常见的内部变化:
- APP 展示的内容发生变化
- APP 支持国际化
- APP 支持动态类型(iOS)
当界面展示的内容发生改变,新的内容会要求一个新的布局。这种情况通常发生在 APP 的文字和图片展示上。举个🌰:新闻类应用需要根据新闻的内容的大小来调整视图。类似的,照片类应用需要根据照片的大小做一个布局。
国际化是使得 APP 内容能够根据不同的语言,地区和文化去展示的一个过程。国家化的 APP 布局的时候必须将这些因素加入考虑。这类 APP 主要有三方面的因素影响布局。
首先,当你改变内容语言的时候, label 一般会要求不同的空间。像是德语就需要更多空间,英语和日语就比较少。
第二,日期和数字的格式会根据不同地区改变,即使语言没有改变。虽然这种情况比语言改变更罕见,但是你还是需要去处理。
第三,改变语言不仅影响文字内容的大小,还有控件布局方案。例如:英语是从左到右的阅读顺序,但阿拉伯语和犹太语是从右到左的阅读顺序。通常界面的控件布局需要适配这种情况,如果在英文界面中一个按钮在右下角,一般在尤太文中是在左下角。
最后,如果你的 APP 支持动态类型,用户可以修改 APP 字体大小,这会导致你界面上所有文本控件的大小发生改变。如果改变文本大小时,你的项目正在运行,控件的大小和位置需要同时发生改变。
Auto Layout 和 Frame 布局
界面主要有三种布局方式。你可以使用编程式方式布局(Frame),也可以使用 autoresizing masks 来响应外部变化,或者你可以使用 Auto Layout 。
通常,APP 布局使用编程式布局来设置视图的 frame ,frame 定义了视图在父视图上的原点和大小。
ut_views_2x.png)
为了展示你的 APP 界面,你需要计算视图层级中每个视图的大小和位置。如果发生变化,你还必须计算变化之后被影响的视图的位置和大小。
在所有的布局方式中,编程式布局提供了最强的灵活性和功能。当变化发生的时候,你可以根据需求进行任何修改。当前你需要掌控所有的变化,所以一个简单的界面也需要花费大量精力进行设计,调试和维护。而开发一个真正自适应的用户界面,难度将会增加一个量级。
你可以使用 autoresizing masks 来减少这种影响。它规定了当父视图发生变化的时候改视图怎么变化。这简化了适应外部变化的布局。
但是,autoresizing masks 只适用于布局相对较轻的子集。对于复杂界面,你通常需要结合编程式布局来完善界面。另外,autoresizing masks 只能自适应外部变化,无法适应内部变化。
尽管,autoresizing masks 只是编程式布局的改进。但 Auto Layout 则是完全新的规范。你不在关心视图的 frame ,你更多的思考的是他们之间的关系。
Auto Layout 根据设置的约束来进行界面布局。约束表示两个界面之间的关系。Auto Layout 会基于约束计算视图的大小和位置。这将产生动态响应内部和外部变化的布局。
使用一组约束来进行布局,其编写过程和使用面向对象的布局方式大不相同。幸好,掌握 Auto Layout 和掌握其他编程技能没什么不同。这里主要有两个步骤:首先,你需要理解约束布局背后的逻辑,接着你需要学习相关的 API 。
这份指南主要目的就是帮助你更简便的掌握 Auto Layout 。 Auto Layout Without Constraints 将描述高度抽象的简易的方式创建用户界面。Anatomy of a Constraint 介绍了 Auto Layout 背后的原理,你需要明白怎么和 Auto Layout 交互。Working with Constraints in Interface Builder 描述了自动布局的工具,Programmatically Creating Constraints 和 Auto Layout Cookbook 则讲述了具体的API。最后,Auto Layout Cookbook 提供了各种复杂的典型布局,你可以学习并在项目中使用,
Debugging Auto Layout 则提供了调试 Auto Layout 的工具和建议。
没有约束的 Auto Layout
Stack views
提供了一种简易的方式去使用 Auto Layout ,且不引入复杂的约束。一个简单的 stack view
定义了一行或者一列界面元素。stack view
通过属性来控制这些元素的布局。
- axis: 规定了
stack view
的方向,是横向还是竖向。(UIStackView) - orientation:规定了
stack view
的方向,是横向还是竖向。(NSStackView) - distribution:子视图沿设置方向的布局方式。
- alignment:子视图沿设置方向的垂直方向的布局方式。
- spacing:相邻子视图的空间。
在 interface build 中,你可以拖拽一个横向或者竖向 stack view
到画布上。然后选择相应的内容,并拖入 其中。
如果一个对象有其本身的内部尺寸,那么它就会直接展示这个尺寸。如果没有,interface build 会给一个默认尺寸,你也可以重新设置它的尺寸,interface build 会为你添加相应的约束。
为了更进一步调整布局,你可以使用stack view
的属性。例如:下面的视图设置了间隔为 8 个点,样式为填充平均。
stack view
的布局还依赖于内部视图的压缩阻力和拉伸阻力。
提示:如果你想更进一步自定义布局,你可以通过添加约束实现。但是你需要避免相应的冲突。通常来说,如果一个视图默认返回它的内部尺寸,那么你可以另外添加相应的约束来决定它最后的尺寸。更多相关信息详见Unsatisfiable Layouts
另外,你可以将一个 stack view
全部放入另一个 stack view
中来设计更复杂的布局。
通常你应该尽可能的使用 stack view
来进行布局,只有当这种方式不能满足你的布局需求时,才通过具体的约束来布局。
stack view
的更多相关信息详见 UIStackView Class Reference和 NSStackView Class
约束说明
视图的约束其实就是一个线性方程,每个约束代表一个方程。你的目的就是使用一组方程使得视图的位置和大小只有唯一的解。下面是个简单的约束:
这个约束描述了红色视图的左边距离蓝色视图的右边8个点。它由以下几部分组成:
- Item1: 在这个方程中,第一个 item 是红色视图。
- Attribute1: 第一个 Attribute 是红色图的左边距。
- Relationship: 红蓝两个视图的边距的关系。这里是相等。
- Multiplier: 倍数关系,这里是1.0
- Item2: 第二个 item 蓝色视图
- Attribute2: 蓝视图的右边距
- Constant: 常量,这里是8.0,这个值被加在 Attribute2 后面
大多数的约束定义了两个 item 之间的关系。同时也可以定义一个 item 的两个 attributes 之间的关系。例如:可以设置一个视图的宽高之间的关系。也可以将视图的宽高定义为常量,如果定义为常量,那么第二个 item 的值是留空的。
布局属性
在自动布局中,属性代表着哪些地方可以被约束。大体上,布局属性包括边界(上,左,下,右),还有宽高以及横向,纵向中心点。文本控件还有不同的基准线。
完整的属性列表,移步NSLayoutAttribute
提示:虽然 OS X 和 iOS 使用同一个 NSLayoutAttribute 的枚举,他们的值的定义还是会有些许不同,在查看列表的时候,需要确保你选择了正确的文档。
简单方程式
方程式中的参数和属性可以让你定制不同的约束。你可以定义两个视图的间距,边距,尺寸关系,或者单个视图的宽高比。但这些并不是完全兼容的。
这里有两种基本布局属性类型,尺寸属性和位置属性。尺寸属性定义了控件具体的大小,而位置属性借助其他控件定义具体的位置坐标。以下是一些约束规则需要遵守:
- 不能将位置相关的属性用于约束大小,或者大小相关的属性用于约束
- 在位置约束中不能使用倍率参数(都是默认的1.0)
- 对于位置属性不能将水平属性用于竖直属性,不能将左右属性用于头尾属性
译者说明:左右属性和头尾属性不一定等同,有些国家阅读顺序是从右到左的。
举个例子:直接设置一个控件的 Top 属性是 20.0 个点,这个是没有意义的。必须参考其他的控件来定义位置。像是定义子视图的 Top 是位于父视图的 Top 下面 20.0 个点,这样才是有效的。但是可以直接设置大小属性,例如:高度为20.0个点,这种是有效的。
清单3-1 展示了一些常见的约束的方程式
本章所有的例子都是使用伪代码
// Setting a constant height
View.height = 0.0 * NotAnAttribute + 40.0
// Setting a fixed distance between two buttons
Button_2.leading = 1.0 * Button_1.trailing + 8.0
// Aligning the leading edge of two buttons
Button_1.leading = 1.0 * Button_2.leading + 0.0
// Give two buttons the same width
Button_1.width = 1.0 * Button_2.width + 0.0
// Center a view in its superview
View.centerX = 1.0 * Superview.centerX + 0.0
View.centerY = 1.0 * Superview.centerY + 0.0
// Give a view a constant aspect ratio
View.height = 2.0 * View.width + 0.0
等式,不是赋值
方程中的等号表示的是等式而不是赋值,明白这一点很重要。
当自动布局计算这些方程式的时候,不是简单的将右边的值赋到左边,而是计算左右两边的两个布局属性,让他们的关系满足方程。这表示我们可以自由的重新排列方程中的 item 。例如:清单3-2
Listing 3-2Inverted equations
// Setting a fixed distance between two buttons
Button_1.trailing = 1.0 * Button_2.leading - 8.0
// Aligning the leading edge of two buttons
Button_2.leading = 1.0 * Button_1.leading + 0.0
// Give two buttons the same width
Button_2.width = 1.0 * Button.width + 0.0
// Center a view in its superview
Superview.centerX = 1.0 * View.centerX + 0.0
Superview.centerY = 1.0 * View.centerY + 0.0
// Give a view a constant aspect ratio
View.width = 0.5 * View.height + 0.0
你可以发现这里有多种方式来解决你的需求,但是你应该选择最清晰简洁的一种。但是不同的开发者对于“最好”这个定义有不同的见解。所以选用一种方式并坚持,你将会碰到更少的问题。这里是一些布局建议:
- 整数的倍数会比分数更好
- 正数会比负数要好
- 无论如何,你视图的约束需要从始至终,从上到下
创建明确的,安全的布局
使用自动布局,就是通过一系列等式,确定一个唯一的布局。不明确和不安全的布局会提供超过一个或者无效的布局。
总的来说,你需要设置一个视图的大小和位置。然而这会有多种做法,下面这三种都是明确的,安全的布局(只展示水平布局)。
- 第一个视图设置了左边距,和视图的宽度。
- 第二个视图设置了左右边距。
- 第三个视图设置了左边距和中心点。
上面三种布局方式都确定了视图水平方向的大小和位置。虽然这三种布局都是明确并安全的,但却不一定是你想要的。例如:在第一个视图中,当父视图改变的时候,子视图的宽度是不变的,这往往不是你想要的。自动布局是为了自适应动态布局而创建的。
现在设想一个更复杂的情况,假设你有两个视图,你想要他们有着固定边距,同时让他们的宽度相同。当屏幕旋转的时候,上述条件依然满足。
那么约束看起来是什么样的呢?下面的图片给了一个最直接的例子:
上面的视图的约束如下所示:
// Vertical Constraints
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
// Horizontal Constraints
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
约束不等式
到目前为止,所有的约束都是等式。但这仅仅只是一部分,不等式也可以表示约束。例如:你可以使用不等式设置最小或者最大尺寸,像清单3-3。
Listing 3-3Assigning a minimum and maximum size
// Setting the minimum width
View.width >= 0.0 * NotAnAttribute + 40.0
// Setting the maximum width
View.width <= 0.0 * NotAnAttribute + 280.0
一旦你使用不等式,一个视图一个维度两个约束的规则就不成立了。通常,你可以将一个等式约束替换成两个不等式约束。在清单3-4中,一个等式约束就被替换成了两个不等式约束。
Listing 3-4Replacing a single equal relationship with two inequalities
// A single equal relationship
Blue.leading = 1.0 * Red.trailing + 8.0
// Can be replaced with two inequality relationships
Blue.leading >= 1.0 * Red.trailing + 8.0
Blue.leading <= 1.0 * Red.trailing + 8.0
但是两个不等式不一定相当于一个等式,在清单3-3中,两个不等式还是未能明确视图的宽度,这个时候你就需要继续添加约束来保证视图的大小和位置都唯一确定。
约束优先级
默认情况下,所有的约束都是必须要计算的。如果不计算,控制台会打印出相关约束,然后中断其中一个约束,完成相应的布局。
你可以创建一个可选约束,所有的约束都有自己的优先级从1~1000,优先级为1000的约束是必须计算的。除此之外,其他都是可选的。
当计算结果的时候,自动布局会尝试按照优先级从高到低将所有约束加入计算。如果可选约束不合适,那么该约束就会被放弃,然后计算下一个约束。
即使一个约束被舍弃了,他还是可能影响布局。当布局存在一些不明确的地方时,系统就会从这些被抛弃的约束中选择最相近的,能解决问题的约束来计算布局。
可选约束和不等式往往是相辅相成的。例如:在清单3-4中,你可以设置“>=”的约束的优先级是1000,“<=”的优先级是250。这意味着,蓝色视图的左边距距离红色视图的左边距一定不能小于8个点,同时其他约束可以让距离大于8个点。在满足这些的前提下,“<=”的约束会尽量让他们之间的距离接近8个点。
内部尺寸
目前为止,我们都是通过约束来定义视图的大小和尺寸。但是,有些视图会根据内容产生一个内部尺寸。例如:按钮的内部尺寸就是标题大小加边距。
但不是所有视图都有内部尺寸的,内部尺寸有的定义了视图的高度,有的是宽度,有的是大小。表3-1给出了一些常见视图的内部尺寸的说明。
内部尺寸是根据视图当前内容来决定的。标签和按钮的内部尺寸是基于文字的数量和大小,对于一些其他的视图,他们的内部尺寸可能更复杂,像是一个没有图片的 image view 是没有内部尺寸的,而一旦设置图片之后,image view 的内部尺寸就变成了图片的大小。
text view 的内部尺寸是根据他的内容,是否可以滚动以及其他外部约束确定的。如果启动滚动,它就不存在内部尺寸。如果不启动滚动,默认情况内部尺寸是根据当前文本内容不换行计算的。如果你设置了它的宽度,那么内部尺寸定义了展现内容需要的高度。
自动布局使用一对约束来决定视图一个维度的内部尺寸。content hugging 将视图向内压,使得其适合周围,compression resistance 将内容向外拓展,使得内容不被遮挡。
这些约束使用清单3-5的不等式来实现,IntrinsicHeight 和 IntrinsicWidth 表示视图中内容的高度和宽度值。
Listing 3-5Compression-Resistance and Content-Hugging equations
// Compression Resistance
View.height >= 0.0 * NotAnAttribute + IntrinsicHeight
View.width >= 0.0 * NotAnAttribute + IntrinsicWidth
// Content Hugging
View.height <= 0.0 * NotAnAttribute + IntrinsicHeight
View.width <= 0.0 * NotAnAttribute + IntrinsicWidth
每个约束都有优先级,Compression Resistance 的约束的优先级是750,Content Hugging 的优先级是250。所以拉伸视图比压缩视图容易。这也是大部分控件希望的。
无论何时,都应该尽量使用视图的内部尺寸。这使得视图根据内容变化而改变大小。同时也减少了你需要创建的约束数量,但是你需要管理 content-hugging、compression-resistance (CHCR) 。下面是一些指导意见:
- 当需要拉伸一系列视图来填充空间的时候,如果他们有相同的 content-hugging 优先级,系统就会不知道去压缩哪一个视图。例如:当一个 label 和 textField 出现时,你会希望在 label 保持固有尺寸的同时,能拉伸 textField 。 此时,你就需要确保 textFiled 的水平 content-hugging 优先级小于 label 的。
- 当不可见背景意外超出视图的内部尺寸,一个畸形和奇怪的布局就会出现。例如:label 和 button ,这个变形可能不是很明显,因为他们仅仅是文字错位。为了防止不想要的拉伸,就需要增加 content-hugging 的优先级。
- baseline 只有当视图的高度为内部尺寸的高度时才有效,当视图在竖直方向被拉伸或者压缩,这个属性就失效了。
- 避免给视图的 CHCR 设置 1000 优先级,因为视图错位总是比发生冲突要好。如果你需要视图总是保持其内部大小,设置他们的 CHCR 的优先级为999,这样往往就能保持他们内部大小,同时给出一个紧急压力阀,以防你的视图大于或小于预期的环境。
内部尺寸和设置尺寸
内部尺寸可以算作是自动布局的输入,当视图有内部尺寸的时候,系统就会自动生成大小相关的约束来计算布局。
设置尺寸相当于自动布局的输出,他是根据视图的约束计算得出的尺寸。如果该视图使用自动布局来布局子视图,则系统会自动计算并给出这个设置尺寸。
名词解释
自动布局中总是以点为单位。但是,具体的含义需要根据属性和布局方式而定。下表给出了相应的说明: