该文章阅读的Masonry的版本为1.1.0。
0.原生实现
本来不想贴直接使用原生API的实现方式,但是文章写到一半发现没有对原生API的解释,Masonry
的实现也不太好解释,于是就添加了这个第0节,对NSLayoutConstraint
这个类稍微介绍一下。
如果我们直接用官方提供的NSLayoutConstraint
类进行布局,应该这样写:
UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:redView];
NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100.0];
NSLayoutConstraint *constraint2 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:200.0];
NSLayoutConstraint *constraint3 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:200];
NSLayoutConstraint *constraint4 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0];
NSArray<NSLayoutConstraint *> *constraints = @[constraint1, constraint2, constraint3, constraint4];
[self.view addConstraints:constraints];
复制代码
- 首先把要设置
autolayout
的控件的translatesAutoresizingMaskIntoConstraints
属性设置为NO
,这个属性默认是YES
。 - 然后通过
NSLayoutConstraint
类提供的工厂方法constraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant:
创建约束对象。 - 最后利用控件的
addConstraints:
方法,将约束对象添加到控件上。
其实直接使用NSLayoutConstraint
添加约束并不难也很好理解,就是太冗杂了。我们重点来看NSLayoutConstraint
类实例化对象的工厂方法:
+(instancetype)constraintWithItem:(id)view1
attribute:(NSLayoutAttribute)attr1
relatedBy:(NSLayoutRelation)relation
toItem:(nullable id)view2
attribute:(NSLayoutAttribute)attr2
multiplier:(CGFloat)multiplier
constant:(CGFloat)c;
复制代码
看这个方法的目的是理解其各个参数的意义,这样在接下里看Masonry
时,就能好理解的多:
- 苹果官方文档中给出的约束公式是:
view1.attr1 <relation> multiplier × view2.attr2 + c
。 view1
:是指要设置的约束的目标视图。attr1
:是指view1要设置约束的属性,是视图的顶部、宽度,还是其他什么的。relation
:是指两个视图属性的关系,一共有三种,分别是不大于、等于和不小于。view2
:是指要设置约束的参考视图。attr2
:是指view2要设置约束的属性。multiplier
:是指约束要乘的倍率。c
:是指约束要加的大小
1.使用Masonry
使用Masonry
布局就简洁很多,同样的布局如下:
UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(200.0, 100.0));
make.top.equalTo(self.view).offset(50.0);
make.centerX.equalTo(self.view);
}];
复制代码
2.创建布局环境
通过上一节可以看到所有的布局都是在mas_makeConstraints:
方法中进行的,点击方法进入查看实现:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
// 用autolayout布局要设置为NO
self.translatesAutoresizingMaskIntoConstraints = NO;
// 创建约束创建者对象
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
// 通过block回调约束创建者对象
block(constraintMaker);
// 返回所有添加的约束
return [constraintMaker install];
}
复制代码
在这个方法的实现中,就是创建一个管理约束的对象,然后通过block回调用以添加约束,添加完成后设置添加的约束。
这个方法更像是创建了一个用于设置约束的环境,用户只需要通过block设置约束即可,其他的都不需要操心。
3.添加约束
我们可以通过MASConstraintMaker
类对象提供的属性为控件添加各种各样的约束,我们选取一个来查看其具体实现:
make.top.equalTo(self.view).offset(50.0);
复制代码
3.1 属性
在上一节中,我们已经知道了对象make
是MASConstraintMaker
类型,所以直接进入MASConstraintMaker
类中查看其top
属性的实现:
- (MASConstraint *)top {
// 调用了另一个方法,并把要设置约束的位置传递过去
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
复制代码
继续点击查看:
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
// 也是调用了另一个方法,并把要设置约束的位置传递过去
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
复制代码
接着点击查看:
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
// 根据当前视图和设置的属性创建视图属性封装对象
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
// 根据视图属性封装对象创建视图约束封装对象
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
// 如果传入的约束对象是MASViewConstraint类及其子类
if ([constraint isKindOfClass:MASViewConstraint.class]) {
// 利用已经添加的约束对象和新添加的约束对象创建多视图约束封装对象
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
// 并替换掉数组中保存的对应的约束对象
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
// 返回多视图约束封装对象
return compositeConstraint;
}
// 如果没有已添加的约束对象,就直接添加到数组中保存
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
// 返回视图约束封装对象
return newConstraint;
}
复制代码
在这一步中,可以看到会返回一个MASViewConstraint
类或MASCompositeConstraint
类的对象,这两个类都是MASConstraint
的子类,可以说是“兄弟类”,它们保存了约束属性及其所在的视图,也就是 view1
和 attr1
。
3.2 关系
- (MASConstraint * (^)(id))equalTo {
// 返回一个返回值类型为id,参数类型是id的block
return ^id(id attribute) {
// 调用下面的方法添加约束关系
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
复制代码
在上面的代码中我们看到make.top
返回的是MASConstraint
的子类MASViewConstraint
或MASCompositeConstraint
类,所以在equalTo
这个方法中调用的其实是MASViewConstraint
类或MASCompositeConstraint
类的对象方法equalToWithRelation
。
先看MASViewConstraint
类对这个方法的实现:
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
// 返回一个返回值为id类型,参数为id和NSLayoutRelation类型的block
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
// 如果传入的属性是一个数组
// 不能重复设置约束关系
NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
// 创建变量保存视图约束封装对象
NSMutableArray *children = NSMutableArray.new;
// 遍历传入的属性
for (id attr in attribute) {
// 创建视图约束封装对象,设置约束属性和约束关系
MASViewConstraint *viewConstraint = [self copy];
viewConstraint.layoutRelation = relation;
viewConstraint.secondViewAttribute = attr;
// 保存视图约束封装对象
[children addObject:viewConstraint];
}
// 利用上面创建的视图约束封装对象数组创建多视图约束封装对象
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self.delegate;
// 替换掉当前视图约束对象
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
// 返回多视图约束封装对象
return compositeConstraint;
} else {
// 如果传入的属性不是数组类型的
// 不能重复设置约束关系
// 如果已经设置了约束关系,必须和原约束关系相同,并且属性必须是NSValue类型的
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
// 保存约束关系和约束属性
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
// 返回当前类对象
return self;
}
};
}
复制代码
接着看一下MASViewConstraint
类中两个setter
的实现:
- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
// 除了保存约束关系,还保存了是否设置了约束关系
_layoutRelation = layoutRelation;
self.hasLayoutRelation = YES;
}
复制代码
这个方法没啥好说的,就是保存了一下。
- (void)setSecondViewAttribute:(id)secondViewAttribute {
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
// 如果参数是NSValue类型的,根据值的类型设置不同的属性
[self setLayoutConstantWithValue:secondViewAttribute];
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
// 如果参数是UIView类型的,就生成 view2 的视图属性封装对象,其中 attr2 和 view1 的 view1 相同,并保存
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
// 如果参数是MASViewAttribute类型的,就直接保存
_secondViewAttribute = secondViewAttribute;
} else {
// 只允许输入 NSValue 、 UIView 和 MASViewAttribute 这三种类型的数据
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}
复制代码
这个方法中的三个条件其实分别对应下面的三种输入情况:
make.width.equalTo(@200);
make.centerX.equalTo(self.view);
make.left.equalTo(self.view.mas_left);
在这一步中,实际做的工作就是保存布局关系和约束的参考视图,也就是 relation
、 view2
以及 attr2
。但有一点需要注意的是Masonry
通过将方法的返回值设置成一个返回值是当前类类型的block,来实现链式编程的效果。
3.3 常数
- 首先看父类
MASConstraint
的实现:
- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}
复制代码
是不是熟悉的配方?是不是熟悉的味道?和equalTo
一样,都是通过返回一个返回值是id
类型的block来实现链式编程的效果。其中的实现也很简单,就是保存了一下传入的参数。
- 再看其子类
MASViewConstraint
中的实现:
- (void)setOffset:(CGFloat)offset {
self.layoutConstant = offset;
}
复制代码
- (void)setLayoutConstant:(CGFloat)layoutConstant {
_layoutConstant = layoutConstant;
#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
if (self.useAnimator) {
[self.layoutConstraint.animator setConstant:layoutConstant];
} else {
self.layoutConstraint.constant = layoutConstant;
}
#else
self.layoutConstraint.constant = layoutConstant;
#endif
}
复制代码
在这个子类中就是保存了一下传入的常数。
- 还有子类
MASCompositeConstraint
中的实现:
- (void)setOffset:(CGFloat)offset {
// 遍历所有视图属性封装对象,设置参数
for (MASConstraint *constraint in self.childConstraints) {
constraint.offset = offset;
}
}
复制代码
这一步就是设置常数,也就是 c
。
到此为止,约束所需的参数都设置完成,下面就是设置约束了。
4.设置约束
在第 2 节中 mas_makeConstraints:
方法的最后一句就是设置约束:
return [constraintMaker install];
复制代码
我们点进 install
方法中:
- (NSArray *)install {
// 如果设置了移除现有约束
if (self.removeExisting) {
// 获取当前视图所有已设置的约束
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
// 遍历得到每个约束并移除
for (MASConstraint *constraint in installedConstraints) {
[constraint uninstall];
}
}
// 获得所有要设置的约束
NSArray *constraints = self.constraints.copy;
// 遍历得到每个约束
for (MASConstraint *constraint in constraints) {
// 设置是否更新已存在的约束
constraint.updateExisting = self.updateExisting;
// 设置约束
[constraint install];
}
// 移除掉属性中保存的约束
[self.constraints removeAllObjects];
// 返回刚刚设置的所有约束
return constraints;
}
复制代码
逻辑很明白,就是先移除之前已经添加的约束,在设置要设置的约束。
然后看一下解除约束的方法实现:
- (void)uninstall {
// 这个为了兼容 iOS8 之前的版本,因为属性 active 是iOS8 才开始生效的
if ([self supportsActiveProperty]) {
// 将约束的活动状态设置为NO
self.layoutConstraint.active = NO;
// 从保存集合中移除
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
// 返回
return;
}
// 如果是 iOS8 之前的版本
// 移除掉视图的约束
[self.installedView removeConstraint:self.layoutConstraint];
// 属性置空
self.layoutConstraint = nil;
self.installedView = nil;
// 从保存集合中移除
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
}
复制代码
接着看设置约束的方法实现:
- (void)install {
// 先判断当前约束是否已被设置,如果设置了就不需要继续向下执行了
if (self.hasBeenInstalled) {
return;
}
// 同样是为了兼容 iOS8
if ([self supportsActiveProperty] && self.layoutConstraint) {
// 将约束的活动状态设置为YES
self.layoutConstraint.active = YES;
// 将约束添加到集合中保存
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}
// iOS7 之前版本的设置方式
// 获取设置的各个参数
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
// 如果设置了像 make.left.equalTo(@10) 这样的约束
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
// view2 就是 view1 的父视图
secondLayoutItem = self.firstViewAttribute.view.superview;
// attr2 就是 attr1
secondLayoutAttribute = firstLayoutAttribute;
}
// 创建约束对象
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
// 设置优先级和key
layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;
if (self.secondViewAttribute.view) {
// 如果设置了 view2
// 获取 view1 和 view2 的公共父视图
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
// 他们必须有公共父视图
NSAssert(closestCommonSuperview,
@"couldn't find a common superview for %@ and %@",
self.firstViewAttribute.view, self.secondViewAttribute.view);
// 要设置约束的视图就是他们的公共父视图
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
// 如果设置的属性为 size 类型的, 要设置约束的视图就是 view1
self.installedView = self.firstViewAttribute.view;
} else {
// 否则,要设置约束的视图就是 view1 的父视图
self.installedView = self.firstViewAttribute.view.superview;
}
// 创建变量保存之前添加的约束
MASLayoutConstraint *existingConstraint = nil;
// 如果需要更新约束
if (self.updateExisting) {
// 获取之前的约束
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// 如果有之前的约束
// 更新约束
existingConstraint.constant = layoutConstraint.constant;
// 保存当前约束
self.layoutConstraint = existingConstraint;
} else {
// 如果没有之前的约束
// 向视图设置约束
[self.installedView addConstraint:layoutConstraint];
// 保存当前约束
self.layoutConstraint = layoutConstraint;
// 将约束添加到集合中保存
[firstLayoutItem.mas_installedConstraints addObject:self];
}
}
复制代码
这个方法里面还有个比较两个约束是否相似的方法:
- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
// 遍历要设置约束视图的所有已设置的约束
for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
// 如果已设置的约束不是 MASLayoutConstraint 类型的,跳过
if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
// 如果已设置的约束的 view1 和要设置约束的 view1 不相同,跳过
if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
// 如果已设置的约束的 view2 和要设置约束的 view2 不相同,跳过
if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
// 如果已设置的约束的 attr1 和要设置约束的 attr1 不相同,跳过
if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
// 如果已设置的约束的 attr2 和要设置约束的 attr2 不相同,跳过
if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
// 如果已设置的约束的 relation 和要设置约束的 relation 不相同,跳过
if (existingConstraint.relation != layoutConstraint.relation) continue;
// 如果已设置的约束的 multiplier 和要设置约束的 multiplier 不相同,跳过
if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
// 如果已设置的约束的优先级和要设置约束的优先级不相同,跳过
if (existingConstraint.priority != layoutConstraint.priority) continue;
// 返回幸存者,也就是除了常数 c 其他参数都要相同
return (id)existingConstraint;
}
// 如果没有符合条件的就返回空对象
return nil;
}
复制代码
5.总结
- 首先我们会调用
View+MASAdditions.h
分类中的mas_makeConstraints:
方法用来创建设置约束的环境,以及获取约束工厂类MASConstraintMaker
的对象make
。 - 接着,我们调用
make
的属性设置约束的属性attr1
。例如make.top
。这个时候在make
对象内部:- 首先会创建一个视图属性封装类
MASViewAttribute
对象,里面封装着view1
和attr1
。 - 再用上一步创建的对象创建视图约束封装类
MASViewConstraint
对象并返回,这个对象就负责管理着NSLayoutConstraint
类对象。
- 首先会创建一个视图属性封装类
- 然后,我们调用上一步返回的对象方法来设置约束的关系、
view2
和attr2
。例如make.top.equalTo(redView.mas_bottom)
。这时在MASViewConstraint
对象内部:- 首先会创建一个视图属性封装类
MASViewAttribute
对象,里面封装着view1
和attr1
。 - 然后将上一步创建的对象保存到当前对象的属性中,并返回当前对象。
- 首先会创建一个视图属性封装类
- 接着,再设置约束的常数。例如,
make.top.equalTo(redView.mas_bottom).offset(30.0);
。在这一步中,MASViewConstraint
对象只是保存了一下传入的参数。 - 设置完约束后,在
mas_makeConstraints:
方法中,就会调用make
对象的install
方法。make
对象会调用刚才创建的MASViewConstraint
对象的install
方法。 - 最后,在
MASViewConstraint
对象的install
方法中,通过刚才设置的约束的参数创建NSLayoutConstraint
对象,并添加到视图上。