源码阅读:Masonry(一)——从使用入手

栏目: IOS · 发布时间: 6年前

内容简介:本来不想贴直接使用原生API的实现方式,但是文章写到一半发现没有对原生API的解释,如果我们直接用官方提供的其实直接使用

本来不想贴直接使用原生API的实现方式,但是文章写到一半发现没有对原生API的解释, Masonry 的实现也不太好解释,于是就添加了这个第0节,对 NSLayoutConstraint 这个类稍微介绍一下。

如果我们直接用官方提供的 NSLayoutConstraint 类进行布局,应该这样写:

UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:redView];
    
NSLayoutConstraint *constraint1 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100.0];
NSLayoutConstraint *constraint2 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:200.0];
NSLayoutConstraint *constraint3 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:200];
NSLayoutConstraint *constraint4 = [NSLayoutConstraint constraintWithItem:redView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.0 constant:0];
NSArray<NSLayoutConstraint *> *constraints = @[constraint1, constraint2, constraint3, constraint4];
[self.view addConstraints:constraints];
复制代码
  • 首先把要设置 autolayout 的控件的 translatesAutoresizingMaskIntoConstraints 属性设置为 NO ,这个属性默认是 YES
  • 然后通过 NSLayoutConstraint 类提供的工厂方法 constraintWithItem: attribute: relatedBy: toItem: attribute: multiplier: constant: 创建约束对象。
  • 最后利用控件的 addConstraints: 方法,将约束对象添加到控件上。

其实直接使用 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;
复制代码

看这个方法的目的是理解其各个参数的意义,这样在接下里看 Masonry 时,就能好理解的多:

  • 苹果官方文档中给出的约束公式是:view1.attr1 <relation> multiplier × view2.attr2 + c。
  • view1:是指要设置的约束的目标视图。
  • attr1:是指view1要设置约束的属性,是视图的顶部、宽度,还是其他什么的。
  • relation:是指两个视图属性的关系,一共有三种,分别是不大于、等于和不小于。
  • view2:是指要设置约束的参考视图。
  • attr2:是指view2要设置约束的属性。
  • multiplier:是指约束要乘的倍率。
  • c:是指约束要加的大小

1.使用Masonry

使用 Masonry 布局就简介很多,同样的布局如下:

UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
    
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.height.mas_equalTo(100.0);
    make.top.equalTo(self.view).offset(50.0);
    make.centerX.equalTo(self.view);
}];
复制代码

2.创建布局环境

通过上一节可以看到所有的布局都是在 mas_makeConstraints: 方法中进行的,点击方法进入查看实现:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    // 用autolayout布局要设置为NO
    self.translatesAutoresizingMaskIntoConstraints = NO;
    // 创建约束创建者对象
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    // 通过block回调约束创建者对象
    block(constraintMaker);
    // 返回所有添加的约束
    return [constraintMaker install];
}
复制代码

在这个方法的实现中,就是创建一个管理约束的对象,然后通过block回调用以添加约束,添加完成后设置添加的约束。

这个方法更像是创建了一个用于设置约束的环境,用户只需要通过block设置约束即可,其他的都不需要操心。

3.添加约束

我们可以通过 MASConstraintMaker 对象提供的属性为控件添加各种各样的约束,我们选取一个来查看其具体实现:

make.top.equalTo(self.view).offset(50.0);
复制代码

3.1 属性

在上一节中,我们已经知道了对象 makeMASConstraintMaker 类型,所以直接进入 MASConstraintMaker 类中查看其 top 属性的实现:

- (MASConstraint *)top {
    // 调用了另一个方法,并把要设置约束的位置传递过去
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
复制代码

继续点击查看:

- (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];
    // 如果传入的约束对象是MASViewConstraint类及其子类
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        // 利用已经添加的约束对象和新添加的约束对象创建多视图约束封装对象
        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;
}
复制代码

在这一步中,可以看到会返回一个 MASViewConstraint 类或 MASCompositeConstraint 类的对象,这两个类都是 MASConstraint 的子类,可以说是“兄弟类”,它们保存了约束属性及其所在的视图,也就是 view1attr1

3.2 关系

