美文网首页
Masonry思路解析

Masonry思路解析

作者: Mage | 来源:发表于2017-11-28 21:05 被阅读54次

    当前iOS最流行的布局方式就是使用autoLayout,本文就时下最流行的Masonry进行分析,解剖其实现思路。
    我们都知道UIKit自带的布局代码超级复杂,代码繁多不够简洁。

        UIView *view = [[UIView alloc]init];
        [self.view addSubview:view];
        view.translatesAutoresizingMaskIntoConstraints = NO;
        [[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
        [[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
        [[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
        [[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
    

    怎样才能实现简单的autoLayout实现方式呢?
    Masonry引入的链式响应方式。如

    // 通过一行或很少的代码即可完成autolayout,Masonry其实就是UIKit中NSLayoutConstraint的封装
    view.top.left.right.equalTo(self.view.mas_top).offset(10)
    

    咱们一步步实现自己的Masonry。

    NSLayoutConstraint简单解析

    由于是封装现有的UIKit的NSLayoutConstraint,所有我们要分析下UIKit自带的布局约束方法有什么特征。

    
    /**
     约束方法
    
     @param view1 约束左边的view1
     @param attr1 view1需要约束的属性
     @param relation 关系描述
     @param view2 约束有边的view2
     @param attr2 view2的布局属性
     @param multiplier 比例关系
     @param c 偏移量
     @return 返回约束模型
     */
    +(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 + c

    一、MAAutoLayout(实现"view1.attr1")

    我们需要给UIView添加一个布局管理属性 ma_layout(MAAutoLayout类型),方便之后布局管理。
    MAAutoLayout管理自己的属性,需要知道view1和attr1,所以MAAutoLayout可以设计为

    @interface MAAutoLayout : NSObject
    
    - (nonnull instancetype)initWithView:(UIView * _Nonnull)view;
    
    // 基本操作,依据NSLayoutAttribute创建所有的方法,用户链式响应的第一步
    @property (nonatomic, strong, readonly) MAAutoLayoutMaker * _Nonnull left;
    @property (nonatomic, strong, readonly) MAAutoLayoutMaker * _Nonnull top;
    #####省略 right, bottom等,详细请看GitHub上的demo #####
    // 激活
    - (void)active;
    // 取消
    - (void)deactivate;
    @end
    

    此时我们就可以使用view.ma_layout.top 来替代公式中的view1.top。

    二、MAAutoLayoutMaker(实现"= id * multiplier + c")

    .equalTo(self.view)需要在MAAutoLayoutMaker中创建有关关系(NSLayoutRelation)的属性。

    @interface MAAutoLayoutMaker : NSObject
    
    @property (nullable, nonatomic,strong, readonly) NSLayoutConstraint *layoutConstraint;
    
    - (nonnull instancetype)initWithFirstItem:(nonnull id)firstItem firstAttribute:(NSLayoutAttribute)firstAttribute;
    // 偏移量
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat offset))offset;
    // 关系
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))equalTo;
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))greaterThanOrEqualTo;
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))lessThanOrEqualTo;
    // 赋值
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_equal;
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_greaterThanOrEqual;
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_lessThanOrEqual;
    // 倍数
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat multiplier))multiplier;
    // 权重
    - (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(UILayoutPriority priority))priority;
    
    - (BOOL)isActive;
    
    - (nonnull NSLayoutConstraint *)active;
    - (void)deactivate;
    @end
    

    有了MAAutoLayoutMaker,我们就能轻易的实现view.top.equalTo().offset(10).multiplier(2).但是equalTo中的属性,也就是view2.attr2还没发表示。

    三、MAViewAttribute(实现"view2.attr2")

    view2.attr2需要在MAViewAttribute保存view2的NSLayoutAttribute。

    @interface UIView (MAAutoLayout)
    
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_left;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_top;
    #####省略 right, bottom等,详细请看GitHub上的demo #####
    //iOS11 safeArea
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideTop;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideBottom;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideLeft;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideRight;
    
    @end
    
    @interface UIViewController (MAAutoLayout)
    
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuide;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuide;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuideTop;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuideBottom;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuideTop;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuideBottom;
    
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaTopLayoutGuide;
    @property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaBottomLayoutGuide;
    
    @end
    

    此时调用布局代码的方式为

    view.kf_layout.top.equalTo(self.view.ma_top).offset(10).active;
    view.kf_layout.left.equalTo(self.view.ma_left).offset(10).active;
    view.kf_layout.bottom.equalTo(self.view.ma_bottom).offset(-10).active;
    view.kf_layout.right.equalTo(self.view.ma_right).offset(-10).active;
    

    四、UIView封装Block

    按照上面的方式布局没有任何问题,但总显得不够优雅,可以将布局代码包装在一个block中,代码更集中也更便于管理。

    // 在UIView的Category中添加下面的方法
    - (void)ma_makeConstraints:(void(^_Nonnull)(MAAutoLayout * _Nonnull make))make;
    - (void)ma_remakeConstraints:(void(^_Nonnull)(MAAutoLayout * _Nonnull make))make;
    

    此时的调用方式就变为:

         [view ma_makeConstraints:^(MAAutoLayout * _Nonnull make) {
            make.top.equalTo(self.view.ma_top).offset(10).active;
            make.left.equalTo(self.view.ma_left).offset(10).active;
            make.bottom.equalTo(self.view.ma_bottom).offset(-10).active;
            make.right.equalTo(self.view.ma_right).offset(-10).active;
        }];
    

    五、代码的实现

    步骤四已经基本实现了链式布局的方式,但是每行都要调用一次active方法,总显得不够优雅。我们可以想办法在Block执行完成时,一次性激活所有约束,因此需要在MAAutoLayout中添加一个数组用于保存所有的约束。
    MAAutoLayout.m的实现如下:

    
    @interface MAAutoLayout()
    // 用于保存约束的数组
    @property (nonatomic,strong) NSMutableArray<MAAutoLayoutMaker *> *constraints;
    @property (nonatomic,weak) id view;
    
    @end
    
    @implementation MAAutoLayout
    // 初始化时,保存view1
    - (id)initWithView:(UIView *)view{
        self = [super init];
        if (!self) return nil;
        
        self.view = view;
        // 使用autoLayout需要手动设置translatesAutoresizingMaskIntoConstraints为NO
        view.translatesAutoresizingMaskIntoConstraints = NO;
        self.constraints = [NSMutableArray array];
        return self;
    }
    
    #pragma mark - standard Attributes
    - (MAAutoLayoutMaker *)top {
        return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
    }
    #######   篇幅有限仅显示top的实现,left等相同的类比调用addConstraintWithLayoutAttribute实现#########
    - (MAAutoLayoutMaker *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
        MAAutoLayoutMaker *maker = [[MAAutoLayoutMaker alloc] initWithFirstItem:self.view firstAttribute:layoutAttribute];
        // 将这个约束添加到数组中
        [self.constraints addObject:maker];
        return maker;
    }
    // 激活所有的约束
    - (void)active{
        for (MAAutoLayoutMaker *maker in self.constraints) {
            if (!maker.isActive) {
                [maker active];
            }
        }
    }
    // 撤销所有的约束
    - (void)deactivate{
        [self.constraints makeObjectsPerformSelector:@selector(deactivate)];
        [self.constraints removeAllObjects];
    }
    @end
    

    MAAutoLayoutMaker需要保存所有的相关NSLayoutConstraint的属性,是MAAutoLayout的核心。
    MAAutoLayoutMaker.m的实现如下:

    @interface MAAutoLayoutMaker()
    @property (nullable, nonatomic,weak) id firstItem;
    @property (nonatomic, assign) NSLayoutAttribute firstAttribute;
    @property (nullable, nonatomic,weak) id secondItem;
    @property (nonatomic, assign) NSLayoutAttribute secondAttribute;
    @property (nonatomic, assign) NSLayoutRelation relation;
    @property (nonatomic, assign) CGFloat multiplierValue;
    @property (nonatomic, assign) CGFloat constant;
    @property (nonatomic, assign) UILayoutPriority priorityValue;
    @property (nonatomic,strong) NSLayoutConstraint *layoutConstraint;
    @end
    
    @implementation MAAutoLayoutMaker
    - (instancetype)initWithFirstItem:(id)firstItem firstAttribute:(NSLayoutAttribute)firstAttribute{
        self = [super init];
        if (!self) return nil;
        self.firstItem = firstItem;
        self.firstAttribute = firstAttribute;
        self.secondItem = nil;
        self.secondAttribute = NSLayoutAttributeNotAnAttribute;
        self.multiplierValue = 1.0;
        self.constant = 0;
        self.priorityValue = UILayoutPriorityRequired;
        return self;
    }
    - (MAAutoLayoutMaker *(^)(CGFloat))offset{
        return ^id(CGFloat offset){
            self.constant = offset;
            return self;
        };
    }
    - (MAAutoLayoutMaker * _Nonnull (^)(id _Nonnull))equalTo{
        return ^id(id attribute) {
            return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
        };
    }
    - (MAAutoLayoutMaker * _Nonnull (^)(CGFloat))kf_equal{
        return ^id(CGFloat constant) {
            return self.equalToWithRelation(@(constant), NSLayoutRelationEqual);
        };
    }
    - (MAAutoLayoutMaker * _Nonnull (^)(UILayoutPriority))priority{
        return ^(UILayoutPriority priority) {
            self.priorityValue = priority;
            return self;
        };
    }
    - (MAAutoLayoutMaker * _Nonnull (^)(CGFloat))multiplier{
        return ^(CGFloat multiplier) {
            self.multiplierValue = multiplier;
            return self;
        };
    }
    
    - (BOOL)isActive{
        return self.layoutConstraint != nil;
    }
    // 核心方法,通过上面的便携方法给firstItem,firstAttribute,secondItem, secondAttribute, relation, multiplierValue, constant, priorityValue,在active方法里创建layoutConstraint并激活。
    - (NSLayoutConstraint *)active{
        if (self.layoutConstraint) self.layoutConstraint.active = NO;
        if (self.firstItem) {
            self.layoutConstraint = [NSLayoutConstraint constraintWithItem:self.firstItem attribute:self.firstAttribute relatedBy:self.relation toItem:self.secondItem attribute:self.secondAttribute multiplier:self.multiplierValue constant:self.constant];
            self.layoutConstraint.priority = self.priorityValue;
            self.layoutConstraint.active = YES;
        }
        return self.layoutConstraint;
    }
    
    - (void)deactivate{
        [self.layoutConstraint setActive:NO];
        self.layoutConstraint = nil;
    }
    
    #pragma mark private
    - (MAAutoLayoutMaker * (^)(id, NSLayoutRelation))equalToWithRelation {
        return ^id(id attribute, NSLayoutRelation relation) {
            self.relation = relation;
            if ([attribute isKindOfClass:[UIView class]]) {
                self.secondItem = attribute;
                self.secondAttribute = self.firstAttribute;
            }else if ([attribute isKindOfClass:[MAViewAttribute class]]){
                self.secondItem = ((MAViewAttribute *)attribute).item;
                self.secondAttribute = ((MAViewAttribute *)attribute).layoutAttribute;
            }else if ([attribute isKindOfClass:[NSNumber class]]){
                self.secondItem = nil;
                self.secondAttribute = NSLayoutAttributeNotAnAttribute;
                self.constant = ((NSNumber *)attribute).floatValue;
            }else{
                NSAssert(attribute, @"格式不正确,必须是UIView或MAAutoLayoutMaker或NSNumber");
            }
            self.relation = relation;
            return self;
        };
    }
    @end
    

    UIView+MAAutoLayout.m的实现如下:

    @implementation UIView (MAAutoLayout)
    static char kInstalledMAAutoLayoutKey;
    - (MAAutoLayout *)ma_layout {
        MAAutoLayout *autolayout = objc_getAssociatedObject(self, &kInstalledMAAutoLayoutKey);
        if (!autolayout) {
            autolayout = [[MAAutoLayout alloc] initWithView:self];
            objc_setAssociatedObject(self, &kInstalledMAAutoLayoutKey, autolayout, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
        return autolayout;
    }
    - (void)ma_makeConstraints:(void (^)(MAAutoLayout *))make{
        make(self.ma_layout);
        [self.ma_layout active];
    }
    - (void)ma_remakeConstraints:(void (^)(MAAutoLayout * _Nonnull))make{
        [self.ma_layout deactivate];
        [self ma_makeConstraints:make];
    }
    - (MAViewAttribute *)ma_left{
        return [self viewAttribute:NSLayoutAttributeLeft];
    }
    #####省略right等,详细请看GitHub上的demo#####
    #pragma mark - iOS11 safeArea
    - (MAViewAttribute *)ma_safeAreaLayoutGuideTop{
        return [self safeAreaViewAttribute:NSLayoutAttributeTop];
    }
    #####省略safeAreaLeft等,详细请看GitHub上的demo#####
    - (UIEdgeInsets)ma_safeAreaInsets{
        UIEdgeInsets safeInsets = UIEdgeInsetsZero;
    #ifdef __IPHONE_11_0
        if (@available(iOS 11.0, *)) {
            safeInsets = self.safeAreaInsets;
        }
    #endif
        return safeInsets;
    }
    #pragma mark - private
    - (MAViewAttribute *)viewAttribute:(NSLayoutAttribute)layoutAttribute {
        return [[MAViewAttribute alloc] initWithItem:self layoutAttribute:layoutAttribute];
    }
    - (MAViewAttribute *)safeAreaViewAttribute:(NSLayoutAttribute)layoutAttribute {
        id item = self;
    #ifdef __IPHONE_11_0
        if (@available(iOS 11.0, *)) {
            item = self.safeAreaLayoutGuide;
        }
    #endif
        return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:layoutAttribute];
    }
    
    @end
    

    UIViewController+MAAutoLayout.m的实现如下:

    @implementation UIViewController (MAAutoLayout)
    
    - (MAViewAttribute *)ma_topLayoutGuideTop{
        return [[MAViewAttribute alloc] initWithItem:self.topLayoutGuide layoutAttribute:NSLayoutAttributeTop];
    }
    #####省略topLayoutGuideBottom等,详细请看GitHub上的demo#####
    - (MAViewAttribute *)ma_safeAreaTopLayoutGuide{
        id item = self.topLayoutGuide;
        NSLayoutAttribute attribute = NSLayoutAttributeBottom;
    #ifdef __IPHONE_11_0
        if (@available(iOS 11.0, *)) {
            item = self.view.safeAreaLayoutGuide;
            attribute = NSLayoutAttributeTop;
        }
    #endif
        return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:attribute];
    }
    
    - (MAViewAttribute *)ma_safeAreaBottomLayoutGuide{
        id item = self.bottomLayoutGuide;
        NSLayoutAttribute attribute = NSLayoutAttributeTop;
    #ifdef __IPHONE_11_0
        if (@available(iOS 11.0, *)) {
            item = self.view.safeAreaLayoutGuide;
            attribute = NSLayoutAttributeBottom;
        }
    #endif
        return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:attribute];
    }
    
    @end
    

    此时完成了AutoLayout的简单封装。这样封装还有一个不便的地方,就是每个view每次都需要四个约束才能完成布局。像Masonry就提供了很多遍历方法,比如 make.top.left.right.bottom.equalTo(self.view).offset(10);或make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(10, 10, 10, 10));这是一条语句添加了多个约束,所以Masonry添加了MASCompositeConstraint的概念。
    MAAutoLayout通过一个文件已经实现了链式响应的处理,如果实现的autoLayout比较简单,引入Masonry这么大的库有些浪费,可以使用MAAutoLayout,简单使用也挺好。
    以上只是我的理解,有错误的地方请大家指出,大家多沟通交流。
    附上demo地址

    相关文章

      网友评论

          本文标题:Masonry思路解析

          本文链接:https://www.haomeiwen.com/subject/htgdbxtx.html