×

iOS学习笔记(5)-Auto Layout基本原理

96
__七把刀__
2016.05.25 00:55* 字数 4581

之前在看MIT那个教学视频时,对iOS的界面布局点到即止,一直对Auto Layout的原理不太明了。最近重新看了遍官方的文档,终于对Auto Layout明白了一二。本文对iOS8加入的Size Class以及iOS9加入的Stack Views暂时不做过多讨论,后续有时间再补上,我是刚开始学习iOS开发,难免有理解错误的地方,请大家指正。

1 UIView的层次结构

在讨论Auto Layout前先来了解下UIView的层次结构,在iOS的视图中,最底层的是UIWindow(UIWindow当然也是从UIView继承而来),其上再是我们的View Controller的UIView,再上面则是我们自己拖拽的各种控件的UIView。要看到UIView的层次结构,可以通过Xcode的Debug View HieraHierarchy按钮来查看。


图1.1 UIView层次查看按钮

下面是我创建的一个测试的工程代码,选择的是Single View Application,工程创建好后,Xcode就已经为我们创建了一个View Controller(本文后面用VC来指代View Controller),并设置好了VC对应的Class。我在Main.storyboard的VC对应的View上面加入了一个Button和一个Label。

我们可以看到这个测试应用的UIView层次结构如下,一共四层:其中最底层为UIWindow,一个应用通常只有一个UIWindow,它是所有子视图的根视图。之上是VC对应的UIView,再上一层就是UILabel和UIButton,最上面那层是UIButtonLabel(也就是我们通常见到的 button.titleLabel)。


图1.2 UIView层次图

这些UIView的层次关系是:

UIWindow.superview -> null
UIView.superview -> UIWindow
UIButton.superview -> UIView
UILabel.superview -> UIView
UIButtonLabel.superview -> UIButton

2 Frame-based Layout

在谈论Auto Layout之前,先看看Auto Layout出现前iOS是通过什么来实现视图的布局的。在Auto Layout出现前,iOS开发要布局视图是基于frame的,如在我的笔记1中提到的那样,即只要指定视图的起始坐标(origin)以及宽度(width)和高度(height)即可确定视图在superview中的位置。如下图所示,第一个视图起始坐标为(20,20),宽度是120,高度为80;第二次视图起始坐标为(20,108),宽度高度与第一个视图相同:


图2.1 基于frame的布局

如果在程序运行过程中,如果有视图的位置改变,则需要重新计算所有受影响的视图的位置。通过编码来实现位置定位固然有很大的灵活性,单页带来了很大的不便,比如我们屏幕尺寸发生变化,或者旋转屏幕,为了保持之前的布局,就需要修改其中一些视图的起始位置以及宽度高度等。虽然在UIView中有一个autoresizingMask的属性,它对应的是一个枚举的值,这个属性能够自动调整子控件与父控件中间的位置,宽高等,能够在一定程度上减轻基于frame布局带来的不便,但是autoresizingMask并只支持父子视图之间进行约束,并不支持同级视图和跨级视图的布局。对于复杂的用户界面同样需要编码进行控制。正是由于这些问题,才诞生了我们这篇文章中要讨论的Auto Layout。

3 Auto Layout

3.1 Auto Layout基本原理

Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。

所谓约束,通常是定义了两个视图之间的关系(当然你也可以一个视图自己跟自己设定约束)。如下图就是一个约束的例子,当然要确定一个视图的位置,跟基于frame一样,也是需要确定视图的横纵坐标以及宽度和高度的,只是,这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果


图3.1 view_formula

约束其实是一个两个视图之间的线性关系。如图3.1所示,就是Blue View和Red View的一条约束。表示Red View的左边缘等于Blue View的右边缘(在从左到右书写的系统里面,leading=left,trailing=right) + 8个Point,注意,在iOS代码里面都是用的逻辑点,不是真正的物理像素点。其中关系可以是=、>=以及<=这三个的一种,当然我们的例子用的是=。

