Masonry 提供了简单方便的api ,供我们完成项目中的自动布局业务。
从使用的 api 开始讲
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superView);
make.left.equalTo(superView).offset(100);
make.height.and.width.equalTo(@40);
}];
调用mas_makeComstraints:
,通过在 Constraint Maker Block 中链式调用make的属性,对make进行配置,就完成了对视图的约束设置。
同时还提供了其他两个 api ,能够指定在添加约束前的行为。
-
mas_updateConstraints:
在添加约束前,如果是已经存在的约束,则对其进行更新。 -
mas_remakeConstraints:
在添加约束前,删除当前视图已经存在的约束。
入口方法
mas_makeConstraints:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
方法中首先设置了translatesAutoresizingMaskIntoConstraints
属性为no,这样我们才能在视图中添加自定义的约束。之后
- 初始化 maker,和对应视图绑定在一起,并且创建一个可变的数组
constraints
来存放需要添加的约束。 - 调用 block 完成对 maker 的配置
- install maker
配置 maker
make.left
调用 left 属性,设置添加的约束类型。会指定对应的 LayoutAttribute ,然后创建约束并添加到make的约束数组中。接下来看具体的实现代码
// MASConstraintMaker.h
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (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];
if ([constraint isKindOfClass:MASViewConstraint.class]) {...}
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
沿着调用链到constraint:addConstraintWithLayoutAttribute
方法的时候,传入的 constraint 为 nil ,所以会在使用 layoutAttribute
和视图本身生成 MASViewAttribute
对象,并用其初始化了MASViewConstraint
后,会将 maker 设置为它的代理,并将约束对象添加到约束数组中,然后返回。
make.left.equal(@40)
make.left
执行后,会返回一个 MASConstraint
对象,接着链式调用equal()
,来制定约束关系。
MASConstraint 是抽象类,提供了链式调用设置约束关系的 block 以及若干抽象接口。
它的子类是 MASViewConstraint 和 MASCompositeConstraint 分别按照各自的需求实现抽象接口。
接着说链式调用 equal 会发生什么,请看代码
// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
equal
方法的返回值是一个返回值为 MASConstraint
、参数为 attr
的闭包。所以才能实现 equal(@10)
这样的调用方式,并且因为闭包的返回值依旧是 MASConstraint
, 所以可以进行接下来的链式调用。
equal(@10)
传入的 attr 参数,即为 -equal
方法体中,返回值闭包的参数 attribute 。接下来会继续调用 self.equalToWithRelation(attribute, NSLayoutRelationEqual)
,传入约束关系和属性参数完成后续的操作。而这个方法的具体实现是在 MASConstraint
的子类中。父类相应的方法实现则是通过使用 MASMethodNotImplemented()
宏,在子类未继承这个方法时或者直接使用父类时产生异常,提示开发者需要在子类中重写这个方法。
// MASViewConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {...}
else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}
我们可以看到返回值依旧是一个 block。看到这可能会对调用的顺序有一点晕,下面梳理一下。
我们首先是调用 equal(@10)
,即调用了equal 方法体中的 block 。所以接下里会执行 self.equalToWithRelation(attribute, NSLayoutRelationEqual)
方法,即执行 equalToWithRelation
方法体中返回的 block。这个 block 返回的是 当前对象(MASViewConstraint),即为执行self.equalToWithRelation(,)
的返回值,也作为equal
返回的 block 的返回值
理清执行顺序后,接下来看实现。因为我们传入的 attr
是 @10
,所以会对 layoutRelation
和 secondViewAttribute
进行设置。
此时就完成了一条约束的配置。
MASConstraintMaker install
在调用mas_makeConstraints:
的block 后,就完成了 make 的属性设置。接下来会执行 constraintMaker install
安装已添加的约束。
// MASConstraintMaker.h
- (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;
}
如果之前为视图添加约束时调用的是 mas_remakeConstraint
那么就会先移除已有的约束,如果调用的是mas_updateConstraint
则会在遍历约束数组时,设置约束的 updateExisting
状态。
然后遍历 constraints
数组, 发送 install
消息。
MASViewConstraint install
接下来就是约束的安装过程,即最后一步为视图添加约束。
// MASViewConstraint.m
if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}
首先是将当前约束添加到 firstViewAttribute.view.mas_installedConstraints
中,保留添加的约束,之后可以对其进行删除更新操作。
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
// alignment attributes must have a secondViewAttribute
// therefore we assume that is refering to superview
// eg make.left.equalTo(@10)
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
这一步先获取之后需要的属性。然后对equal(@10)
这种情况进行处理,它指定的是当前视图和父视图的关系。相当于make.left.equalTo(superview.mas_left).offset(10)
。所以需要设定 secondLayoutItem
为 SuperView
、secondLayoutAttribute
与 firstLayoutAttribute
相同。
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;
然后用之前获取的属性,初始化 NSLayoutConstraint
的子类 MASLayoutConstraint
。
之后会确定添加约束的视图:installedView
if (self.secondViewAttribute.view) {
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) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}
此时会根据 secondViewAttribute
来设置正确的 installedView
。
- 如果存在
secondViewAttribute.view
,即equal(view.mas_left)
,就会寻找firstLayoutItem
和secondLayoutItem
视图的公共superView
,设置为installedView
- 如果是
Size
类型属性,installedView
为firstViewAttribute.view
- 其他情况会直接在把
superview
设置为installedView
MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// just update the constant
existingConstraint.constant = layoutConstraint.constant;
self.layoutConstraint = existingConstraint;
} else {
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}
最后则是对是否需要更新约束进行判断,如果存在相似的约束,那么只改变约束的常量值。对于之前不存在的约束,则直接将约束添加在 installView
中。
总结
这就是 Masonry 为视图添加约束的过程了。
总的概括下来就是, Masonry
会创建一个 MASConstraintMaker,传入入口方法的 block 中。我们通过调用maker的api 生成一个MASConstraint
对象,此时完成约束关系中第一个视图的属性,并且通过接下来的链式调用,指定约束的关系、设置约束关系中第二个视图及其布局属性。最后调用 make 的 install 消息对添加到maker 中的 约束进行 install ,初始化NSLayoutConstraint
并确定添加约束的视图,完成最后的约束添加工作。