Masonry 解析

来源:互联网 发布:nginx 打印php错误 编辑:程序博客网 时间:2024/05/20 03:39

Masonry 解析

在了解一个开源库之前,我们需要先搞清楚一件事情:

这个开源库解决了什么问题?

Masonry 解决了什么问题?


熟悉的人都知道:

Masonry 将 NSLayoutConstraint 进行了封装,使用了优雅高效易读的链式语法,让 Objective-C 开发者在手写 Autolayout 的时候不再那么麻烦。

因此,在看本文之前,你需要知道,什么是 Autolayout?

如果你已经知道什么是 Autolayout 了,那你至少还应该知道,如何使用代码来对视图添加约束?

现在应该知道,使用代码来写 Autolayout,苹果官方的 API 使用繁琐代码量大不易阅读。Masonry 非常完美的解决了这个问题。

Masonry 的 Github 地址:https://github.com/Masonry/Masonry

如果英语能力还可以的话,看它的 Github 里的 Readme 就能看明白很多东西。

这里有一个如何使用 Masonry 的教程写的不错:http://adad184.com/2014/09/28/use-masonry-to-quick-solve-autolayout/

Masonry 如何解决这些问题的?


我们先看看 NSLayoutConstraint 的创建方法:

// UIKit// NSLayoutConstraint.h/* Create constraints explicitly.  Constraints are of the form "view1.attr1 = view2.attr2 * multiplier + constant"  If your equation does not have a second view and attribute, use nil and NSLayoutAttributeNotAnAttribute. */+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c; 

看注释里面的,这段代码最终的结果是:

view1.attr1 = view2.attr2 * multiplier + constant

再来看它的使用:

// NSLayoutConstraint[superView addConstraint:[NSLayoutConstraint constraintWithItem:view1                                                      attribute:NSLayoutAttributeTop                                                      relatedBy:NSLayoutRelationEqual                                                         toItem:superView                                                      attribute:NSLayoutAttributeTop                                                     multiplier:1.0                                                       constant:10]];// 根据上面的定义,这段代码的结果是:// view1.top = superView.top * 1.0 + 10;// 再看看 Masonry  的做法    [view1 mas_makeConstraints:^(MASConstraintMaker *make) {    make.top.equalTo(superview.mas_top).with.offset(10); }];                          // 简直完美!   

Masonry 非常完美的解决了系统提供的复杂使用方式,让写出来的代码简洁,又易懂。
可以思考一下,如果我们要做这样一个封装,该怎么做?

思路:

我们首先需要一个类,就像 NSLayoutConstraint 一样,可以管理所有的东西,然后用简洁的输入方式输入所需要的内容,然后再调用一个方法,把需要的参数传递给 NSLayoutConstraint,并且生成,然后添加到 UIView 上。

最好是可以做到这样:

// 这将是我们的最终目标,请牢记这个,后面我们所有的想法,都是为了这个目标而努力。

view1.top = superView.top * 1.0 + 10;

所以我们需要的属性有上面的例子中所需要的所有东西:view1 , top , = , superView , top , 1.0 , 10. 根据 NSLayoutConstraint 的创建方法:

View1 和 superView 是 UIView 类型,

top 是 NSLayoutAttribute,

= 是 NSLayoutRelation,

1.0 是乘积, CGFloat 类型,

10 是偏移量 CGFloat 类型,

然后我们可以调用一个方法来完成这个约束的安装。

可是问题来了:

这跟系统提供的方法有什么区别呢?

Masonry 是如何实现链式语法的?

Masonry 类结构


Masonry 主要的类结构有:

  1. MASConstraint:继承自 NSObject,是我们上一部分设想的那个类,负责收集 NSLayoutConstraint 的创建方法需要的参数,并给对应的 View 添加 Constraint,不过这个类只是基础的抽象类,只负责收集信息并且提供一些基础方法和属性。

  2. MASViewConstraint:继承自 MASConstraint,使用 MASViewAttribute 保存 view1 和 superView 与其 NSLayoutAttribute,然后通过一个方法来给 view1 添加约束。

  3. MASViewAttribute:继承自 NSObject,负责将 view 和 NSLayoutAttribute 绑定在一起,目的是简化代码,不用在 MASViewConstraint 里面自己维护 view 和 NSLayoutAttribute 的关系。

  4. MASCompositeConstraint:继承自 MASConstraint,批量存储 MASConstraint/MASViewConstraint,并提供批量添加约束的方法。

  5. MASLayoutConstraint:继承自 NSLayoutConstraint,使用系统方法用来做最后的 constraint 生成,多了一个 key 的属性,来做 Debug。

  6. MASConstraintMaker:继承自 NSObject,最重要的一个类,每个 MASConstraintMaker 维护一个 View,用来处理这个 View 相关的约束,这个 view 每一个相关的约束都是一个 MASViewConstraint,有一个数组 Constraints 来维护这些数据。最后通过一个方法,给 view 批量添加约束。

  7. View+MASAdditions:因为我们的目标是做到可以直接使用 view.top… 所以我们必须给 UIView 加一个分类来处理这些,其实相关的内容都只是操作 MASConstraintMaker。