- (MASConstraint * (^)(id))equalTo {
    // 返回一个返回值类型为id,参数类型是id的block
    return ^id(id attribute) {
        // 调用下面的方法添加约束关系
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
复制代码

在上面的代码中我们看到 make.top 返回的是 MASConstraint 的子类 MASViewConstraintMASCompositeConstraint 类,所以在 equalTo 这个方法中调用的其实是 MASViewConstraint 类或 MASCompositeConstraint 类的对象方法 equalToWithRelation

先看 MASViewConstraint 类对这个方法的实现:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    // 返回一个返回值为id类型,参数为id和NSLayoutRelation类型的block
    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 {    
            // 如果传入的属性不是数组类型的
            // 不能重复设置约束关系
            // 如果已经设置了约束关系,必须和原约束关系相同,并且属性必须是NSValue类型的
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            // 保存约束关系和约束属性
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            // 返回当前类对象
            return self;
        }
    };
}
复制代码

接着看一下 MASViewConstraint 类中两个 setter 的实现:

- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
    // 除了保存约束关系,还保存了是否设置了约束关系
    _layoutRelation = layoutRelation;
    self.hasLayoutRelation = YES;
}
复制代码

这个方法没啥好说的,就是保存了一下。

- (void)setSecondViewAttribute:(id)secondViewAttribute {
    if ([secondViewAttribute isKindOfClass:NSValue.class]) {
        // 如果参数是NSValue类型的,根据值的类型设置不同的属性
        [self setLayoutConstantWithValue:secondViewAttribute];
    } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
        // 如果参数是UIView类型的,就生成 view2 的视图属性封装对象,其中 attr2 和 view1 的 view1 相同,并保存
        _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
    } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
        // 如果参数是MASViewAttribute类型的,就直接保存
        _secondViewAttribute = secondViewAttribute;
    } else {
        // 只允许输入 NSValue 、 UIView 和 MASViewAttribute 这三种类型的数据
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}
复制代码

这个方法中的三个条件其实分别对应下面的三种输入情况:

make.width.equalTo(@200);
make.centerX.equalTo(self.view);
make.left.equalTo(self.view.mas_left);

在这一步中,实际做的工作就是保存布局关系和约束的参考视图。但有一点需要注意的是 Masonry 通过将方法的返回值设置成一个返回值是当前类类型的block,来实现链式编程的效果,也就是 relationview2 以及 attr2

3.3 常数

  • 首先看父类 MASConstraint 的实现:
- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}
复制代码

是不是熟悉的配方?是不是熟悉的味道?和 equalTo 一样,都是通过返回一个返回值是 id 类型的block来实现链式编程的效果。其中的实现也很简单,就是保存了一下传入的参数。

  • 再看其子类 MASViewConstraint 中的实现:
- (void)setOffset:(CGFloat)offset {
    self.layoutConstant = offset;
}
复制代码
- (void)setLayoutConstant:(CGFloat)layoutConstant {
    _layoutConstant = layoutConstant;

#if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
    if (self.useAnimator) {
        [self.layoutConstraint.animator setConstant:layoutConstant];
    } else {
        self.layoutConstraint.constant = layoutConstant;
    }
#else
    self.layoutConstraint.constant = layoutConstant;
#endif
}
复制代码

在这个子类中就是保存了一下传入的常数。

  • 还有子类 MASCompositeConstraint 中的实现:
- (void)setOffset:(CGFloat)offset {
    // 遍历所有视图属性封装对象,设置参数
    for (MASConstraint *constraint in self.childConstraints) {
        constraint.offset = offset;
    }
}
复制代码

这一步就是设置常数,也就是 c

到此为止,约束所需的参数都设置完成,下面就是设置约束了。

4.设置约束

在第 2 节中 mas_makeConstraints: 方法的最后一句就是设置约束:

return [constraintMaker install];
复制代码

我们点进 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;
}
复制代码

逻辑很明白,就是先移除之前已经添加的约束,在设置要设置的约束。

然后看一下解除约束的方法实现:

