之前在看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
按钮来查看。
下面是我创建的一个测试的工程代码,选择的是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)。
这些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),宽度高度与第一个视图相同:
如果在程序运行过程中,如果有视图的位置改变,则需要重新计算所有受影响的视图的位置。通过编码来实现位置定位固然有很大的灵活性,单页带来了很大的不便,比如我们屏幕尺寸发生变化,或者旋转屏幕,为了保持之前的布局,就需要修改其中一些视图的起始位置以及宽度高度等。虽然在UIView中有一个autoresizingMask的属性,它对应的是一个枚举的值,这个属性能够自动调整子控件与父控件中间的位置,宽高等,能够在一定程度上减轻基于frame布局带来的不便,但是autoresizingMask并只支持父子视图之间进行约束,并不支持同级视图和跨级视图的布局。对于复杂的用户界面同样需要编码进行控制。正是由于这些问题,才诞生了我们这篇文章中要讨论的Auto Layout。
3 Auto Layout
3.1 Auto Layout基本原理
Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。
所谓约束,通常是定义了两个视图之间的关系(当然你也可以一个视图自己跟自己设定约束)。如下图就是一个约束的例子,当然要确定一个视图的位置,跟基于frame一样,也是需要确定视图的横纵坐标以及宽度和高度的,只是,这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果。
约束其实是一个两个视图之间的线性关系。如图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却会有警告。一开始学习都会有这个困惑,为什么会出现这个情况呢?
原因其实就是,如果我们什么约束都不加,那么Xcode其实已经帮你自动加了约束信息了,这个约束称之为prototyping constraints
,也就是说,这个添加的Green View的横纵坐标,宽度高度都已经设定为一个值了(这个值可以在属性标签里面看到),所以,Green View的位置已经固定,自然Xcode也就不会有错误或警告了。而如果我们手动加了一条约束,那么Xcode认为你要自己添加约束了,那么在Auto Layout引擎检查约束完备性的时候自动添加的约束会被忽略,所以,这个时候因为我们只加了一个Y轴的约束条件,缺少X轴的约束条件,因此会报约束错误的提示(当然这个并不影响工程的运行,你要编译运行还是可以的,而且自动添加的约束如果没有被显示添加的约束覆盖,也还是会生效的,只是控件的位置可能会存在歧义,影响最终布局效果)。那么我们再加上其他的三个约束,好了,错误没有了。最终添加的约束如下(约束还有优先级这个非常重要的属性,后面再谈):
这四个约束可以用下面的四个等式来表示:
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并没有报错或者警告。
下表列出了一些常用控件的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容易。这也符合我们的期望,比如我们期望拉伸一个按钮大于其自身内容尺寸,而不是缩小按钮尺寸导致内容显示不全。
// 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所示。
接下来再看一个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。
4 更多例子
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
约束关系:
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 自身内容尺寸
约束:
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
约束:
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
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