MASConstraint : NSObject


这个类是我们设想的最基础的类,按道理它应该负责保存 NSLayoutConstraint 生成方法所需要的所有参数。
但是 Masonry 没有这么做,Masonry 只把对 view 的 layoutAttribute 使用属性保存了起来:

  • NSLayoutConstraint 中的两个枚举类型:NSLayoutAttribute、 NSLayoutRelation

  • 乘积 multiplier (CGFloat)

  • 偏移量 constant (CGFloat)

  • 优先级 UILayoutPriority (float)

  • 并且生成了一些其他的衍生属性,比如 offset, insets, sizeOffset, centerOffset, dividedBy 等。

这个类是一个非常标准的抽象类,将基础的数据全部处理掉,然后在 MASConstraint+Private.h 中使用分类 MASConstraint (Abstract) 和一个匿名分类来声明几个基类和子类都将使用到的方法,并且在这里声明代理和代理方法。这一段很有意思,对于开发者来说应该能提供很多灵感。使用分类和协议,能帮你减少很多代码。

这里是链式语法的比较关键的一步,就是 view.top.equalTo(superView.top) 的 eauqlTo(),我们先看看它是如何定义的:

// MASConstraint.h/** *  Sets the constraint relation to NSLayoutRelationEqual *  returns a block which accepts one of the following: *    MASViewAttribute, UIView, NSValue, NSArray *  see readme for more details. */- (MASConstraint * (^)(id attr))equalTo;

看了就比较明白了,其实它就是将 block 作为返回值,而这个 block 的返回值就是 MASConstraint 自己,这样可以继续使用点语法操作其他内容,看看实现:

- (MASConstraint * (^)(id))equalTo {    return ^id(id attribute) {        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);    };}

在这里,- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation 这个方法只是一个抽象的声明而已,因为在 MASConstraint 里面没有涉及到任何的 UIView,因此这些只是概念,具体的实现还是要到 MASViewConstraint 里面。

同理:

- (MASConstraint * (^)(CGFloat offset))offset;- (MASConstraint * (^)(CGFloat multiplier))multipliedBy;

等等,也是如此的做法。

MASViewConstraint : MASConstraint


当基础类搞定,只能在抽象的意义上设定 view 的 top、left、right、bottom,并没有涉及到具体的 view。在这里就将来完成这一步。对于 NSLayoutConstraint 的生成方法,view 是有顺序的,而且在 NSLayoutConstraint 的属性中,也有 firstItem 、firstAttribute、secondItem、secondAttribute。因此我们也需要这样的东西。不过 Masonry 并没有这么做,它新创建了一个类,叫 MASViewAttribute ,负责将 view 和 attribute 绑定在一起。于是就有了如下属性:

  • 第一个视图和它的约束 firstViewAttribute ( MASViewAttribute)

  • 第二个视图和它的约束 secondViewAttribute ( MASViewAttribute)

然后再配一个如下的初始化方法:

// MASViewConstraint.h/***   initialises the MASViewConstraint with the first part of the equation**   @param  firstViewAttribute  view.mas_left, view.mas_width etc.**   @return a new view constraint**/- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute;

还记得我们当初的设想么?

 view1.top = superView.top * 1.0 + 10;

看看目前为止的代码执行是不是能满足这个要求了:

  1. 使用初始化方法,将 view1.top 整合成 MASViewAttribute 类型的实例,并赋值给 firstViewAttribute 属性

  2. 代码执行到 view1.top = 的时候,调用 equal 方法

  3. 代码执行到 view1.top = superView.top 的时候,将 superView.top 整合成 MASViewAttribute 类型的实例,并赋值给 secondViewAttribute 属性

  4. 代码执行到 view1.top = superView.top*1.0 的时候,将 1.0 保存到 multiplier 属性

  5. 代码执行到 view1.top = superView.top*1.0 + 10 的时候,将 10 保存到 constrant 属性

完全满足了!然后我们加一个 install (安装) 方法,来调用 NSLayoutConstraint 的创建方法就可以了。

