当内部或外部条件发生变化的时候,自动重新计算视图的位置和大小。
对于ios应用,可以触发自动布局的变化可以分为外部变化和内部变化:
外部的变化可以是设备的方向改变,ipad的分屏,不同尺寸的屏幕等。
内部的变化可以是内容的变化(加载不同的图片等),应用支持动态类型(允许用户修改视图),国际化(添加对多国使用者的支持)等。
ios的自动布局与基于frame布局的关系,就像安卓的相对布局与绝对布局的关系。基于frame的布局,也就是通过子视图相对于父视图坐标的绝对位置和绝对大小,这种布局的好处在于坐标位置确定,但是在面对多种尺寸屏幕,动态内容等问题时显得非常的麻烦。而自动布局不同,它描述的是视图与视图之间的位置关系,无论屏幕大小或者内容如何改变,视图之间的相对位置不变,视图排版就不会乱掉。
自动布局需要通过约束constraint来实现。下面,先说明使用约束来实现自动布局的原理,后说明如何创建、修改、查看约束。
布局,实际上是关于视图位置的一系列方程。每个方程就是一个约束,一个约束通常描述的就是两个属性之间的数值关系,比如长是宽的多少倍、view2的起始位置是view1的计数位置加10等。
图中的方程就是一个典型的约束。要约束的对象就是RedView(item1)的leading属性(Attribute1),约束的具体内容就是BlueView(item2)的trailing属性(Attribute2)乘上1.0再加上8.0。这个1.0叫做乘积因子multiplier,8.0是个常量constant。这几个量的意义要明确,因为之后在创建约束的时候离不开这几个量。
视图的四个边界:leading(左边界)、trailing(右边界)、top(上边界)、bottom(下边界)
视图的大小:height(高)、width(宽)
视图的中心:center(中心坐标)
首先,分清自动布局的属性可以分为位置相关属性和大小相关属性。这两个属性之间不应该有任何关联,具体要遵守以下原则:
不要用位置属性去约束大小属性。
不要给位置属性赋常量值。
位置属性的约束中乘积因子只能是1.0。
不要用水平方向的位置属性去约束垂直方向的位置属性。
不要用leading或trailing属性来约束left或right属性。
对于以方程形式存在的约束,并不是把等号右边的值赋给等号左边的值,而是通过解方程,使得等号两边相等。
因此,当方程有多个解,也就是说可能有多种布局方案可以满足这些约束,这时候,就要从中确定一种方案,并且一直运用下去。比如说:整数的乘积因子比小数乘积因子优先,正数乘积因子比负数优先,views的排列按照从上至下,从左至右排列等等。这些规则需要自己去定,并且在一个项目中一直遵守。
使用自动布局的目标是可以产生一套确定的布局,也就是说同时满足所有约束的布局方案尽可能地少。就像两点确定一条直线一样,要创建一个确定的布局,也有它的规则:两个属性确定一个view的一个维度的位置。
如果确定一个view在水平方向上的位置(三种方法,对应下图左、中、右)
方法一:同时约束左边界相对于父视图的距离,以及视图的宽
方法二:同时约束左右边界相对于父视图的距离
方法三:同时约束左边界相对于父视图的距离,以及视图的中心位置
对以上三种方法的分析:
方法一:缺点是视图的宽不可以随父视图的大小变化而变化
方法二:左右边界相对于父视图的距离固定(两个距离可以不相等)
方法三:在需要安置许多中心对齐的视图的时候,这个方法最便利
在约束的方程中,除了可以等号=,也可以有大于号>、小于号<、大于等于>=和小于等于<=。
不等式约束了属性的变动范围。可以用等式约束和不等式约束一起来确定布局。
默认情况下,自动布局会去计算出一个可以满足所有约束条件的布局方案,如果计算不出来,就会把不能满足的约束条件在控制台中输出,并且选择其中一个,不去遵守,然后再重新计算一次。
可以通过确定约束的优先级来创建可选的约束。优先级范围在1-1000。优先级为1000的约束就是必须要满足的,其他的都是可选的。自动布局会按优先级顺序来计算布局方案,如果满足不了可选的约束,就会跳过。如果跳过了某条约束以后,布局变得不确定(即有多种方案),那么布局会选择一个最接近那条约束的值来确定布局。
有些控件会有默认的大小,比如常见的Sliders有默认的宽,label、button、switch、textField等有默认的宽和高。
通常这些控件的默认大小是基于它的内容的。就像imageView设置了图片以后,默认就是图片大小。imageView和textView的默认大小,会受到内容,还有是否允许滚动,还有其他的约束条件的影响。
iOS官方建议尽可能地使用视图的默认大小,因为它允许视图自动调整大小以适应其内容的变化,也可以减少要指定的约束数目。
使用默认大小的视图,通常要联合两个与大小相关的约束,一个是“防止被拉伸”,一个是“防止被压缩”,文档上把它叫做CHCR(Content-Hugging and Compression-Resistance)。使用这两个约束的重点是,注意调整视图与视图之间CHCR的优先级的相对大小。比如说,在同一水平高度上有两个视图,都是使用默认大小的,当需要将这两个视图(都比较小)拉伸以填满父视图的宽度的时候,就会去比较这两个视图在水平方向上的“防止被拉伸”约束的优先级,谁的优先级小,相应的视图就会被拉伸,而另一个视图保持默认大小。如果这两个优先级相等,那自动布局机制就不知道应该拉伸哪个视图了。
通常,使用默认大小的视图布局出现意料之外的结果,很多时候,可能是被拉伸了。为了避免这种情况,防止控件被拉伸,可以把“防止被拉伸”约束(Content-Hugging)的优先级定大一点。
关于基线(baseline)的约束,只会对维持着默认高度的视图起作用。
有些属性是可以在写约束的时候作为参照物(约束方程中的item2)来用的:
我这里这里所说的上、下导航栏,不是狭义的navigationBar和tabBar,而是泛指当前根视图view的顶部和底部被遮挡了的部分(通常是导航栏之类)。具体来说,就是,当statusBar和半透明的navigationBar都可见的时候,topLayoutGuide就是状态栏加导航栏的部分,navigationBar不可见的时候,topLayoutGuide就是状态栏部分,如果连状态栏也隐藏了,那topLayoutGuide就没有了。而对于bottomLayoutGuide来说,当tabBarController.tabBar或toolBar可见的时候,bottomLayoutGuide就是tabBar或toolBar部分,而当tabBar不可见的时候,bottomLayoutGuide就没有了。
这两个属性本身也遵循了一个协议,UILayoutSupport协议。这个协议定义了4个属性:length、bottomAnchor、heightAnchor、topAnchor。
length是什么?对于topLayoutGuide来说,就是topLayoutGuide的下边界;对于bottomLayoutGuide来说,就是bottomLayoutGuide的上边界 。
如果在代码中直接访问viewController.topLayoutGuide会返回viewController.topLayoutGuide.length,访问bottomLayoutGuide也是返回bottomLayoutGuide.length。因此在代码中,也可以直接把topLayoutGuide和bottomLayoutGuide理解成是viewController.view的可视区域的上下边界。
不过在可视化的autoLayout当中,还是把这两者理解成viewController.view上下被遮挡的部分。
bottomAnchor、heightAnchor、topAnchor是帮助在代码实现中实现autoLayout用的,这里先不细说。
内边距就是view的边界到子视图的最小距离,这个不多说。
UIViewController有两个与内边距有关的属性,一个叫layoutMargins,一个叫layoutMarginsGuide。layoutMargins就是内边距的大小。layoutMarginsGuide指的就是内边界。
在写约束的时候,通常都是相对于内边界。默认的内边距大小是8个点。
UIViewController有一个与文本的边界有关的属性readableContentGuide。指的就是文本可以显示的最大的区域的边界。通常情况下,这个边界和内边界不会有太大的区别,只有在pad横屏的时候会有比较明显的区别。必要时,可以选择让布局相对于这个文本边界,而不相对于内边界。
约束的管理可以通过可视化的Interface Builder来实现,也可以通过代码来是实现。第二节,也就是本节,先讲InterfaceBuilder管理约束的方法。下一节讲代码的实现。
实现方法是通过直接拖动view,并且在xcode右侧的interface Builder中设置相关约束来实现的。其实约束是可以用代码来实现的。本节中先不说。下一节说。
对于之前都只是用代码来创建view的同学有必要先知道怎么创建xib和关联xib。如果已经会的这一小段略过。
(1)方法一:创建ViewController的同时创建xib。和平时新建ViewController一样,就是多勾选了Also create XIB file。这样就会自动新建一个xib,并且和所创建的ViewController已经关联了。
(2)方法二:自己创建xib,并与现有的ViewController关联起来。
通过new File 创建xib。要把xib与现有的ViewController关联起来,只要一步。
打开xib,在左侧找到这样的图标,点击它,然后到右侧找到下图中的选项卡,点击第三个,然后就看到下面的Custom Class,在Class栏中选择你要关联的ViewController。
注意:需要设定xib的根视图,方法是按住Ctrl同时,点击上面file’s Owner,拖向view。即可。
注意:之后在创建ViewController时还需要注意,不要直接[[ViewController alloc ]init],而要使用initWithNibName:bundle:方法进行初始化,指定该xib文件作为自己的根视图。
回到正题,使用约束来实现自动布局:
按住control+鼠标点击item1并拖向item2,就可以建立item1的某属性对item2的某属性的约束(如图)。
第一个工具是可以快速建立stackView。
第二个工具可以快速建立有关视图对齐的约束。
第三个工具可以快速建立有关视图的宽、高、长宽比例、视图与相邻视图间距的约束。
第四个工具可以实现约束的自动生成、约束的清除、视图布局的更新等。
使用这四个工具的注意事项:
使用这几个工具需要注意,要先点击需要约束的视图,然后再点击某个工具。
对于第一个工具,需要先全选需要加入到stackView的视图,然后点击第一个工具。
对多个视图进行多选,可以按住shift再逐个点选要选中的视图。也可以按住Alt键,一次性框选需要选中的视图。
对于第二个工具需要注意,由于该工具是关于对齐的,如果是要设置某个视图相对于根视图的对齐关系,只要选择这个视图,然后点击第二个工具。如果要设置多个视图之间的对其关系,需要先将这些视图同时选中,然后再点击第二个工具,进而进行设置。
上图就是设置某个视图相对于父视图的对其关系。勾选的是有效的,未勾线的是无效的。horizontally in container是视图中心的水平位置偏离父视图中心水平位置的量。往左是负值,往右是正值。Vertically in Container是垂直方向。
第三个工具中管理视图与相邻视图间距的地方,有一个constrain to margins,如果勾选,表明这些数值相对于内边界,如果不勾选,就相对于根视图的边界(边缘)。
还有,上面途中左边和上面的数值是生效的,右边和下面的数值不生效。也就是说线的虚实决定是否生效。
第四个工具分为两栏,一栏是selected Views,一栏是All Views in View Controller,前者表明操作的对象为选中的视图,后者表明操作的对象是这个视图控制器中的所有视图。Update Frame就是根据现有的约束更新视图的frame。Update Constraints就是根据当前视图布局来更新约束。
(1)方法一:xcode左侧
(2)方法二:查看某个视图的约束,先点击该视图,再点击xcode右侧的第五个图标,就可以看到关于该视图的所有约束(下图)。
(3)方法三:通过小标签或者线条:
选中要查看的视图,然后可以看到他四周出现了小标签和线条(代表约束),点击它们就能在右侧查看对应的约束。
标签和线的颜色是有意义的:
通常绿色表示对这个视图的约束可以唯一确定这个视图的布局。
红色表示仍未能确定唯一确定这个视图的布局,或者这个约束有冲突。
橘色表示该约束确定的是视图相对于根视图的位置。
(1)按查看约束的第一种方法或第三种方法查看约束,点击需要编辑的约束。再到xcode右侧,点击(下图)第四或第五个图标,就可以看到约束的编辑区域。
(2)按查看约束的第二种方法查看约束,点击约束旁边的edit按钮就可以编辑约束。
不论用interfaceBuilder还是用代码来实现自动布局,这些建议都是适用的。
(1)不用view的frame、bounds、center来指定view的形状
(2)尽可能地使用stackView来布局
(3)约束尽量建立在view和其相邻view之间
(4)避免给view指定固定的长和宽
(5)自动更新view的frame时要留心,尤其是对于约束条件不足的view。
(6)view的命名要有意义,方便布局时认得它们。
(7)使用leading和trailing约束,不要用left和right。
(8)在写相对于view边界的约束的时候,分两种情况:
水平方向上的约束:对于大多数的控件,约束应该相对于根视图的内边界
对于像小说阅读器这样文字布满屏幕的情况,约束应该相对于文本边界。
对于需要铺满根视图宽度的视图,约束可以相对于根视图的边界。
垂直方向上的约束:如果根视图有被导航栏、tabBar等部分遮挡了,那么约束应该相对于top margin和bottom margin。
(9)在使用autolayout来布局那些用代码创建的view的时候,要把他们的translatesAutoresizingMaskIntoConstraints属性设置为NO。这个属性如果设为YES,系统会自动为这些view生成一些约束,这些约束可能会和我们设置的约束产生冲突。
用代码创建约束有三种方法:使用NSLayoutConstraint类,使用布局anchor(锚),使用Visual Format Language可视化格式语言。
在约束的原理中有说道,约束方程的形式是这样的,item1.attribute1 = multiplier × item2.attribute2 + constant。它的组成有7部分:item1、attribute1、item2、attribute2、relationship、multiplier、constant。NSLayoutConstraint类的功能在于,利用约束方程7个量来构造约束方程,并让他成立。直接上例子最直观:
NSLayoutConstraint(item: myView, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .LeadingMargin, multiplier: 1.0, constant: 0.0).active = true
如上就建立了一个约束:myView.leading=view.leadingMargin×1.0+0.0。
这个方法来创建约束是个老方法,苹果官方推荐,iOS 9.0之后出现的新方法,使用anchor来创建约束,看下一点。
使用anchor的原理本质上和使用NSLayoutConstraint类是一样的,但是表达更加简洁了。
具体看NSLayoutAnchor类。
直接看官网文档吧Visual Format Language,那么直观。
初学自动布局,目前觉得,实现autoLayout还是使用interface Builder提供的四个工具最方便。因为这四个工具可以一次性创建多条约束。用代码的话得一条条地写。不知道随着以后用的时间长会不会改变这种想法。改了的话,再来说。
本文参考:苹果文档 Auto Layout Guides