还有一个要注意的是,这里只是给出了一个约束来说明约束的基本范式,显然一个约束是不能完成Blue View和Red View的自动布局的,下一节通过实例来看看自动布局具体应该怎么操作。

3.2 Auto Layout初体验 & Fitting Size

新建一个Single View Application,然后添加一个View到视图中,
我们什么约束都不加,发现Xcode是没有任何错误和警告的。但是如果我们自己手动加了一条约束(见图3.2),Xcode却会有警告。一开始学习都会有这个困惑,为什么会出现这个情况呢?


图3.1 layout缺少约束

原因其实就是,如果我们什么约束都不加,那么Xcode其实已经帮你自动加了约束信息了,这个约束称之为prototyping constraints,也就是说,这个添加的Green View的横纵坐标,宽度高度都已经设定为一个值了(这个值可以在属性标签里面看到),所以,Green View的位置已经固定,自然Xcode也就不会有错误或警告了。而如果我们手动加了一条约束,那么Xcode认为你要自己添加约束了,那么在Auto Layout引擎检查约束完备性的时候自动添加的约束会被忽略,所以,这个时候因为我们只加了一个Y轴的约束条件,缺少X轴的约束条件,因此会报约束错误的提示(当然这个并不影响工程的运行,你要编译运行还是可以的,而且自动添加的约束如果没有被显示添加的约束覆盖,也还是会生效的,只是控件的位置可能会存在歧义,影响最终布局效果)。那么我们再加上其他的三个约束,好了,错误没有了。最终添加的约束如下(约束还有优先级这个非常重要的属性,后面再谈):


图3.2 添加完整的约束

这四个约束可以用下面的四个等式来表示:

Green View.Trailing = Superview.Trailing Margin
Green View.Leading = Superview.Leading Margin
Green View.Bottom = Bottom Layout Guide.Top + 20
Green View.Top = Top Layout Guide.Bottom + 20

注意到这里引入了几个变量,一个是Top/Bottom Layout Guide(顶部/底部导航),一个是Superview.leading/Trailing Margin(左/右边缘间距)。Top Layout Guide其实是指的根视图的顶部,模拟器在竖屏下有状态栏,状态栏默认高度为20(注:导航栏与状态栏高度不同,导航栏的竖屏默认高度为44,横屏默认高度为32),则Green View的Y坐标就是20 + 20 = 40。模拟器在横屏下没有状态栏,则Top Layout Guide.Bottom为0,则Green View的Y坐标就是20。Superview.leading Margin在竖屏时为16,横屏是为20。这几个结论可以通过打印Green View的frame值来验证:

green view frame:{{16, 40}, {343, 607}} //iPhone6 竖屏
green view frame:{{20, 20}, {627, 335}} //iPhone6 横屏

我们可以发现,Green View在横屏和竖屏的大小和位置都是不同的,但是整体布局是我们所希望的效果。这就是Auto Layout做的事情,通过这些约束,根据屏幕大小不同,屏幕方向不同来动态计算控件的大小和位置。计算方法也很简单,比如我们的例子,因为iPhone6的逻辑像素点是375 X 667,因此可以通过上面的约束计算Green View的大小。由于我们并没有设置视图的大小,视图最终呈现的大小是由Auto Layout引擎根据约束计算得到的,这个大小也称之为视图的Fitting Size,这也就是Auto Layout的便捷之处,我们不需要写任何代码去控制

width = 375 - 16*2  = 343, height = 667 - 40 - 20 = 607 //iPhone6 竖屏
width = 667 - 20*2 = 627, height = 375 - 20*2 = 335 //iPhone6 横屏

3.3 自身内容尺寸 & 抗压缩抗拉伸效果

先简化一下这两个概念:

  • 自身内容尺寸(Intrinsic Content Size,以下简称ICS)。
  • 抗压缩抗拉伸(Compression-Resistance and Content-Hugging,以下简称CRCH)

自身内容尺寸