这里最重要的实现,就是将 MASConstraint 里面定义的一些抽象方法具体实现,因为在这里已经有 view、superView 和对应的 NSLayoutConstraint。现在我们可以看看 MASConstraint 中 - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation 是怎么实现的了:

- (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.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;        }    };}

if 里面的内容是批量添加的代码,目前来说不是重点。重点看 else 里面的内容,其实很简单,只是记录一下。 self.layoutRelation 就是 NSLayoutRelation 类型的,在 view1.top = superView.top * 1.0 + 10 里面负责的是 = 的部分,当然也可以是 >= 或者 <=

同理:

- (MASConstraint * (^)(CGFloat offset))offset;- (MASConstraint * (^)(CGFloat multiplier))multipliedBy;

等等,也是如此的做法。

offset() 是控制偏移量的,对应的内容是 +10,它最终的设置的是 self.layoutConstant。

multipliedBy() 是控制倍数的,对应的内容是 1.0,它最终的设置的是 self.layoutMultiplier。

当然还有很多衍生用法,基本都是来控制这 self.layoutConstantself.layoutMultiplier 这两个属性的。

MASViewAttribute : NSObject


这个类是用来绑定 view 和 NSLayoutAttribute 的,对于我们的设想,它达到的效果是 view1.top / superView.top, 它的属性有:

  • 视图 view (UIView)

  • 约束位置 layoutAttribute (NSLayoutAttribute)

有了以上这些,我们就可以设置一个视图的某一条约束了:

MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:view1 layoutAttribute:NSLayoutAttributeTop];MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];// 先忽略 superView.top 的问题,后面会讲newConstraint.equalTo(superView.top).offset(10). multipliedBy(1.0);

这样已经方便很多了,但是依然需要创建 MASViewAttributeMASViewConstraint 这两个东西,如果我们有一个类来维护这些东西,那就更方便了,于是就有了 MASConstraintMaker。

MASConstraintMaker : NSObject


按照我们的想法,上面的几个类已经可以完成我们想要的了,但是问题是,这样依然不能做到简化语法的目的。

Masonry 最关键的地方在于这个类,它保存一个主视图 view1,然后针对这个主视图,使用一个 NSArray 来统一管理 MASViewConstraint。

这样,我们初始化一个 MASConstraintMaker 类,并赋值一个 view,然后添加其他约束就可以了。因此需要将 view 的基础约束添加成属性来调用:

// MASConstraintMaker.h@property (nonatomic, strong, readonly) MASConstraint *left;@property (nonatomic, strong, readonly) MASConstraint *top;@property (nonatomic, strong, readonly) MASConstraint *right;@property (nonatomic, strong, readonly) MASConstraint *bottom;@property (nonatomic, strong, readonly) MASConstraint *leading;@property (nonatomic, strong, readonly) MASConstraint *trailing;@property (nonatomic, strong, readonly) MASConstraint *width;@property (nonatomic, strong, readonly) MASConstraint *height;@property (nonatomic, strong, readonly) MASConstraint *centerX;@property (nonatomic, strong, readonly) MASConstraint *centerY;@property (nonatomic, strong, readonly) MASConstraint *baseline;// 这里的 MAS_VIEW 其实就是 UIView,Masonry 自己定义的宏- (id)initWithView:(MAS_VIEW *)view;

这里我们使用 left 来举例,readonly 是因为这个约束不希望被外部修改,因为 Masonry 特有的链式语法,使用点语法 view.left 其实是为了利用 left 的 get 方法,然后创建一个 对应的 MASViewConstraint 并且存到数组中:

 // MASConstraintMaker.m- (MASConstraint *)left {    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];}- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {     return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];}- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {    // 创建 MASViewAttribute ,将 NSLayoutAttribute 和 view 绑定起来,    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;}

这个想法是 Masonry 第二精妙之处。我们可以理解为,将 MASConstraintMaker 替换成 view。

MASConstraintMaker *make = [[MASConstraintMaker alloc] initWithView:view1];make.left.equalTo(superView.mas_top).offset(10).multipliedBy(1.0);

以此类推,将 left、right、top、bottom 都添加好以后,调用一个方法,就可以将一个 View 的所有约束都安装好了。

现在看来,已经非常接近我们的目标 view1.top = superView.top * 1.0 + 10; 了,不过还是要创建一个 MASConstraintMaker,我们也需要想办法简化。

View+MASAdditions : UIView (MASAdditions)


