源碼閱讀:Masonry(一)——從使用入手

該文章閱讀的Masonry的版本爲1.1.0。編程

0.原生實現

原本不想貼直接使用原生API的實現方式,可是文章寫到一半發現沒有對原生API的解釋,Masonry的實現也不太好解釋,因而就添加了這個第0節,對NSLayoutConstraint這個類稍微介紹一下。數組

若是咱們直接用官方提供的NSLayoutConstraint類進行佈局,應該這樣寫:bash

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時,就能好理解的多:ui

  • 蘋果官方文檔中給出的約束公式是:view1.attr1 <relation> multiplier × view2.attr2 + c
  • view1:是指要設置的約束的目標視圖。
  • attr1:是指view1要設置約束的屬性,是視圖的頂部、寬度,仍是其餘什麼的。
  • relation:是指兩個視圖屬性的關係,一共有三種,分別是不大於、等於和不小於。
  • view2:是指要設置約束的參考視圖。
  • attr2:是指view2要設置約束的屬性。
  • multiplier:是指約束要乘的倍率。
  • c:是指約束要加的大小

1.使用Masonry

使用Masonry佈局就簡潔不少,一樣的佈局以下:spa

UIView *redView = [UIView new];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
    
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.size.mas_equalTo(CGSizeMake(200.0, 100.0));
    make.top.equalTo(self.view).offset(50.0);
    make.centerX.equalTo(self.view);
}];
複製代碼

2.建立佈局環境

經過上一節能夠看到全部的佈局都是在mas_makeConstraints:方法中進行的,點擊方法進入查看實現:code

- (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設置約束便可,其餘的都不須要操心。ip

3.添加約束

咱們能夠經過MASConstraintMaker類對象提供的屬性爲控件添加各類各樣的約束,咱們選取一個來查看其具體實現:rem

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);
    }
}
複製代碼

這個方法中的三個條件其實分別對應下面的三種輸入狀況:

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

在這一步中,實際作的工做就是保存佈局關係和約束的參考視圖,也就是 relationview2 以及 attr2。但有一點須要注意的是Masonry經過將方法的返回值設置成一個返回值是當前類類型的block,來實現鏈式編程的效果。

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;
}
複製代碼

5.總結

  • 首先咱們會調用 View+MASAdditions.h 分類中的 mas_makeConstraints: 方法用來建立設置約束的環境,以及獲取約束工廠類 MASConstraintMaker 的對象 make
  • 接着,咱們調用 make 的屬性設置約束的屬性 attr1。例如 make.top。這個時候在 make 對象內部:
    • 首先會建立一個視圖屬性封裝類 MASViewAttribute 對象,裏面封裝着 view1attr1
    • 再用上一步建立的對象建立視圖約束封裝類 MASViewConstraint 對象並返回,這個對象就負責管理着 NSLayoutConstraint 類對象。
  • 而後,咱們調用上一步返回的對象方法來設置約束的關係、view2attr2。例如 make.top.equalTo(redView.mas_bottom)。這時在MASViewConstraint 對象內部:
    • 首先會建立一個視圖屬性封裝類 MASViewAttribute 對象,裏面封裝着 view1attr1
    • 而後將上一步建立的對象保存到當前對象的屬性中,並返回當前對象。
  • 接着,再設置約束的常數。例如,make.top.equalTo(redView.mas_bottom).offset(30.0);。在這一步中,MASViewConstraint 對象只是保存了一下傳入的參數。
  • 設置完約束後,在 mas_makeConstraints: 方法中,就會調用 make 對象的 install 方法。make 對象會調用剛纔建立的 MASViewConstraint 對象的install 方法。
  • 最後,在 MASViewConstraint 對象的install 方法中,經過剛纔設置的約束的參數建立 NSLayoutConstraint 對象,並添加到視圖上。
相關文章
相關標籤/搜索