ios自动布局原理

苹果在 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,
};


约束层级

约束描述两个视图之间的关系,但是前提是:两个视图必须属于同一个视图层级结构。
这种层级结构有两种:

  1. 一个视图是另一个视图的视图
  2. 两个视图在一个窗口下有一个非 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)。

具有固有内容尺寸的视图有以下这些:

ViewIntrinsic Content Size
SlidersDefines 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 值是 250Content 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 的布局渲染可以分为三个阶段,如下所示:

  1. 约束更新(Constraints Update)
  2. 布局更新(Layout Update)
  3. 显示重绘(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 都会导致另一个布局操作的话,将会陷入无限循环中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值