版本:v1.1.0
引子
首先我们根据较为完整的使用,与NSLayoutConstraints
作对比,然后再逐步分析Masonry实现自动布局的过程。
UIView *superview = self.view;
UIView *view1 = [[UIView alloc] init];
view1.backgroundColor = UIColor.redColor;
[superview addSubview:view1];
//Masonry布局
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];
//NSLayoutConstraint布局
[superview addConstraints:@[
//view1 constraints
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:padding.top],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:padding.left],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:-padding.bottom],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeRight
multiplier:1
constant:-padding.right],
]];
从布局代码量可以看出,使用Masonry布局是简洁很多的,可读性也较高。
当然,Masonry是通过链式调用
的方式简化了代码,实际上最后也是转化为NSLayoutConstraint布局
(下文会具体分析)。
分析过程
我们从上面的mas_makeConstraints
布局角度出发,分析Masonry如何做到如此简洁地完成一个view的约束。主要分析步骤为
一、给谁做约束
二、如何组成约束
三、如何完成约束
一、给谁做约束
通过查看mas_makeConstraints
方法的定义可知,约束对象类型为MAS_VIEW
,在MASUtilities.h
文件中可以找到其宏定义。
#if TARGET_OS_IPHONE || TARGET_OS_TV
#define MAS_VIEW UIView
#elif TARGET_OS_MAC
#define MAS_VIEW NSView
#endif
这里我们仅针对iOS架构来分析,可以得出:是给UIView做约束。
二、如何组成约束
我们先来分析这一句代码,研究各个链式调用
是如何最终组成一句约束的,我们可以先从make
入手
make.left.equalTo(superview.mas_left).with.offset(padding.left);
(1)make是何物?
由mas_makeConstraints
调用得知,make为MASConstraintMaker类型对象,可以理解为协助创建约束的类。那其对应的top
、left
、right
等等属性是什么概念呢?
(2)MASConstraintMaker的top
、left
等属性
① 查看定义得知,这些属性定位在MASConstraintMaker
类中,返回了MASConstraint
类型
@property (nonatomic, strong, readonly) MASConstraint *left;
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
②以left
为例,实际的操作为,构造一个MASViewConstraint
(继承自MASConstraint
)对象并返回,其firstViewAttribute
的item
为当前做约束的view,layoutAttribute为对应的NSLayoutAttributeLeft
。
- (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]) {
//replace with composite constraint
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;
}
如上我们得到MASConstraint
类型对象,可以算是确认了需要创建约束的具体属性,然后将该约束添加到constraints
数组属性中。
接下来就是equalTo(superview.mas_left).with.offset(padding.left);
操作,我们先来研究MASConstraint
的equalTo(...) / mas_equalTo(...)
操作。
(3)MASViewConstraint的equalTo(...) / mas_equalTo(...)
方法
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
#define equalTo(...) mas_equalTo(__VA_ARGS__)
- (MASConstraint * (^)(id))mas_equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
//MASViewConstraint的equalToWithRelation方法
- (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,传入attribute
返回MASConstraint
类型对象。
此方法确认了layoutRelation为NSLayoutRelationEqual
, 而传入的参数(superview.mas_left)即为secondViewAttribute
。
(4)MASViewConstraint的with/and
方法
- (MASConstraint *)with {
return self;
}
- (MASConstraint *)and {
return self;
}
这两个方法返回了self
,其实的起到了连接语义的作用,一般可以省略。接下来我们看看offset方法
(5)MASViewConstraint的offset(...)/mas_offset(...)
方法
#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))
#define offset(...) mas_offset(__VA_ARGS__)
- (MASConstraint * (^)(NSValue *value))valueOffset {
return ^id(NSValue *offset) {
NSAssert([offset isKindOfClass:NSValue.class], @"expected an NSValue offset, got: %@", offset);
[self setLayoutConstantWithValue:offset];
return self;
};
}
- (void)setLayoutConstantWithValue:(NSValue *)value {
if ([value isKindOfClass:NSNumber.class]) {
self.offset = [(NSNumber *)value doubleValue];
} else if (strcmp(value.objCType, @encode(CGPoint)) == 0) {
CGPoint point;
[value getValue:&point];
self.centerOffset = point;
} else if (strcmp(value.objCType, @encode(CGSize)) == 0) {
CGSize size;
[value getValue:&size];
self.sizeOffset = size;
} else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) {
MASEdgeInsets insets;
[value getValue:&insets];
self.insets = insets;
} else {
NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value);
}
}
- (void)setOffset:(CGFloat)offset {
self.layoutConstant = offset;
}
可以看出offset方法会根据传入的类型,去设置offset/centerOffset/sizeOffset/insets
属性。而设置这些属性,最后都会确认对应约束属性的layoutConstant
。
(6)MASViewConstraint的multipliedBy(...)
方法
- (MASConstraint * (^)(CGFloat))multipliedBy {
return ^id(CGFloat multiplier) {
NSAssert(!self.hasBeenInstalled,
@"Cannot modify constraint multiplier after it has been installed");
self.layoutMultiplier = multiplier;
return self;
};
}
因为MASViewConstraint对象初始化时layoutMultiplier为1.0时,所以layoutMultiplier为1.0时常常不写。这里也就是将layoutMultiplier
属性赋值。
(7)总结
make.left.equalTo(superview.mas_left).with.offset(padding.left);
至此,这一整句代码的流程就可以总结为:
设置MASViewConstraint对象的以下属性
firstViewAttribute.item = view1
firstViewAttribute.layoutAttribute = NSLayoutAttributeLeft
layoutRelation = NSLayoutRelationEqual
secondViewAttribute.item = superview
secondViewAttribute.layoutAttribute = NSLayoutAttributeLeft
layoutMultiplier = 1.0
layoutConstant = padding.left
对比以下代码,有没有感觉和NSLayoutConstraint
写约束时的参数一模一样
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:padding.left],
有了这些属性之后,Masonry自然就可以通过mas_makeConstraints/mas_updateConstraints/mas_remakeConstraints
方法进行添加/更新/移除后添加
相应约束。
接下来我们通过分析mas_makeConstraints
方法,了解Masonry如何完成约束。
三、如何完成约束
我们先来看mas_makeConstraints
的实现
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
//translatesAutoresizingMaskIntoConstraints 默认情况下,视图上的自动调整掩码会产生完全确定的约束视图的位置。这允许自动布局系统跟踪视图的帧。布局是手动控制的(例如,通过-setFrame:)。当您选择通过添加自己的约束来使用自动布局来定位视图时,您必须将此属性设置为NO
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
可以看到该方法首先创建了constraintMaker
,然后调用block,也就是设置并添加了block中的每一条约束到constraints
中,然后返回[constraintMaker install]
,我们就来研究MASConstraintMaker的install方法。
(1)MASConstraintMaker的install
方法
- (NSArray *)install {
//removeExisting在mas_remakeConstraints情况下为YES
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_remakeConstraints
会先卸载之前的约束
②将constraints中的每条约束执行install,然后清空constraints。此时我们应该查看MASConstraint的install方法
(2)MASConstraint的install
方法
- (void)install {
//...
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;
}
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;
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;
}
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];
}
}
代码较多,这里就简单总结一下。
① 如果secondViewAttribute =nil,且约束属性不为width和height时,默认设置secondLayoutItem为view.superview、secondLayoutAttribute为firstViewAttribute.layoutAttribute。eg make.left.equalTo(@10)
②用对应的属性构造了MASLayoutConstraint
约束对象
③确定要设置安装约束的installedView为 (1)最近的公共祖先view (2)宽高属性约束时,为firstViewAttribute.view (3)firstViewAttribute.view.superview
④判断是否是更新约束,是则更新,不是则installedView
添加该约束([self.installedView addConstraint:layoutConstraint];)。
因为MASLayoutConstraint继承自NSLayoutConstraint,所以这里installedView(MAS_VIEW类型)的addConstraint
方法,其实也就是UIView的addConstraint
方法,也就成功地为installedView添加上了该约束。
//Adds a constraint on the layout of the receiving view or its subviews.
- (void)addConstraint:(NSLayoutConstraint *)constraint API_AVAILABLE(ios(6.0)); // This method will be deprecated in a future release and should be avoided. Instead, set NSLayoutConstraint's active property to YES.
总结
(1)Masonry添加约束流程总结
通过block中的每一行创建一个约束MASViewConstraint
,添加到约束数组constraints
中,通过install
方法逐条添加到对应的installedView
上。
(2)equalTo和mas_equalTo、offset和mas_offset功能基本相同,只不过mas_equalTo
和mas_offset
添加了对数字字面量的支持(即_MASBoxValue)。
make.height.equalTo(123.0); //error: Passing 'double' to parameter of incompatible type '__strong id'
make.height.mas_equalTo(123.0);
(3)如果约束属性不为宽高,且equalTo/mas_equalTo
的对象是父view的相同属性,可省略mas_equalTo
。(即不写equalTo/mas_equalTo,默认相对父view布局)
[self.view addSubview:view1];
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
//因为view1.superview = self.view, 所以以下两句代码效果相同
make.right.mas_equalTo(self.view.mas_right).mas_equalTo(-20);
make.right.mas_equalTo(-20);
}];