源码结构分析
Masonry是一个iOS用于自动布局的第三方框架,可以很方便的给UIView添加布局约束,从而进行自动的布局;
底层的基础API
Masonrys是在NSLayoutContraint上的基础上进行封装的,接下来先看下NSLayoutContraint的用法:
NSLayoutContraint使用示例
假设有superView和subView两个view,将subView添加到superView上,给这两个view添加约束,使这两个view在左上角重合,subView的宽高都为50;
//创建superView
UIView *superView = [[UIView alloc]initWithFrame:CGRectMake(30, 200, SCREEN_WIDTH - 60, 300)];
superView.backgroundColor = [UIColor lightGrayColor];
//创建subView
UIView *subView = [[UIView alloc]init];
subView.backgroundColor = [UIColor yellowColor];
subView.frame = CGRectMake(0, 0, 100, 100);
/**
添加约束之前,需要将视图添加到父视图上;
*/
[superView addSubview:subView];
/**
当使用autolayout布局时,需要将视图的translatesAutoresizingMaskIntoConstraints属性设置为NO,不设置的话约束白加
*/
subView.translatesAutoresizingMaskIntoConstraints = NO;
/**
七个参数
1、要约束的视图view1
2、约束类型attr1,宽,高,中心,上,下,左右
3、约束方式,等于,小于等于。。。
4、约束参照视图view2
5、参照视图的参照属性attr2,上下左右。。。
6、乘数multiplier,倍数
7、约束值constant。
约束结果:view1.attr1 等于(约束方式) view2.attr2*multiplier +constant
*/
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:subView attribute:(NSLayoutAttributeTop) relatedBy:(NSLayoutRelationEqual) toItem:superView attribute:(NSLayoutAttributeBottom) multiplier:1 constant:30];
NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:subView attribute:(NSLayoutAttributeLeft) relatedBy:(NSLayoutRelationEqual) toItem:superView attribute:(NSLayoutAttributeLeft) multiplier:1 constant:30];
NSLayoutConstraint *constraint2 = [NSLayoutConstraint constraintWithItem:subView attribute:(NSLayoutAttributeWidth) relatedBy:(NSLayoutRelationEqual) toItem:nil attribute:NSLayoutAttributeWidth multiplier:1 constant:50];
NSLayoutConstraint *constraint3 = [NSLayoutConstraint constraintWithItem:subView attribute:(NSLayoutAttributeHeight) relatedBy:(NSLayoutRelationEqual) toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:50];
/**
约束添加到哪个视图
1、自身约束。不需要依赖其他视图,如宽高数值,view2 = null,attr2 = NSLayoutAttributeNotAnAttribute。约束添加到view1上
2、相对父视图约束。约束添加到父视图上。
3、相对兄弟视图约束。约束添加到兄弟视图共同的父视图上 (别说面试造航母了, 这不就是是两个链表的第一个公共节点面试题应用场景)
View+MASAdditions 类中的mas_closestCommonSuperview方法便是找父视图
*/
[superView addConstraint:constraint];
[superView addConstraint:constraint1];
[subView addConstraint:constraint2];
[subView addConstraint:constraint3];
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 API_AVAILABLE(macos(10.7), ios(6.0), tvos(9.0));
从这个方法中可以看出,创建约束需要七个参数:
-
指定要约束的视图view1
-
约束属性attr1,宽、高、左、右、上、下...
-
约束关系relation,等于、小于等于...
-
约束参照视图view2.如果设置具体宽高数据,可以传空
-
参照视图的参照属性attr2, view2为空时传NSLayoutAttributeNotAnAttribute,传其他的也一样
-
乘数multiplier,倍数
-
约束常量值constant。
最终得到的约束结果:view1.attr1 等于(约束方式) view2.attr2multiplier +constant。*
Masonry添加约束流程解析
使用实例
假设有一个progessSlider,他的父视图为processControl上,兄弟视图有一个playeButton,都已经添加到父视图上,现在要求的centerY和left与processControl一致,right相对于button的left向左偏移10,高度为5; 使用masonry给slider添加布局约束的方式如下所示:
[progessSlider mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.left.equalTo(processControl);
make.right.equalTo(playeButton.mas_left).offset(-10);
make.height.mas_equalTo(5);
}];
打断点进行调试,看整个添加约束的流程:
- 先调用UIView的分类MASAdditions中的mas_makeConstraints方法,在该方法中调用constraintMaker的初始化方法,传给该方法要设置约束的view(progessSlider),生产maker对象,传给block回调出去,然后再调用maker的install方法;
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
- 在constraintMaker的初始化方法的初始化方法initWithView中,会记录下要设置约束的当前view,同时创建一个数组用来存储即将使用maker添加的约束:
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new;
return self;
}
- 在传给mas_makeConstraints方法的block参数中,使用回调出来的maker进行一一添加约束,添加约束属性是left和centerY,他们保持和processControl相等;
make.centerY.left.equalTo(processControl);
这里拆开来看, 第一步,先调用maker的centerY方法,而在maker中,centerY方法的调用链是centerY:-->addConstraintWithLayoutAttribute:-->constraint:nil addConstraintWithLayoutAttribute: 可以看出,最终在addConstraintWithLayoutAttribute方法中创建了一个MASViewContraint类型的约束对象newConstraint,将这个对象的代理设置为maker,添加到maker存储约束的数组中,最后返回这个对象;
- (MASConstraint *)centerY {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterY];
}
- (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]) {
//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;
}
第二步,调用返回的newConstraint对象的left方法,在MASViewConstraint中,left方法的调用链是 left:-->addConstraintWithLayoutAttribute:-->delegate的constraint:self addConstraintWithLayoutAttribute:layoutAttribute
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
因为newContraint(针对maker.view的centerY)的代理是maker,因此会调用maker的constraint:self addConstraintWithLayoutAttribute:layoutAttribute方法,在这个方法中会根据left布局属性和maker.view创建一个新的MASViewConstraint对象,然后将maker.view原来centerY约束对象和现在的left约束对象创建一个组合约束对象compositeConstraint,将compositeConstraint替换掉maker的约束数组中的left约束对象,然后将compositeConstraint的代理设置为maker,返回compositeConstraint。
第三步,调用返回compositeConstraint的equalTo方法,equalTo方法的调用链为equalTo:-->MASCompositeContraint的equalToWithRelation:-->子约束MASViewConstraint的equalToWithRelation:, 在compositeConstraint的equalToWithRelation方法中,会为newConstraint对象中每个子约束对象设置layoutRelation和secondViewAttribute。
调用equalTo返回的是(MASConstraint * (^)(id))类型block,再调用这个block,传给attribute对象,返回的是compositeConstraint;
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attr, NSLayoutRelation relation) {
for (MASConstraint *constraint in self.childConstraints.copy) {
constraint.equalToWithRelation(attr, relation);
}
return self;
};
}
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
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 {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}
其他两个条设置约束的过程跟上述相似,不再赘述。
make.left.equalTo(playeButton.mas_right).offset(-10);
make.height.mas_equalTo(5);
maker添加约束完毕,就调用maker的install方法,install方法的调用链为maker的install-->maker的约束数组中存储的每个约束的install
------------------MASConstraintMaker.m
- (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;
}
------------------MASViewConstraint.m
- (void)install {
if (self.hasBeenInstalled) {
return;
}
if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}
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];
}
}
整体设计思想
约束的抽象类MASContraint
首先,Masonry将约束封装为一个MASContraint类,这个类是一个抽象类,里面定义了一系列的抽象方法,这些抽象方法大概可以分为以下几类: (1)设置布局常量的setter方法:
- (void)setInsets:(MASEdgeInsets __unused)insets { MASMethodNotImplemented(); }
- (void)setSizeOffset:(CGSize __unused)sizeOffset { MASMethodNotImplemented(); }
- (void)setCenterOffset:(CGPoint __unused)centerOffset { MASMethodNotImplemented(); }
- (void)setOffset:(CGFloat __unused)offset { MASMethodNotImplemented(); }
(2)设置布局倍数的setter方法:
- (MASConstraint * (^)(CGFloat multiplier))multipliedBy { MASMethodNotImplemented(); }
- (MASConstraint * (^)(CGFloat divider))dividedBy { MASMethodNotImplemented(); }
(3)设置布局优先级的setter方法:
- (MASConstraint * (^)(MASLayoutPriority priority))priority { MASMethodNotImplemented(); }
(4)设置布局关系的setter方法:
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }
在MASConstraint的拓展类中,有一个updateExisting的bool值属性和实现了MASConstraintDelegate协议的delegate
@interface MASConstraint ()
/**
* Whether or not to check for an existing constraint instead of adding constraint
*/
@property (nonatomic, assign) BOOL updateExisting;
/**
* Usually MASConstraintMaker but could be a parent MASConstraint
*/
@property (nonatomic, weak) id<MASConstraintDelegate> delegate;
/**
* Based on a provided value type, is equal to calling:
* NSNumber - setOffset:
* NSValue with CGPoint - setPointOffset:
* NSValue with CGSize - setSizeOffset:
* NSValue with MASEdgeInsets - setInsets:
*/
- (void)setLayoutConstantWithValue:(NSValue *)value;
@end
而在在MASConstraint的分类abstract中,定义了一个很重要的抽象方法addConstraintWithLayoutAttribute:
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
MASMethodNotImplemented();
}
另外,在MASConstraint类中,还定义了一些属性链方法,这些方法最终都会调到abstract分类中的抽象方法addConstraintWithLayoutAttribute;
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
约束的子类MASCompositeConstraint和MASViewConstraint
接着Masonry设计了MASContraint的两个子类,MASCompositeConstraint和 MASViewConstraint,用来表示具体的约束, MASViewConstraint表示一个view属性和另一个views属性的单个约束,MASCompositeConstraint表示一个view属性与另一个view属性的组合约束(也就是单个约束的组合)。
在两个子类中,实现了MASConstraint定义的所有抽象方法。
前面我们知道,在调用MASContraint的属性链方法left、right、top等来添加约束时,会调用抽象方法addConstraintWithLayoutAttribute:, 由于抽象方法在子类中被覆盖,所以 如果是MASCompositeConstraint类型的子类调用属性链方法,则会调到子类MASCompositeConstraint的addConstraintWithLayoutAttribute方法; 如果是MASViewConstraint类型的子类调用属性链方法,则会调到子类MASViewConstraint的addConstraintWithLayoutAttribute方法;
同理,在调用MASContraint的其他抽象方法,最终会调到子类的对应抽象方法。
约束的代理MASConstraintMaker
在MASCompositeConstraint和 MASViewConstraint的addConstraintWithLayoutAttribute:中,对象本身不做任何的操作,而是抛给它的代理MASConstraintMaker对象执行代理方法constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute添加约束;
MASConstraintMaker作为MASCompositeConstraint和MASViewConstraint的代理,主要负责为某个view记录他的所有属性约束,存储在constraint数组中,等view的所有属性约束添加完成后,才将这些约束一一添加到到应该添加的view上
[progessSlider mas_makeConstraints:^(MASConstraintMaker *make) {
//约束1,processSlider的centerY、left属性与processControl的centerY、left属性约束
make.centerY.left.equalTo(processControl);
//约束2,processSlider的rightt属性与playeButton的centerY、left属性约束
make.right.equalTo(playeButton.mas_left).offset(-10);
//约束3,processSlider的height属性约束;
make.height.mas_equalTo(5);
}];
类与类之间的关系
一些细节
自动装箱语法
因为MASConstraint的equalTo方法只能传对象类型,传其他类型会出现问题,因此这里使用了自动装箱,只要定义了MAS_SHORTHAND_GLOBALS 这个宏,传递给equalTo方法的非对象类型参数会自动封装成NSValue或NSNumber类型。
/**
* Convenience auto-boxing macros for MASConstraint methods.
*
* Defining MAS_SHORTHAND_GLOBALS will turn on auto-boxing for default syntax.
* A potential drawback of this is that the unprefixed macros will appear in global scope.
*/
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))
#ifdef MAS_SHORTHAND_GLOBALS
#define equalTo(...) mas_equalTo(__VA_ARGS__)
#define greaterThanOrEqualTo(...) mas_greaterThanOrEqualTo(__VA_ARGS__)
#define lessThanOrEqualTo(...) mas_lessThanOrEqualTo(__VA_ARGS__)
#define offset(...) mas_offset(__VA_ARGS__)
#endif
/**
* Given a scalar or struct value, wraps it in NSValue
* Based on EXPObjectify: https://github.com/specta/expecta
*/
static inline id _MASBoxValue(const char *type, ...) {
va_list v;
va_start(v, type);
id obj = nil;
if (strcmp(type, @encode(id)) == 0) {
id actual = va_arg(v, id);
obj = actual;
} else if (strcmp(type, @encode(CGPoint)) == 0) {
CGPoint actual = (CGPoint)va_arg(v, CGPoint);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(CGSize)) == 0) {
CGSize actual = (CGSize)va_arg(v, CGSize);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(double)) == 0) {
double actual = (double)va_arg(v, double);
obj = [NSNumber numberWithDouble:actual];
} else if (strcmp(type, @encode(float)) == 0) {
float actual = (float)va_arg(v, double);
obj = [NSNumber numberWithFloat:actual];
} else if (strcmp(type, @encode(int)) == 0) {
int actual = (int)va_arg(v, int);
obj = [NSNumber numberWithInt:actual];
} else if (strcmp(type, @encode(long)) == 0) {
long actual = (long)va_arg(v, long);
obj = [NSNumber numberWithLong:actual];
} else if (strcmp(type, @encode(long long)) == 0) {
long long actual = (long long)va_arg(v, long long);
obj = [NSNumber numberWithLongLong:actual];
} else if (strcmp(type, @encode(short)) == 0) {
short actual = (short)va_arg(v, int);
obj = [NSNumber numberWithShort:actual];
} else if (strcmp(type, @encode(char)) == 0) {
char actual = (char)va_arg(v, int);
obj = [NSNumber numberWithChar:actual];
} else if (strcmp(type, @encode(bool)) == 0) {
bool actual = (bool)va_arg(v, int);
obj = [NSNumber numberWithBool:actual];
} else if (strcmp(type, @encode(unsigned char)) == 0) {
unsigned char actual = (unsigned char)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedChar:actual];
} else if (strcmp(type, @encode(unsigned int)) == 0) {
unsigned int actual = (unsigned int)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedInt:actual];
} else if (strcmp(type, @encode(unsigned long)) == 0) {
unsigned long actual = (unsigned long)va_arg(v, unsigned long);
obj = [NSNumber numberWithUnsignedLong:actual];
} else if (strcmp(type, @encode(unsigned long long)) == 0) {
unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
obj = [NSNumber numberWithUnsignedLongLong:actual];
} else if (strcmp(type, @encode(unsigned short)) == 0) {
unsigned short actual = (unsigned short)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedShort:actual];
}
va_end(v);
return obj;
}
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))
反思借鉴之处
适用于给对象添加一系列属性。