- (void)uninstall {
    // 这个为了兼容 iOS8 之前的版本,因为属性 active 是iOS8 才开始生效的
    if ([self supportsActiveProperty]) {
        // 将约束的活动状态设置为NO
        self.layoutConstraint.active = NO;
        // 从保存集合中移除
        [self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
        // 返回
        return;
    }
    
    // 如果是 iOS8 之前的版本
    // 移除掉视图的约束
    [self.installedView removeConstraint:self.layoutConstraint];
    // 属性置空
    self.layoutConstraint = nil;
    self.installedView = nil;
    
    // 从保存集合中移除
    [self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
}
复制代码

接着看设置约束的方法实现:

- (void)install {
    // 先判断当前约束是否已被设置,如果设置了就不需要继续向下执行了
    if (self.hasBeenInstalled) {
        return;
    }
    
    // 同样是为了兼容 iOS8 
    if ([self supportsActiveProperty] && self.layoutConstraint) {
        // 将约束的活动状态设置为YES
        self.layoutConstraint.active = YES;
        // 将约束添加到集合中保存
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }
    
    // iOS7 之前版本的设置方式
    // 获取设置的各个参数
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    // 如果设置了像 make.left.equalTo(@10) 这样的约束
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        // view2 就是 view1 的父视图
        secondLayoutItem = self.firstViewAttribute.view.superview;
        // attr2 就是 attr1
        secondLayoutAttribute = firstLayoutAttribute;
    }
    
    // 创建约束对象
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    
    // 设置优先级和key
    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
    
    if (self.secondViewAttribute.view) {
        // 如果设置了 view2
        // 获取 view1 和 view2 的公共父视图
        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 类型的, 要设置约束的视图就是 view1
        self.installedView = self.firstViewAttribute.view;
    } else {
        // 否则,要设置约束的视图就是 view1 的父视图
        self.installedView = self.firstViewAttribute.view.superview;
    }

    // 创建变量保存之前添加的约束
    MASLayoutConstraint *existingConstraint = nil;
    // 如果需要更新约束
    if (self.updateExisting) {
        // 获取之前的约束
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }

    if (existingConstraint) {
        // 如果有之前的约束
        // 更新约束
        existingConstraint.constant = layoutConstraint.constant;
        // 保存当前约束
        self.layoutConstraint = existingConstraint;
    } else {
        // 如果没有之前的约束
        // 向视图设置约束
        [self.installedView addConstraint:layoutConstraint];
        // 保存当前约束
        self.layoutConstraint = layoutConstraint;
        // 将约束添加到集合中保存
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}
复制代码

这个方法里面还有个比较两个约束是否相似的方法:

- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
    // 遍历要设置约束视图的所有已设置的约束
    for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
        // 如果已设置的约束不是 MASLayoutConstraint 类型的,跳过
        if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
        // 如果已设置的约束的 view1 和要设置约束的 view1 不相同,跳过
        if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
        // 如果已设置的约束的 view2 和要设置约束的 view2 不相同,跳过
        if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
        // 如果已设置的约束的 attr1 和要设置约束的 attr1 不相同,跳过
        if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
        // 如果已设置的约束的 attr2 和要设置约束的 attr2 不相同,跳过
        if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
        // 如果已设置的约束的 relation 和要设置约束的 relation 不相同,跳过
        if (existingConstraint.relation != layoutConstraint.relation) continue;
        // 如果已设置的约束的 multiplier 和要设置约束的 multiplier 不相同,跳过
        if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
        // 如果已设置的约束的优先级和要设置约束的优先级不相同,跳过
        if (existingConstraint.priority != layoutConstraint.priority) continue;
        
        // 返回幸存者,也就是除了常数 c 其他参数都要相同
        return (id)existingConstraint;
    }
    // 如果没有符合条件的就返回空对象
    return nil;
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

疯长

疯长

[美]肖恩· 阿美拉蒂 / 中信出版集团 / 2018-10 / 45

实现财务回报以及扩大影响力是企业家长期关注和讨论的问题。 为什么有些公司实现了10倍的投资回报,而其他的则勉力支撑?产品类似的公司,为什么有的家喻户晓,有的默默无闻直至退出市场…… 为了了解真相,作者阿美拉蒂在这本书中精选10组对照公司,比如,同为社交平通的Facebook(脸谱网)和Friendster(交友网),同为快餐领域先驱的麦当劳和白色城堡,再比如都在开发电动汽车市场的特斯拉......一起来看看 《疯长》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HTML 编码/解码
HTML 编码/解码

HTML 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换