本文主要是介绍Masonry 框架小结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Masonry 框架小结
自从 iOS 6.0 苹果引入自动布局以来,经过不断的发展,其已经成为我们进行 UI
布局的主要技术之一。在 Autolayout
之前,可以使用 Autoresizing
来约束视图的父子关系,但是局限性很大,很多任务无法实现,如其不能约束兄弟视图之间的关系。
相比之下,Autolayout 的功能则强大的多,其核心概念便是参照和约束。
NSLayoutConstraint
自动布局的关键是创造完整的约束,并将约束添加到正确的视图中。
NSLayoutConstraint
类是对约束的描述,其核心概念可以用一个公式表示,如下:
firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant
对于视图自身大小尺寸的约束而言,是不需要 secondItem
参照物的。不过,如果将此情况视为以自身为参照,那么就可以将约束理解为总是参照其他视图而存在的,即约束是相对而言的。
该类提供了两个类方法用来创建约束实例:
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;+ (NSArray<__kindof NSLayoutConstraint *> *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(nullable NSDictionary<NSString *,id> *)metrics views:(NSDictionary<NSString *, id> *)views;
第一个方法,基本上就是对核心公式的赋值。而第二个方法,则是使用 VFL(Visual Format Language) 字符串来创建约束。
要准确快速的使用约束,需要了解 NSLayoutAttribute
、NSLayoutRelation
和 UILayoutPriority
的可选值。
对于 NSLayoutAttribute
而言,需要注意的是 NSLayoutAttributeLeft
和 NSLayoutAttributeRight
总是表示视图的左、右两侧,但是 NSLayoutAttributeLeading
和 NSLayoutAttributeTrailing
却不总是表示视图的左、右两侧,其表示的含义同当地视图排版习惯相关。所以,其表示的含义可能是左、右两侧,也可能是右、左两侧。
而 NSLayoutAttribute***Margin
表示的参照位置总是同 UIView
的 layoutMargins
属性相关。
创建的约束要添加到视图中才会生效,但是需要将 UIView
的 translatesAutoresizingMaskIntoConstraints
属性置为 NO
,并且约束相关联的视图必须都添加到视图层级中,而且约束需要添加到相关视图的共同父视图中(将视图本身看作其本身的父视图,即对于有包含关系的相关视图,约束要添加到层级高的视图中,而只涉及自身的约束,则添加到其自身即可)。
约束的使用的确减少了视图的布局和适配工作,但是直接使用 NSLayoutConstraint 类定义约束是十分繁琐的,并且代码会变得十分臃肿,不易理解、查错和维护,所以第三方框架的使用必不可少。
Masonry
Masonry
框架十分易用,支持链式编程,其实现并不复杂,但是设计的十分巧妙。理解 Masonry
中约束的实现过程,可以从约束的核心公式入手。
MASViewAttribute
MASViewAttribute
类,可以理解为对 NSLayoutAttribute
的封装,其除了保存布局时要参照的位置外,还持有了具体的布局对象和参照对象。
@property (nonatomic, assign, readonly) NSLayoutAttribute layoutAttribute;@property (nonatomic, weak, readonly) MAS_VIEW *view;
@property (nonatomic, weak, readonly) id item;
可见,布局对象是 UIView 对象,而参照对象则未必。并且,两者都是弱引用。
MASLayoutConstraint
MASLayoutConstraint
只是简单继承了 NSLayoutConstraint
类,可以预见,其是该框架中描述约束的类。另外,其定义了一个属性以便于调试,通过该属性,可以得知一个约束是否是通过该框架创建的。
@property (nonatomic, strong) id mas_key;
MASConstraint
MASConstraint
虽然是以 Constraint
结尾,但是其是 NSObject
的子类。
该类的巧妙设计使得 Masonry
框架中的方法可以链式调用,对于有参数的方法,无法使用点语法进行调用,所以在该类中通过返回包含参数的 block 来进行参数的传递。
可以将该类中的方法分为三类:
-
返回 block 的方法
如下两个方法,分别表示对偏移量和约束属性的修改:
- (MASConstraint * (^)(CGFloat))offset {return ^id(CGFloat offset){self.offset = offset;return self;}; }- (MASConstraint * (^)(id))equalTo {return ^id(id attribute) {return self.equalToWithRelation(attribute, NSLayoutRelationEqual);}; }
如同第一个方法,在返回的 block 中会使用调用时随后的指定参数对变量进行赋值,而对于类似第二个方法的对参照关系属性的修改,同样是在 block 中实现的。而其所使用的方法,在本类中实际是抽象方法,需要子类去具体实现。
-
返回本类实例的方法
诸如
left
、right
、top
、bottom
等方法,实际是返回了一个MASConstraint
实例对象,从而可以进一步调用上面所说的第一种类型的方法。- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {MASMethodNotImplemented(); }- (MASConstraint *)left {return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]; }
这些方法,实际都是对
addConstraintWithLayoutAttribute:
方法的调用,而其也是由子类实现的。 -
抽象方法
该类中定义了诸多抽象方法,由子类来实现。
从上面的分析可知,该类的主要功能就是实现链式编程,而具体的约束创建则是由子类实现的。主要关注 addConstraintWithLayoutAttribute:
和 equalToWithRelation
方法。
MASViewConstraint
MASViewConstraint
作为 MASConstraint
的子类,除了继承父类的相关属性外,还定义了 firstViewAttribute
和 secondViewAttribute
两个属性,其实际就是约束相关的两个参照。
除了一个初始化方法外,还提供了一个类方法,用来获取所有通过该框架添加到一个视图中的约束。其是通过运行时函数,为 UIView
对象添加了一个属性 mas_installedConstraints
来保存所有通过该框架添加到该视图中的约束。
+ (NSArray *)installedConstraintsForView:(MAS_VIEW *)view {return [view.mas_installedConstraints allObjects];
}
虽然 MASViewConstraint
并不直接描述约束,但是其实际上包含了所有创建约束的条件,并且也仅仅表示一个约束。该类中定义了一个 NSLayoutConstraint
实例来保存该类的实例对象生成的具体约束,并且这个约束生效后,该实例对象会被添加到 mas_installedConstraints
属性中。不过,这个过程并不是自动的,而是需要调用 install
方法。
在 iOS 8.0 后,约束支持 active
属性,可以通过设置该属性的值来取代向视图中添加约束的操作。
if ([self supportsActiveProperty] && self.layoutConstraint) {self.layoutConstraint.active = YES;[self.firstViewAttribute.view.mas_installedConstraints addObject:self];return;
}
但是,self.layoutConstraint
第一次创建,或者不支持 active
时,仍然需要执行下面的操作。
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {secondLayoutItem = self.firstViewAttribute.view.superview;secondLayoutAttribute = firstLayoutAttribute;
}
增加一个判断,当不是对视图自身尺寸的约束,而又没有参照时,则默认参照父视图。
接着,使用相关属性创建约束 MASLayoutConstraint
实例。
然后,获取应该添加该约束的视图,如果要求更新与之相同的约束,那么先遍历所有约束,找到与之相同的约束,直接更新即可,否则,将该约束添加到视图中,所以每个约束生效之前不会主动进行查重,即可能存在重复的约束。
该类中重要的是重写的两个方法 addConstraintWithLayoutAttribute:
和 equalToWithRelation
。
- (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];
}
- (MASConstraint * (^)(id))equalTo {return ^id(id attribute) {return self.equalToWithRelation(attribute, NSLayoutRelationEqual);};
} - (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;}};
}
方法 equalToWithRelation
是没有参数的,但是 equalTo
方法返回的 block 中调用该方法时,似乎是传递了两个参数,这实际是同样的道理,参数实际是传递给 equalToWithRelation
函数返回的 block 的。
如果参数 attribute
是数组,即同时要添加多个约束,那么便创建一个 MASCompositeConstraint
实例来保存这些约束的相关属性。并且,这里将代理同样赋值给该实例。
从重写的 addConstraintWithLayoutAttribute:
可知,该代理实现了新增约束的操作,十分重要,其究竟是哪个类实现的呢!MASCompositeConstraint
作为 MASViewConstraint
的集合和代理,是否实现了新增约束的操作呢。
MASCompositeConstraint
MASCompositeConstraint
是 MASConstraint
的子类,由一组 MASViewConstraint
初始化得到。其对父类抽象方法的实现,实际就是对其所持有的每个 MASViewConstraint
实例执行相应方法。即,该类可以对一组约束执行相同的操作。
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {[self constraint:self addConstraintWithLayoutAttribute:layoutAttribute];return self;
}- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {id<MASConstraintDelegate> strongDelegate = self.delegate;MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];newConstraint.delegate = self;[self.childConstraints addObject:newConstraint];return newConstraint;
}
其实现的代理函数,依然只是将消息传递给了原本的代理对象。那么,这个真正的代理是谁,又执行了什么操作呢。
MASConstraintMaker
从 MASConstraintMaker
的属性来看,似乎同 MASConstraint
类很是相同,但它是 NSObject
的子类,和 MASConstraint
并没有继承关系。
类似的该类中有如下方法:
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}- (MASConstraint *)left {return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
同 MASViewConstraint
中不同的是,这里的 addConstraintWithLayoutAttribute:
方法直接调用了自身的 constraint:addConstraintWithLayoutAttribute:
方法,而不是代理的相应方法。
这方法是 MASConstraintDelegate 代理声明的,且当前类实现如下:
- (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 constraintNSArray *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;
}- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {NSUInteger index = [self.constraints indexOfObject:constraint];NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);[self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
可知,其根据自身关联的 view 和参数提供的位置属性创建一个 MASViewConstraint
实例对象,并且如果该方法的执行是由存在的 MASViewConstraint
实例引发的(即 constraint
参数存在),那么将两者合为一个 MASCompositeConstraint
实例对象,并替代引发该操作的实例。
最后,将得到的实例保存到 self.constraints
中。
该类同样定义了 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;
}
但是,这里会先根据 self.removeExisting
属性来判断是否要将关联的 view 中约束清空。
而且属性 self.updateExisting
同样是判断是否更新已存在约束的依据,这里更新的只是 NSLayoutConstraint
的 constant
属性,实际如果其他属性不同,则不会被认为是同一个约束。
并且,该方法会清空 self.constraints
中的成员,表示所有约束创建完毕。
那么,我们现在可以直接创建 MASConstraint
子类对象,并调用 install 方法来创建约束,也可以使用 MASConstraintMaker
来创建约束,那么那种方式更好呢。
实际上,如果直接使用 MASConstraint
来创建约束是无法使用链式编程的,因为其返回的 block 执行时,实际是向代理传递了消息,而此时代理并没有设置。
View+MASAdditions.h
Masonry
框架为 UIView
创建了分类,方便约束的创建。理解上面的约束创建过程,再来看这几个方法,则一目了然了。
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {self.translatesAutoresizingMaskIntoConstraints = NO;MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];block(constraintMaker);return [constraintMaker install];
}- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {self.translatesAutoresizingMaskIntoConstraints = NO;MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];constraintMaker.updateExisting = YES;block(constraintMaker);return [constraintMaker install];
}- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {self.translatesAutoresizingMaskIntoConstraints = NO;MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];constraintMaker.removeExisting = YES;block(constraintMaker);return [constraintMaker install];
}
添加、更新、重置约束,都是根据 MASConstraintMaker
中的属性来控制的。
[self.button mas_makeConstraints:^(MASConstraintMaker *make) {make.left.top.equalTo(@20);
}];
在 block 中,make.left
返回的是 MASConstraint
实例,之后调用 top 方法,实际是调用了 delegate 的 constraint:addConstraintWithLayoutAttribute:
方法,而该代理实际就是 make 对象,然后返回了一个 MASCompositeConstraint
实例对象,调用 equalTo
方法,进而调用了如下方法:
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {return ^id(id attr, NSLayoutRelation relation) {for (MASConstraint *constraint in self.childConstraints.copy) {constraint.equalToWithRelation(attr, relation);}return self;};
}
这样,便实现了对多个约束设置相同偏移量的操作。
这篇关于Masonry 框架小结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!