上面的内容中,有一个关键的点,superView.top 是从哪里来的?UIView 并没有这样的属性。于是我们需要创建一个 UIView 的分类,给它添加几个属性:

//  View+MASAdditions.h// 为了不与其他库冲突,添加前缀是一个三方库最基本的操守@property (nonatomic, strong, readonly) MASViewAttribute *mas_left;@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;@property (nonatomic, strong, readonly) MASViewAttribute *mas_right;@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom;@property (nonatomic, strong, readonly) MASViewAttribute *mas_leading;@property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing;@property (nonatomic, strong, readonly) MASViewAttribute *mas_width;@property (nonatomic, strong, readonly) MASViewAttribute *mas_height;@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX;@property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY;@property (nonatomic, strong, readonly) MASViewAttribute *mas_baseline;@property (nonatomic, strong, readonly) MASViewAttribute *(^mas_attribute)(NSLayoutAttribute attr);

这样,我们就可以调用 view1.top 或者 superView.top 了。

当然这个类的作用不止于此,程序员都是追求极致简单的。

我们连 MASConstraintMaker 都不希望在使用的时候自己创建,于是需要给 UIView 添加这么一个方法:

//  View+MASAdditions.h/** *  Creates a MASConstraintMaker with the callee view. *  Any constraints defined are added to the view or the appropriate superview once the block has finished executing * *  @param block scope within which you can build up the constraints which you wish to apply to the view. * *  @return Array of created MASConstraints */ - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;

这就是我们经常使用的那个方法,为了在给一个 view 添加约束的时候看起来整体性更加强,Masonry 将所有的添加约束方法放在了 block 里面进行,于是就有了如下使用:

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {    make.top.equalTo(superview.mas_top).with.offset(10);     make.left.equalTo(superview.mas_left).with.offset(10);     make.bottom.equalTo(superview.mas_bottom).with.offset(-10);     make.right.equalTo(superview.mas_right).with.offset(-10); }]; 

这样,其实很容易就能想到这个 mas_makeConstraints:^(MASConstraintMaker *make) 方法是如何实现的:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {    self.translatesAutoresizingMaskIntoConstraints = NO;    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];    block(constraintMaker);    return [constraintMaker install];}

当然了,系统比较傻,view 的添加约束方法只有添加

// UIView.h// UIView (UIConstraintBasedLayoutInstallingConstraints)- (void)addConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);

但是问题是,约束重复叠加是会出问题的。因此 Masonry 提供了两个方法:

//  View+MASAdditions.h// 更新现有的约束- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;// 移除旧的约束,并重新添加新的约束- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

它们的具体实现也不复杂,其实也就是给 MASConstraintmaker 添加两个布尔值,然后在 [constraintMaker install] 的和 [constraint install] 的时候根据布尔值来做对应的操作而已。

MASLayoutConstraint : NSLayoutConstraint


这个类其实很简单,就是添加了一个属性来做 Debug。其他没有了。

@property (nonatomic, strong) id mas_key;

至此,我们回头看看,我们已经完成目标了,其实 Masonry 的最关键的几个类也就在这里了,下面还有一些衍生的。

NSArray+MASAdditions


批量给 view 添加约束,数组里面必须是 UIView 类型的,然后统一给所有 view 添加一致的约束。

NSArray+MASShorthandAdditions


龟毛的作者,非要简化一下批量添加约束的方法名….

NSLayoutConstraint+MASDebugAdditions


用来做 Debug 用的,这个还是比较有用的,因为官方的 NSLayoutConstraint Debug 跟使用一样恶心。

View+MASShorthandAdditions


恩,依然是作者的龟毛,简化一下 View+MASViewAddition 的命名。

ViewController+MASAdditions


其实不只是 UIView, UIViewController 也是可以添加约束的。

总结


总的来看,Masonry 有很多可以学习的地方:

  1. 链式语法:脑洞大开的使用方式,让代码使用简单并且易读。这需要非常深厚的编码功底才能想的出来。
  2. 巧用分类:使用分类,配合继承和代理,可以简化很多代码,如果说链式语法在实战中使用还是比较麻烦,那么熟练使用分类、继承、协议,将会让你的代码更上一层楼。
  3. 思维能力:到现在为止我依然对 Masonry 的作者表示敬佩,非常棒的思维能力,解决了一个大问题。灵活的运用技能会让你在工作的时候事半功倍。

VFL(Visual Format Language) 是官方出的简化版,不过真的很难用,而且代码写起来一点都不优雅。

最后,这里还有一篇解读 Masonry 的文章,写的很好,提供了一个类导图,可以让你更加清除整个 Masonry 的结构。

0 0