zoukankan      html  css  js  c++  java
  • Masonry1.0.2 源码解析

    在了解Masonry框架之前,有必要先了解一下自动布局的概念。在iOS6之前,UI布局的方式是通过frame属性和Autoresizing来完成的,而在iOS6之后,苹果公司推出了AutoLayout的布局方式,它是一种基于约束性的、描述性的布局系统,尤其是苹果的手机屏幕尺寸变多之后,AutoLayout的应用也越来越广泛。

    但是,手写AutoLayout布局代码是十分繁琐的工作(不熟悉的话,可以找资料体验一下,保证让你爽到想哭,^_^);鉴于此,苹果又开发了VFL的布局方式,虽然简化了许多,但是依然需要手写很多代码;如果,你希望不要手写代码,那么可以用xib来布局UI,可以图形化添加约束,只是xib的方式不太适合多人协作开发。综合以上的各种问题,Masonry出现了,这是一款轻量级的布局框架,采用闭包、链式编程的技术,通过封装系统的NSLayoutConstraints,最大程度地简化了UI布局工作。

    本文主要分析一下Masonry的源码结构、布局方式和实现原理等等。

    框架结构

    Masonry框架的源码其实并不复杂,利用自己的描述语言,采用优雅的链式语法,使得自动布局方法简洁明了,并且同时支持iOSMacOS两个系统。Masonry框架的核心就是MASConstraintMaker类,它是一个工厂类,根据约束的类型会创建不同的约束对象;单个约束会创建MASViewConstraint对象,而多个约束则会创建MAXCompositeConstraint对象,然后再把约束统一添加到视图上面。

    图1

    布局方式

    Masonry的布局方式比较灵活,有mas_makeConstraints(创建布局)、mas_updateConstraints(更新布局)、mas_remakeConstraints(重新创建布局)三种:

    1.mas_makeConstraints:

    给视图(视图本身或父视图)添加新的约束

    // 给view1添加约束,frame和superView一样
    [view1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(superview);
    }];
    

    2.mas_updateConstraints:

    更新视图的约束,从视图中查找相同的约束,如果找到,就更新,会设置makerupdateExistingYES

    // 更新view1的上边距离superView为60,宽度为100
    [view1 mas_updateConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(@60);
        make.width.equalTo(@100);
    }];
    

    3.mas_remakeConstraints:

    给视图添加约束,如果视图之前已经添加了约束,则会删除之前的约束,会设置makerremoveExistingYES

    // 重新设置view1的约束为:顶部距离父视图为300,左边距离父视图为100,宽为100,高为50
    [view1 mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(superview).offset(300);
        make.left.equalTo(superview).offset(100);
        make.width.equalTo(@100);
        make.height.equalTo(@50);
    }];
    

    Masonry的布局相对关系也有三种:.equalTo(==)、.lessThanOrEqualTo(<=)、.greaterThanOrEqualTo(>=)。

    Masonry的布局关系的参数也有三种:

    1. @100 --> 表示指定具体值

    2. view --> 表示参考视图的相同约束属性,如view1的left参考view2的left等

    3. view.mas_left --> 表示参考视图的特定约束属性,如view1的left参考view2的right等

    实现原理

    Masonry是利用闭包和链式编程的技术实现简化操作的,所以需要对闭包和链式编程有一定的基础。下面会根据案例来具体分析一下Masonry的实现细节,代码实现的功能是设置view1frameCGRectMake(100, 100, 100, 100);其中,mas_equalTo(...)是宏,会被替换成equalTo(MASBoxValue((...))),功能是把基本类型包装成对象类型:

    [view1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.top.equalTo(@100);
        make.size.mas_equalTo(CGSizeMake(50, 50));
    }];
    

    1.创建maker

    首先调用mas_makeConstraints:,这是一个UIView的分类方法,参数是一个设置约束的block,会把调用视图作为参数创业一个maker

    // View+MASAdditions.h
    - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        // 创建maker,并保存调用该方法的视图view
        MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
        block(constraintMaker);
        return [constraintMaker install];
    }
    

    2.生成约束

    接下来,开始利用maker产生约束,即调用block(constraintMaker)

    2.1 设置坐标x

    make.left
    

    调用过程:

    // MASConstraintMaker.h
    - (MASConstraint *)left {
        return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
    }
    
    - (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]) {
            // 传入的参数是nil,所以此处代码不会执行
            ...
        }
        if (!constraint) {
            // 设置newConstraint的代理为maker
            newConstraint.delegate = self;
            // 把约束加入到数组中
            [self.constraints addObject:newConstraint];
        }
        // 返回MASViewConstraint类型的约束对象
        return newConstraint;
    }
    

    其中,上述代码根据maker保存的view和传入的约束属性layoutAttribute创建了一个MASViewAttribute对象,然后根据viewAttribute对象创建了一个MASViewConstraint约束对象,代码如下:

    // MASViewAttribute.h
    - (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
        self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
        return self;
    }
    
    - (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
        self = [super init];
        if (!self) return nil;
        
        // 保存视图view
        _view = view;
        _item = item;
        // 保存约束属性:NSLayoutAttributeLeft
        _layoutAttribute = layoutAttribute;
        
        return self;
    }
    
    // MASViewConstraint.h
    - (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
        self = [super init];
        if (!self) return nil;
        
        // 保存第一个属性(封装了视图view和约束属性NSLayoutAttributeLeft)
        _firstViewAttribute = firstViewAttribute;
        self.layoutPriority = MASLayoutPriorityRequired;
        self.layoutMultiplier = 1;
        
        return self;
    }
    

    2.2 设置坐标y

    make.left.top
    

    由于make.left返回的是MASViewConstraint对象,所以调用的top应该是MASViewConstraint类中的方法(该方法继承自父类MASConstraint),调用过程如下:

    // MASConstraint.h
    - (MASConstraint *)top {
        // self是MASViewConstraint对象
        return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
    }
    
    // MASViewConstraint.h
    - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
        NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
    
        // self.delegate是maker对象
        return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    }
    
    // MASConstraintMaker.h
    - (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]) {
            // 由于参数constraint不为nil,所以进入此处执行
            //replace with composite constraint
            NSArray *children = @[constraint, newConstraint];
            // 创建约束集合对象,并把先前的约束对象和本次新创建的约束对象保存到数组中
            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
            // 设置约束集合对象的代理为maker
            compositeConstraint.delegate = self;
            // 用约束集合对象替换maker中已经保存的约束对象,因为我们同一个maker设置了2个以上的约束
            [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
            // 返回MASCompositeConstraint约束集合对象
            return compositeConstraint;
        }
        if (!constraint) {
            ...
        }
        return newConstraint;
    }
    

    如果一个maker添加多个约束后,就会创建MASCompositeConstraint对象,创建约束集合的过程如下:

    - (id)initWithChildren:(NSArray *)children {
        self = [super init];
        if (!self) return nil;
    
        // 保存约束数组
        _childConstraints = [children mutableCopy];
        for (MASConstraint *constraint in _childConstraints) {
            // 设置数组中所有的约束对象的代理为MASCompositeConstraint对象
            constraint.delegate = self;
        }
    
        return self;
    }
    

    在创建了MASCompositeConstraint对象后,就会更新maker中的约束数组,在最后添加约束的时候,就会是全部的约束对象,代码如下:

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

    2.3 设置x、y的值

    make.left.top.equalTo(@100)
    

    make.left.top返回的对象是MASCompositeConstraint类型,调用过程如下:

    // MASConstraint.h
    - (MASConstraint * (^)(id))equalTo {
        return ^id(id attribute) {
            return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
        };
    }
    
    // MASCompositeConstraint.h
    - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
        return ^id(id attr, NSLayoutRelation relation) {
            for (MASConstraint *constraint in self.childConstraints.copy) {
                // 遍历数组,把每个MASViewConstraint对象都调用该方法
                constraint.equalToWithRelation(attr, relation);
            }
            return self;
        };
    }
    
    // MASViewConstraint.h
    - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
        return ^id(id attribute, NSLayoutRelation relation) {
            if ([attribute isKindOfClass:NSArray.class]) {
                // 由于attribute是@100的包装类型,不是数组,此处代码不会执行
                ...
            } else {
                NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
                // 设置约束类别为NSLayoutRelationEqual
                self.layoutRelation = relation;
                // 设置第二个属性
                self.secondViewAttribute = attribute;
                return self;
            }
        };
    }
    
    - (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
        _layoutRelation = layoutRelation;
        // 表明已经有了约束关系
        self.hasLayoutRelation = YES;
    }
    

    下面分析一下设置第二个属性secondViewAttribute的过程,因为Masonry重写了setter方法,过程如下:

    // MASViewConstraint.h
    - (void)setSecondViewAttribute:(id)secondViewAttribute {
        if ([secondViewAttribute isKindOfClass:NSValue.class]) {
            // secondViewAttribute是@100类型
            [self setLayoutConstantWithValue:secondViewAttribute];
        } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
            // secondViewAttribute是视图UIView类型
            _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
        } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
            // secondViewAttribute是view.mas_left类型
            _secondViewAttribute = secondViewAttribute;
        } else {
            NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
        }
    }
    
    // MASConstraint.h   @100类型
    - (void)setLayoutConstantWithValue:(NSValue *)value {
        // 根据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);
        }
    }
    

    由于@100NSNumber类型,所以执行self.offset来设置偏移量,代码如下:

    // MASViewConstraint.h
    - (void)setOffset:(CGFloat)offset {
        // 设置layoutConstant属性值,在最后添加属性时作为方法参数传入
        self.layoutConstant = offset;
    }
    
    - (void)setLayoutConstant:(CGFloat)layoutConstant {
        _layoutConstant = layoutConstant;
    
    #if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
        ...
    #else
        self.layoutConstraint.constant = layoutConstant;
    #endif
    }
    

    这里有网友疑惑,因为self.layoutConstraint在上面的方法中一直是nil,设置它的constant属性是没有意义的,不知道这么写有何意义?其实,我也有同样的疑问!!!

    2.4 设置size

    另外,make.size的实现过程和上面的分析类似,有兴趣的可以自行参考,看一看具体的实现过程,在此不做分析。

    3.安装约束

    下面分析一下约束的安装过程

    [constraintMaker install]
    

    调用过程如下:

    // MASConstraintMaker.h
    - (NSArray *)install {
        if (self.removeExisting) {
            // 是remake,所以要先删除已经视图中存在的约束
            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.h
    - (void)uninstall {
        if ([self supportsActiveProperty]) {
            // 如果 self.layoutConstraint 响应了 isActive 方法并且不为空,会激活这条约束并添加到 mas_installedConstraints 数组中,最后返回
            self.layoutConstraint.active = NO;
            [self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
            return;
        }
        
        [self.installedView removeConstraint:self.layoutConstraint];
        self.layoutConstraint = nil;
        self.installedView = nil;
        
        [self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
    }
    

    下面分析一下install的过程:

    - (void)install {
        // 如果已经安装过约束,直接返回
        if (self.hasBeenInstalled) {
            return;
        }
        
        // 如果 self.layoutConstraint 响应了 isActive 方法并且不为空,会激活这条约束并添加到 mas_installedConstraints 数组中,最后返回
        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) {
            // 如果第一个属性不是size属性,并且第二个属性为nil,就把第二个视图设置为view的父视图,约束属性设置为view的约束属性
            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) {
            // 如果第一个属性是设置size的,就把第一个视图赋值给installedView
            self.installedView = self.firstViewAttribute.view;
        } else {
            // 否则就取第一个视图的父视图赋值给installedView
            self.installedView = self.firstViewAttribute.view.superview;
        }
    
    
        MASLayoutConstraint *existingConstraint = nil;
        if (self.updateExisting) {
            // 如果是更新属性,就根据layoutConstraint查看视图中是否存在该约束
            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];
        }
    }
    

    求两个视图的最小父视图的代码如下:

    - (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
        MAS_VIEW *closestCommonSuperview = nil;
    
        MAS_VIEW *secondViewSuperview = view;
        while (!closestCommonSuperview && secondViewSuperview) {
            MAS_VIEW *firstViewSuperview = self;
            while (!closestCommonSuperview && firstViewSuperview) {
                if (secondViewSuperview == firstViewSuperview) {
                    // 如果first和second的视图一样,就设置closestCommonSuperview,并返回
                    closestCommonSuperview = secondViewSuperview;
                }
                firstViewSuperview = firstViewSuperview.superview;
            }
            secondViewSuperview = secondViewSuperview.superview;
        }
        return closestCommonSuperview;
    }
    

    其实,上述代码是先判断firstsecond的视图是否一样,如果一样,直接返回;如果不一样,就判断fisrt的父视图和second是否一样,如果一样,就返回;不一样,继续判断first的父视图和second的父视图是否一样,如果一样,就返回;不一样,重复迭代。


    结束语

    Masonry的源码分析完结,如果文中有不足之处,希望指出,互相学习。

    参考资料

    Masonry

    Masonry 源码解析

    RAC之masonry源码深度解析

    Masonry 源码进阶

    学习AutoLayout(VFL)

    iOS开发-自动布局篇:史上最牛的自动布局教学!

  • 相关阅读:
    jQuery EasyUI API 中文文档 可调整尺寸
    jQuery EasyUI API 中文文档 链接按钮(LinkButton)
    jQuery EasyUI API 中文文档 手风琴(Accordion)
    jQuery EasyUI API 中文文档 表单(Form)
    jQuery EasyUI API 中文文档 组合(Combo)
    jQuery EasyUI API 中文文档 布局(Layout)
    jQuery EasyUI API 中文文档 拆分按钮(SplitButton)
    jQuery EasyUI API 中文文档 菜单按钮(MenuButton)
    jQuery EasyUI API 中文文档 搜索框
    jQuery EasyUI API 中文文档 验证框(ValidateBox)
  • 原文地址:https://www.cnblogs.com/fishbay/p/7402482.html
Copyright © 2011-2022 走看看