前面我们添加了一个View到根视图中,也初次体会到了Auto Layout的强大之处,接下来我们来添加一个按钮。如下图所示,我们只添加了两个约束,Xcode居然没有报错,这可能让人纳闷了,我们并没有指定按钮的宽度和高度,那最终按钮是如何定位的呢?这就是这一节要讨论的内容,一些iOS控件如按钮控件,文本控件等其实是有一个自身内容尺寸的,这类控件会根据自身内容尺寸添加布局约束,如果我们没有显示指定控件的宽度和高度,则其自动添加的约束就会起作用。正如下图中的按钮,我们只指定了横纵坐标的约束,并没有指定宽度和高度,但是Xcode并没有报错或者警告。


图3.3 自身内容尺寸完成完整约束

下表列出了一些常用控件的ICS,由表中可以发现,label, button, text fields等都是有ICS的,而UIView和NSView是没有ICS的。

View Intrinsic content size
UIView and NSView No intrinsic content size.
Sliders Defines only the width (iOS).
Labels, buttons, switches, and text fields Defines both the height and the width.
Text views and image views Intrinsic content size can vary.

控件的ICS基于视图的当前内容。Button或者Label的ICS基于其展示的文字数目和字体大小,空的Image View是没有ICS的,只有当你添加了图片到Image View中,这个时候才会有ICS,而且尺寸大小为图片的尺寸。

Updated:视图UIView也是没有ICS的,有时候想只指定位置而不指定UIView的大小,可以在Storyboard的Size inspector中设置Intrinsic Size为Placeholder,这样便不会报错了。注意一点的是,这个设置并不影响运行时UIView的Intrinsic Size。

抗压缩和抗拉伸效果

抗压缩(Compression-Resistance) 和抗拉伸(Content-Hugging)效果是跟自身内容尺寸关联在一起的,如图3.4所示,抗压缩定义了视图抗压缩的优先级,优先级越大,表示越难压缩;抗拉伸则定义了视图抗拉伸的优先级,优先级越大,则越难被拉伸。抗压缩和抗拉伸的优先级是针对横竖两个方向的,每个方向都有一个优先级。默认的View和Button的抗压缩优先级为750,抗拉伸优先级为250。从优先级大小可以看出来,拉伸一个View比压缩一个View容易。这也符合我们的期望,比如我们期望拉伸一个按钮大于其自身内容尺寸,而不是缩小按钮尺寸导致内容显示不全。


图3.4 CRCH图示
// 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

对于两个控件来说,为了满足Auto Layout的约束,通常会优先压缩那个抗压缩优先级小的控件来适应视图的布局。

下面看一个例子,我们在视图中添加一个Label和一个Text Field。然后分别设置了Label的左上的约束和Text Field的右上约束,然后设置Label和Text Field的间距为20。约束关系我们可以看到左边的5个等式,因为Label和Text Field都有自身内容尺寸,所以这5个等式已经可以完成布局了。在这个例子中我们看到Text Field被拉伸了,而Label还是保持自身内容尺寸的,这是因为Label的默认抗拉伸优先级为251大于Text Field的默认抗拉伸优先级250,因此Label更难被拉伸,所以看到的是Text Field被拉伸了。那如果我们把Text Field的抗拉伸优先级改为252,则最终运行的界面如图3.5.4所示。


图3.5.1 默认的CRCH效果

图3.5.2 Label的CRCH优先级

图3.5.3 Text Field的CRCH优先级

图3.5.4 增大了Text Field的CRCH效果

接下来再看一个Image View的例子,可以看看自身内容尺寸和CRCH对Image View的影响。这里我在Image View里面加了个apple.jpg的图片,图片原始尺寸为241*300。开始的时候我设置Image View水平垂直居中,不设置宽度高度,则Image View的宽度和高度为图片原始尺寸241和300。然后再添加一个宽度约束,设置图片宽度为300。由于显示添加的约束的默认优先级为1000,而Image View的抗拉伸的优先级为251,所以会以显示添加的约束为准,图片宽度会被拉升到300。而如果我们把显示添加的宽度约束的优先级改成250,则图片宽度会被设置为原始宽度241。


