Autolayout笔记:基本概念
AutoLayout使用非常简单,Xcode的支持也非常直观。但是因为和之前的方式有很大的不同,学习曲线比较陡峭,所以给新手造成一些心理负担,下面我将通过一些列的文章来给大家简单的讲解一下Autolayout的基本用法。
AutoLayout是一个基于约束的布局系统。描述各种约束的行为,比如一个View 距离父View上边距多少,相邻之间的间隔多少,各个View之间的宽高关系等等。这一系列的条件就是为了最终确定之前提到的传统布局中需要的东西,这个View的大小、位置。所以,当我们设置的条件不足,或是条件冲突时,就会产生异常。
Constraints
上面说了Autolayout是基于约束的,约束在iOS/OS X里面就是Constraint,让我们看看新建一个Constraint的API是怎样的:
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:item1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:item2
attribute:NSLayoutAttributeLeft
multiplier:1
constant:10];
这里的参数比较多,但仔细看可以发现它表述了这样一种关系:
Item. Attribute relatedBy toItem. Attribute * multiplier + constant。用人话解释一遍就是一个view的某个属性在位置上和另外一个view的某个属性有一定关系。举个例子比如说我想表达view1的右边和view2的左边相互紧挨着,那应该这么写
View1.右边 = view2.左边 * 1 + 0
这里的=
表示的是位置相同的意思,另外除了等于以外还有≤
和≥
同样它们都是表示位置上关系。完成一个constraint后需要把它添加到constraint所关联的iteam的共同superView上。
需要注意的是constraints是累积的,它们之间是不能互相覆盖。如果你有一个constraints,你再添加一个同类型的constraints并不覆盖前一个。
Intrinsic Content Size
苹果在使用Autolayout的原则里有一条是“视图是趋于自治的”,因此苹果鼓励用户在自定义view时自己实现Intrinsic Content Size,让view自己决定自己的大小。
Intrinsic Content Size指的是view能显示内容的最佳尺寸。例如,UILabel的最佳高度取决于它的字体大小,它的最佳宽度取决于它的字体还有所显示的内容。一个UIProgressView的最佳高度取决于它的样式,而宽度则没有要求。一个空白的UIView则在宽度和高度上都没有要求。
如果一个自定义的view的大小和其所显示的内容有关的话,就需要实现Intrinsic Content Size相关的方法。在实现Intrinsic Content Size的相关方法时你需要做两件事情:
- 重写intrinsicContentSize方法,根据view显示的内容返回合适的intrinsic content size;
- 在有些影响到Intrinsic Content Size的情况发生的时候调用
invalidateIntrinsicContentSize
例如当UILabel的文本内容变化的时候就需要调用上述的方法。如果view只有在某一个维度上有Intrinsic Content Size,那在另一个维度上返回UIViewNoIntrinsicMetric
或者NSViewNoIntrinsicMetric
就可以了。
需要注意的是intrinsic content size不能依赖view的frame。例如根据frame按比例返回intrinsic content size就是不可行的。
Sprint&struts VS. Autolayout
在Autolayout出现之前,我们使用autoresizingMask
来描述当父视图的尺寸变化时子视图的动作。现在autolayout里使用Compression Resistance 和 Content Hugging来替代它们。不过为了兼容autoresizingMask
,UIView在iOS6之后添加了一个新属性translatesAutoResizingMaskIntoConstraints
(默认为YES)来自动把autoresizingMask
转换成Constraints。
在这里可能会有人问那在Autolayout环境下还能继续使用设置frame的方式进行布局吗。答案肯定是能,因为Autolayout系统仅仅是在frame布局的过程前面添加了一个计算约束的过程,最后计算好的结果还是通过frame来应用的。
这里可能会有人有过在Autolayout环境下设置frame不生效的经历,这里简单说下为什么。如果requiresConstraintBasedLayout
被设置为YES
则只能使用Autolayout进行布局,
否则就看translatesAutoresizingMaskIntoConstraints
的设置,如果是YES则Autolayout和frame都可以进行布局,如果为NO则需要继续看是否存在intrinsicContentSize
,如果存在则只能用Autolayout布局、不存在则Autolayout和frame都可以进行布局。
你可以给view的每个维度(就是x方向和y方向)设置Compression Resistance、Content Hugging优先级。不过这个优先级仅仅对定义了intrinsic content size的view才会生效。
在iOS上可以用setContentHuggingPriority:forAxis:
和 setContentCompressionResistancePriority:forAxis:
分别设置优先级(如果是NSVIew则是setContentHuggingPriority:forOrientation:
和 setContentCompressionResistancePriority:forAxis:
)优先级的默认值是NSLayoutPriorityDefaultHigh
或 NSLayoutPriorityDefaultLow
.
最后有一点需要注意,如果设定intrinsic content size和上述的优先级的话,系统把这些解释成一系列的constraint。例如一个label的intrinsic content size为{100,30},x/y方向的compression resistance 优先级是750,Content Hugging优先级是250,这些条件会生成4个constraint:
H:[label(<=100@250)]
H:[label(>=100@750)]
V:[label(<=30@250)]
V:[label(>=30@750)]
Frame VS. Alignment Rect
Autolayout在布局的时候不是操作view的frame而是alignment rect。这两者有一些非常容易让人忽略的细小差异,在大部分情况下他们功能都一样,但在某些场合alignment rect的功能可能更强大。
大家可能都遇到过这样一种情况,一个button设置一个比较小的图片(或者图片周围有大面积的透明区)就会照成按钮看来很小,而且在布局的时候若以frame来做的话就会形成代码上是“对齐了”,但看起来就是“不整齐” 的情况,这是如果用alignment rect就会完美地解决这个问题。
用alignment rect可以很灵活地自定义用于layout的区域,大部分情况下仅仅需要重写alignmentRectInsets方法就行了,它会让你返回一个相对于frame的Inset。如果上面的方法还不能满足需求则可以重写alignmentRectForFrame:
和frameForAlignmentRect:
这两个方法,这两个方法可以让你更加灵活的根据frame来返回对应的alignment rect,需要注意的是这两个方法有互逆关系,你必须要保证frame和alignment rect能通过这两个方法互相转换。
Baseline alignment
如果对自定义的view用NSLayoutAttributeBaseline
的方式布局的话需要自己实现相关的方法。在iOS平台需要实现viewForBaselineLayout
方法,方法返回的view的bottom edge将被用作baseline,系统默认实现是直接返回view本身。注意返回的view只能是此方法调用者本身或者子view。在OS X上你需要实现baselineOffsetFromBottom来返回基于view的bottom edge的一个offset,默认返回0。
Phases of Display
使用AutoLayout之后,把view显示到屏幕上面大体分成3步。
- Update constraints,就是更新约束,把所有的约束更新到最新状态;
- Layout views,根据约束布局调整View之间的距离,大小;
- Display,调用drawRect绘制view。
下面来讲一下具体的工作流程。
Update constraints 这个过程是从底向上的(从subview
到supperview
),你可以通过调用setNeedsUpdateConstraints
来主动触发这个过程,你对constraint的任何更改都会导致系统自动调用这个方法。还可以重写updateConstraints
方法来添加应用于自定义view内部的constraint。
Layout view 这个过程是自顶向下的(从super
view到subview
),这一步实际上是应用上一步的约束的结果的过程,根据约束调整view的frame
(在OS X),或者view的center
和bounds
(在iOS)。你可以调用setNeedsLayout
方法来主动触发这个过程。
Display 这个过程是也是自顶向下的,也可以调用setNeedsDisplay
来主动触发这个过程,重写drawRect:
来添加一些自定义的绘制代码这个相信大家都已经非常熟悉了。
需要注意的是这3个步骤并不是严格按照顺序依次发生的。基于约束的布局是一个迭代的过程。layout的改变可以影响到生成上一次layout的constraint,当其他的layout改变后它能再次触发更新约束的过程。不过这个也存在死循环的风险,就是在其他的layout改变时调用你自己实现的layoutSubviews
方法。