苹果在 iOS 6 时推出了自动布局(Auto Layout)。在自动布局逐步完善的过程中,苹果也推出了诸如:Size Class、Stack View、UILayoutGuide 等技术,但是它们的本质都是基于自动布局。
来源
1997 年,Alan Boring,Kim Marriott,Peter Stuckey 等人在它们发表的论文《Solving Linear Arithmetic Constraints for User Interface Applications(用户界面应用线性算术约束的求解)》中提出了解决布局问题的 Cassowary constraints-solving 算法实现。
2011 年,苹果将 Cassowary 算法应用到了自家的布局引擎 Auto Layout 中。
Cassorwary
Cassowary 能够有效解析 线性等式系统 和 线性不等式系统,用来表示用户界面的相等关系和不等关系。基于此,Cassowary 开发了一种规则系统,可以通过 约束 来描述视图之间的关系。约束就是规则,能够表示出一个视图相对于另一个视图的位置。
由于 Cassowary 算法的先进性,很多编程语言都实现了对应的库,如:JavaScript、.NET、Java、SmallTalk、C++。当然也包括OC和Swift.
约束
Cassowary 的核心是基于 约束(Constraint) 来描述视图之间的关系。约束本质上就是一个方程式:
item1.attribute1 = multiplier × item2.attribute2 + constant
下面我们通过一个简单的约束来介绍约束方程式。
该约束表示红色视图的左边界在蓝色视图的右边界再往右 8 个像素点。注意,这里的 =
并不是赋值的意思,而是相等的意思。
在自动布局系统中,约束不仅可以定义两个视图之间的关系,还可以定义单个视图的两个不同属性之间的关系,如:在视图的高度和宽度之间设置比例。一般而言,一个视图需要四个约束来决定其大小和位置。
约束规则
上述约束方程式主要描述了两个视图属性之间的关系。那么,我们来看一下 iOS 定义了哪些属性和关系。
属性
苹果使用 NSLayoutAttribute
类型的枚举值来表示布局属性,其主要包含以下这些属性:
typedef NS_ENUM(NSInteger, NSLayoutAttribute) {
// 视图位置
NSLayoutAttributeLeft = 1,
NSLayoutAttributeRight,
NSLayoutAttributeTop,
NSLayoutAttributeBottom,
// 视图前后
NSLayoutAttributeLeading,
NSLayoutAttributeTrailing,
// 视图宽高
NSLayoutAttributeWidth,
NSLayoutAttributeHeight,
// 视图中心
NSLayoutAttributeCenterX,
NSLayoutAttributeCenterY,
// 视图基线
NSLayoutAttributeLastBaseline,
NSLayoutAttributeFirstBaseline NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeLeftMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeRightMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeTopMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeBottomMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeLeadingMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeTrailingMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeCenterXWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeCenterYWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
// 占位符,在与另一个约束的关系中没有用到某个属性时可以使用占位符
NSLayoutAttributeNotAnAttribute = 0
};
NSLayoutAttributeLeft
表示视图的最左边;值得注意的是,NSLayoutAttribute
有类似 NSLayoutAttributeLeft
和 NSLayoutAttributeLeftMargin
这样的枚举。两者的区别是:
-
NSLayoutAttributeLeftMargin
表示视图的左边,距离最左边有多大的 margin 与视图的layoutMargins
有关。
关于 layoutMargins
我们会在下文提到。
关系
苹果使用 NSLayoutRelation
类型的枚举值来表示属性关系,其主要包含以下这些关系:
typedef NS_ENUM(NSInteger, NSLayoutRelation) {
NSLayoutRelationLessThanOrEqual = -1,
NSLayoutRelationEqual = 0,
NSLayoutRelationGreaterThanOrEqual = 1,
};
约束层级
约束描述两个视图之间的关系,但是前提是:两个视图必须属于同一个视图层级结构。
这种层级结构有两种:
- 一个视图是另一个视图的视图
- 两个视图在一个窗口下有一个非
nil
的公共祖先视图。
约束优先级
约束具有优先级。当布局引擎计算布局时,会按照优先级从高到低的顺序逐个计算。如果发现一个可选的约束无法被满足时,就会跳过这个约束,计算下一个约束。有时候,即使一个约束无法被正好适配,它依然可以影响布局。
苹果默认定义了 4 种优先级枚举值。除此之外,苹果允许创建其他的优先级,但是其范围必须在 1~1000 之间。
static const UILayoutPriority UILayoutPriorityRequired = 1000;
static const UILayoutPriority UILayoutPriorityDefaultHigh = 750;
static const UILayoutPriority UILayoutPriorityDefaultLow = 250;
static const UILayoutPriority UILayoutPriorityFittingSizeLevel = 50;
约束创建
关于约束的创建,苹果提供了 Interface Build,可以实现以非编程的方式创建约束。但是在大型项目中,我们主要还是以编程的方式创建约束。
以编程方式创建约束的方式主要有三种:
- 约束构造器(NSLayoutConstraint)
- 布局锚点(Layout Anchors)
- 可视化格式语言(Visual Format Language, VFL)
其中较常用的NSLayoutConstraint,很多三方库都是基于NSLayoutConstraint进行封装的, 比如自动布局的三方框架Masonry:https://github.com/Masonry/Masonry。
NSLayoutConstraint
苹果使用 NSLayoutConstraint
类型表示约束。NSLayoutConstraint
类提供了一个构造方法可以直接创建约束。构造方法的各个参数对应着约束方程式的各个项。
+ (instancetype)constraintWithItem:(id)view1
attribute:(NSLayoutAttribute)attr1
relatedBy:(NSLayoutRelation)relation
toItem:(id)view2
attribute:(NSLayoutAttribute)attr2
multiplier:(CGFloat)multiplier
constant:(CGFloat)c;
布局因素
布局的构建主要由 布局引擎(Layout Engine)完成。毫无疑问,视图是构建布局的作用对象。约束作为自动布局的核心,是构建布局的重要依据。除此之外,布局引擎在构建布局时还会参考以下这些因素:
- 约束优先级(Constraint Priorities)
- 内容优先级(Content Priorities)
- 固有内容尺寸(Intrinsic Content Size)
- 尺寸约束(Sizing Constraints)
- 水平对齐(Horizontal Alignment)
- 垂直对齐(Vertical Alignment)
- 基线对齐(Baseline Alignment)
- 对齐矩形(Alignment Rect)
尺寸约束
事实上,在上文 约束创建 中创建的约束就已经包含了尺寸约束。这里的再次提到尺寸约束,主要是针对 Self-Sizing 的视图。
比如,我们可以通过自动布局自动计算 TableView 的 Cell 高度。不过,默认情况下未启用该功能。
默认情况下,TabelView 的 Cell 高度由协议声明的 tableView:heightForRowAtIndexPath:
方法确定。除此之外,我们可以通过对 TabeView 的两个属性赋值,从而启用 Self-Sizing 功能,如下所示:
tableView.estimatedRowHeight = 85.0
tableView.rowHeight = UITableViewAutomaticDimension
接下来,我们需要在 TableView 的 Cell 的 contentView
中进行布局。为了能让布局引擎自动计算出 Cell 的高度,我们必须对 contentView
的子视图在垂直方向上定义一系列完善的约束,尤其是高度约束。在布局引擎计算高度过程中,它会优先使用尺寸约束,其次它会使用固有内容尺寸。
固有内容尺寸 & 内容优先级
iOS 中有部分视图具有固有内容尺寸(intrinsic content size),固有内容尺寸就是视图内容和边距所占据的尺寸。比如,UIButton
的固有内容尺寸等于 Title 的尺寸加上内容边距(margin)。
具有固有内容尺寸的视图有以下这些:
View | Intrinsic Content Size |
Sliders | Defines only the width (iOS).Defines the width, the height, or both—depending on the slider’s type (OS X). 仅定义宽度(iOS)。根据滑块的类型(OSX)定义宽度和/或高度。 |
Labels, buttons, switches, and text fields | Defines both the height and the width. 可同时定义高度和宽度。 |
Text views and image views | Intrinsic content size can vary. 内部内容大小可能会有变化。 |
固有内容尺寸的大小受很多因素的影响。以 UITextView
为例,其固有内容尺寸的大小取决内容、是否启用了滚动、以及应用于 UITextView
的其他约束。如果可以滚动,则没有固有内容尺寸,如果不可滚动,则取决于所有文字的尺寸。
固有内容尺寸的大小还受内容优先级的影响,内容优先级有以下两个方面:
Content Hugging Priority
Content Compression Resistance Priority
Content Hugging Priority
:表示一个视图抗拉伸的优先级,数值越高优先级越高,越不容易被拉伸。
Content Compressing Priority
:表示一个视图抗压缩的优先级,数值越高优先级越高,越不容易被压缩。
默认情况下,视图的 Content Hugging Priority
值是 250
,Content Compression Resistance Priority
值是 750
。因此,拉伸视图比压缩视图更容易。
Intrinsic Content Size 与 Fitting Size 的关系
Intrinsic Content Size 是布局引擎的输入,基于此可以生成约束,并最终生成布局; Fitting Size 则相反,它是布局引擎的输出,是基于约束生成的布局结果。
对齐方式
对齐方式有三种类型:
- 水平对齐
- 垂直对齐
- 基线对齐
对于前两者,通过前文的描述我们也算是有所了解了。水平对齐,用于在 X 轴上产生约束;垂直对齐,用于在 Y 轴上产生约束。
基线对齐则是文本专有的一种专有的对齐方式。基线对齐包括 firstBaseline
和 lastBaseline
两种对齐方式。如下所示:
对齐矩形
在自动布局中,我们可能会认为约束是使用 frame
来确定视图的大小和位置的,但实际上,它使用的是 对齐矩形(alignment rect)。在大多数情况下,frame
和 alignment rect
是相等的,所以我们这么理解也没什么不对。
那么为什么是使用 alignment rect
,而不是 frame
呢?
有时候,我们在创建复杂视图时,可能会添加各种装饰元素,如:阴影,角标等。为了降低开发成本,我们会直接使用设计师给的切图。如下所示:
其中,(a) 是设计师给的切图,(c) 是这个图的 frame
。显然,我们在布局时,不想将阴影和角标考虑进入(视图的 center
和底边、右边都发生了偏移),而只考虑中间的核心部分,如图 (b) 中框出的矩形所示。
对齐矩形就是用来处理这种情况的。UIView
提供了方法可以实现从 frame
得到 alignment rect
以及从 alignment rect
得到 frame
。
// The alignment rectangle for the specified frame.
- (CGRect)alignmentRectForFrame:(CGRect)frame;
// The frame for the specified alignment rectangle.
- (CGRect)frameForAlignmentRect:(CGRect)alignmentRect;
此外,系统还提供了一个简便方法,有 UIEdgeInsets
指定 frame
和 alignment rect
的关系。
// The insets from the view’s frame that define its alignment rectangle.
- (UIEdgeInsets)alignmentRectInsets;
// 如果希望 alignment rect 比 frame 的下边多 10 个点,可以这些写:
- (UIEdgeInsets)alignmentRectInsets {
return UIEdgeInsetsMake(.0, .0, -10.0, .0);
}
布局渲染
iOS 的布局渲染可以分为三个阶段,如下所示:
- 约束更新(Constraints Update)
- 布局更新(Layout Update)
- 显示重绘(Display Redraw)
其中,每一步都是依赖前一步操作。显示重绘依赖布局更新,布局更新依赖约束更新。
约束更新
约束更新是 自下而上(从子视图到父视图)进行的。我们可以通过调用 setNeedsUpdateConstraints
来触发约束更新。当然,我们对布局因素(约束/内容优先级、约束、固有内容尺寸…)作出的任何修改都会 自动触发 setNeedsUpdateConstraints
方法。
对于自定义视图,我们可以在约束更新阶段重写 updateConstraints
来为视图增加需要的本地约束。
布局更新
布局更新是 自上而下(从父视图到子视图)进行的。事实上,布局更新操作是通过设置 frame
(OS X )或 center
和 bounds
(iOS)将布局引擎的计算结果应用到视图上。我们可以通过条用 setNeedsLayout
来触发布局更新。这并不会立刻应用布局,而是延迟进行处理。因为所有的布局请求将会被合并到一个布局操作中。这种延迟处理的过程被称为 Deferred Layout Pass
。
我们可以调用 layoutIfNeeded
(iOS) 或 layoutSubtreeIfNeeded
(OS X)强制系统立即更新视图树的布局。如果我们下一步的操作依赖于更新后视图的 frame
,这将非常有用。
对于自定义视图,我们可以布局更新阶段重写 layoutSubviews
(iOS)或 layout
(OS X)来获取控制布局变化的所有权。
显示重绘
显示重绘时 自上而下(从父视图到子视图)进行的。我们可以通过调用 setNeedsDisplay
来触发显示重绘,这回导致所有的调用都被合并到一起延迟重绘。
对于自定义视图,我们可以在显示重绘阶段重新 drawRect:
来获取自显示过程的所有权。
注意事项
要注意的是,这三个阶段并不是单向的。基于约束的布局是一个迭代的过程。布局更新可以基于之前的布局来对约束作出修改,而这将再次触发约束更新,并紧接另一个布局更新。这可以被用来创建高级的自定义视图布局。但是如果我们每一次调用的自定义 layoutSubviws
都会导致另一个布局操作的话,将会陷入无限循环中。