图3.5.5 Image View的CRCH效果

4 更多例子

4.1 两个宽度相等的View


4.1两个宽度相等的View
约束关系:
1.Yellow View.Leading = Superview.LeadingMargin
2.Green View.Leading = Yellow View.Trailing + Standard
3.Green View.Trailing = Superview.TrailingMargin
4.Yellow View.Top = Top Layout Guide.Bottom + 20.0
5.Green View.Top = Top Layout Guide.Bottom + 20.0
6.Bottom Layout Guide.Top = Yellow View.Bottom + 20.0
7.Bottom Layout Guide.Top = Green View.Bottom + 20.0
8.Yellow View.Width = Green View.Width

4.2 两个宽度不等的View


图4.2 两个宽度不等的View
约束关系:
1.Purple View.Leading = Superview.LeadingMargin
2.Orange View.Leading = Purple View.Trailing + Standard
3.Orange View.Trailing = Superview.TrailingMargin
4.Purple View.Top = Top Layout Guide.Bottom + 20.0
5.Orange View.Top = Top Layout Guide.Bottom + 20.0
6.Bottom Layout Guide.Top = Purple View.Bottom + 20.0
7.Bottom Layout Guide.Top = Orange View.Bottom + 20.0
8.Orange View.Width = 2.0 x Purple View.Width

4.3 自身内容尺寸


图4.3 自身内容尺寸布局
约束:
1.Name Label.Leading = Superview.LeadingMargin
2.Name Text Field.Trailing = Superview.TrailingMargin
3.Name Text Field.Leading = Name Label.Trailing + Standard
4.Name Text Field.Top = Top Layout Guide.Bottom + 20.0
5.Name label.Baseline = Name Text Field.Baseline

这个例子跟前面提到的类似,注意并不需要设置Label和Text Field的宽度和高度。而且默认设置中,Label的抗拉伸的优先级251比Text Field的250更高,所以最终看到的效果是Text Field被拉伸了。

4.4 自适应View


图4.4 自适应View
约束:
1.Blue View.Leading = Superview.LeadingMargin
2.Blue View.Trailing = Superview.TrailingMargin
3.Blue View.Top = Top Layout Guide.Bottom + Standard (Priority 750)
4.Blue View.Top >= Superview.Top + 20.0
5.Bottom Layout Guide.Top = Blue View.Bottom + Standard (Priority 750)
6.Superview.Bottom >= Blue View.Bottom + 20.0

前面的例子都是=的约束,这个例子加了>=的约束。
注意到我们设置的>=的约束4优先级比约束3要高,约束6的优先级比约束5的高,这样如果显示状态栏(模拟器里面竖屏的时候),我们知道状态栏的高度为20,那么这时约束3满足的时候,也就是Blue View的y坐标为28(状态栏高度20+标准距离8),这时约束4也满足,因此会选择约束3这个优先级较低的约束。如果不显示状态栏(模拟器里面横屏的时候),则此时只能满足约束4,无法满足约束3。不过Auto Layout引擎会选择一个最接近的约束,也就是设置Blue View的y坐标为20。

更多例子:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSimpleConstraints.html#//apple_ref/doc/uid/TP40010853-CH12-SW1
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/ViewswithIntrinsicContentSize.html#//apple_ref/doc/uid/TP40010853-CH13-SW1

Stack View布局例子:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/LayoutUsingStackViews.html#//apple_ref/doc/uid/TP40010853-CH11-SW1

Size Class例子:
https://www.raywenderlich.com/113768/adaptive-layout-tutorial-in-ios-9-getting-started

使用代码和VFL来添加约束可以参见:
http://blog.csdn.net/pucker/article/details/45070955
http://blog.csdn.net/pucker/article/details/45093483

5 参考资料

iOS学习笔记
Web note ad 1