頁面佈局知識梳理

在iOS中佈局UI經常使用的幾種方式

  1. 經過設置視圖的frame
CGRectMake(<#CGFloat x#>, <#CGFloat y#>, <#CGFloat width#>, <#CGFloat height#>)
複製代碼

設置view.frame會當即生效,但明顯這種方式會將視圖固定死,若是要在不一樣尺寸的屏幕上都顯示完美比較難,可能須要些幾套UI或者設置比例。算法

  1. 使用Autoresizing來適配父視圖bounds發生改變的狀況 數組

  2. 使用AutoLayout自動佈局技術 蘋果自iOS 6開始引入AutoLayout自動佈局技術,通過一系列的優化,開發效率大大提升,蘋果官方也推薦使用AutoLayout自動佈局技術來佈局UI界面。使用AutoLayout佈局時,能夠指定一系列的視圖約束,如:高度、寬度。整個界面上的全部約束組合在一塊兒就明確的定義了整個界面的佈局。bash

自動佈局AutoLayout

功能實現

  1. 利用NSLayoutConstraint類給視圖建立具體的約束對象:
NSLayoutConstraint *layoutConstraint = [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeRight multiplier:1 constant:20.f];
複製代碼

公式:函數

view1.attribute = view2.attribute * multiplier + constant;
複製代碼
  1. 添加約束到相應的視圖上:

API:佈局

- (void)addConstraint:(NSLayoutConstraint *)constraint;
- (void)addConstraints:(NSArray *)constraints;
複製代碼

約束添加規則:性能

  • 對於兩個相同層級上的視圖之間的約束關係(約束對象),添加到它們的父視圖上。
  • 對於兩個不一樣層級上的視圖之間的約束關係,添加到它們最近的共同父視圖上。
  • 對於兩個有層級關係的視圖之間的約束,添加到層級較高的父視圖上。
  1. 注意事項
  • 使用autolayout就要禁止autoresizing功能:view.translatesAutoresizingMaskIntoConstraints = NO;
  • 由上面的約束添加規則可知:添加約束以前,必需要肯定視圖之間的層級關係,所以必定要保證相關控件都已經被添加到了各自的父視圖上。
  • 不要重複對視圖佈局,即不須要再設置frame等。

實現原理

iOS中想要肯定一個視圖的佈局只須要兩個屬性:origin/center和size。也就是

  • x & y(相對父視圖)
  • width & height

只要直到了這些數據就能夠固定一個視圖了。 使用view.frame就是最直接的方式,而Auto Layout中大部分的約束都是描述性語言,表示視圖間的相對距離。如上圖:優化

A.left = superView.left + 20;
    A.top = superView.top + 25;
    A.width = 100;
    A.height = 100;
    
    B.left = A.left + 20;
    B.top = A.top;
    B.width = A.width;
    B.height = A.height;
複製代碼

咱們經過求解👆的八元一次方程組,是能夠將A、B兩個視圖佈局所須要的信息(x、y、width、height)求解出來的。實質上由AutoLayout引擎在運行時求解上述的方程組,最終使用frame來繪製視圖。所以也能夠知道AutoLayout不能當即生效的(systemLayoutSizeFitting能夠獲取自動佈局結果),不一樣於直接設置frame。動畫

Auto Layout 其實就是對 Cassowary(將佈局問題抽象成線性等式和不等式約束進行求解) 算法的一種實現。ui

性能

使用AutoLayout佈局,就是讓整個界面上的全部約束(線性等式/不等式)在一塊兒明確的、無衝突定義整個界面的佈局。(當發生衝突時,AutoLayout會先嚐試打破一些優先級低的約束,儘可能知足優先級高的約束)spa

使用AutoLayout的過程是須要先求解方程組的,獲得結果再設置frame。這樣的時間複雜度就遠高於直接設置frame了。當視圖層級很深或者對性能要求較高的頁面使用自動佈局的性能就差強人意了。

第三方封裝 Masonry

實現
  1. 使用AutoLayout佈局代碼:
NSLayoutConstraint *layoutConstraint1 = [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeRight multiplier:1 constant:20.f];
NSLayoutConstraint *layoutConstraint2 = [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeTop multiplier:1 constant:0];
[view1 addConstraints:@[layoutConstraint1,layoutConstraint2]];
複製代碼

NSLayoutConstraint 佈局約束對象

NSLayoutAttribute 佈局屬性對象(左右上下等)

NSLayoutRelation 佈局關係對象(>、<、=等待)

  1. 使用Masonry編寫相同約束:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
         make.left.equalTo(view2.mas_right).offset(20);
         make.top.equalTo(view2.mas_top);
 }];
複製代碼

能夠看出兩種方式是類似,Masonry的語言最後能夠被轉化成AutoLayout代碼。

設計到一些關鍵詞:

  • makeConstraints:
  • make:
  • left\right\top...
  • equalTo
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    // 1. 要使用AutoLayout,須要關閉autoSizing
    self.translatesAutoresizingMaskIntoConstraints = NO;
    // 2. 建立ConstraintMaker對象(它負責建立相關約束)
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    // 3. 執行block,即建立各類約束:    
    //   constraintMaker.left.equalTo(view2.mas_right).offset(20);
    //   constraintMaker.top.equalTo(view2.mas_top);
    block(constraintMaker);
    return [constraintMaker install];// 添加約束
}
複製代碼
MASConstraintMaker
// step 1
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
// step 2
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
// step 3
// MASViewAttribute對一個視圖和它的佈局屬性的封裝
// MASViewConstraint 一條 約束(view.left和view2.right)封裝了兩個MASViewAttributes:firstViewAttribute和secondViewAttribute,和以後的約束關係、offset等數值
// 返回約束對象MASViewConstraint

- (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]) {
        // constaint 存在(left),newConstraint(right)
        //採用了鏈式約束(make.left.right)就用組合約束(left.right)替換前面的舊約束(left),即用left和right的組合約束替換left約束
        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對象繼承自MASConstraint,這個約束對象仍然有left、top等佈局屬性。子類繼承父類屬性,而且它重寫了addConstraintWithLayoutAttribute:方法。

// MASConstraint.m 父類
- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}

// MASViewConstraint.m 子類
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
    // 它的代理就是MASConstraintMaker對象
    // 可是constraint不是nil而是當前的約束如make.left.top,當前約束是left,進入方法建立的是top約束,重回step3
    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}

// 在約束數組中找到舊約束,並用新的組合約束替換
- (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];
}

複製代碼

所以Masonry可使用鏈式調用,如:make.left.top;且佈局屬性返回的都是MASConstraint對象,它還有佈局關係屬性equalTo(view2.right)等。 經過添加布局關係屬性和後邊的offset等,將約束對象MASViewConstraint的約束條件補充完整了。

這樣一條條完整的約束就建立完成了。接下來須要將這些完整的約束添加到view上。即又回到了mas_makeConstaints方法上了,它最終執行[constraintMaker install]

- (NSArray *)install {
    if (self.removeExisting) {//remake 刪除原有已經設置的全部約束
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    // self.constaints是以前方法中出現的約束數組:存儲關於這個視圖的全部約束語句,讓每一個約束都執行install方法
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;// 是不是要updateConstraints
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}
複製代碼

[constraint install]方法取出該約束對象中的全部約束條件,建立了約束對象MASLayoutConstraint,它繼承自系統的NSLayoutConstaint:

MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
複製代碼

那麼這個約束要被添加到哪一個view上呢?

// 查找要被添加約束的installView
    // 1. 設置secondView,則按照添加約束規則找出共同父視圖
    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) { // 2. 未設置secondView,可是當前佈局屬性是視圖width、height。
        self.installedView = self.firstViewAttribute.view;
    } else { // 3. 以上都不是,則secondView應該默認是當前視圖的父視圖了
        self.installedView = self.firstViewAttribute.view.superview;
    }
複製代碼

若是想要更新某個約束呢?

MASLayoutConstraint *existingConstraint = nil;
    if (self.updateExisting) { // 找出和要更新的約束同類型的已經設置的約束
        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];
    }
複製代碼

equalTo()和mas_equalTo()

#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
複製代碼

equalTo()參數是id類型,而mas_equalTo()參數能夠是id、數值、CGSize等等,它就是一個宏,經過MASBoxValue函數將其中參數轉化成id類型。

left和mas_left

  1. left是maker和MASConstraint的屬性;而mas_left是UIView(MASAdditions)分類中給view添加的屬性。所以mas_left能夠用於equalTo(參數)參數。
  2. 兩個屬性最終都會被包裝成MASViewAttribute佈局屬性。
總結

綜上能夠看出: MASConstraintMaker是一個簡單工廠類(或者說工廠方法)用來生產MASConstraint約束對象。

外界經過給工廠傳遞不一樣的類型left、right等,從而生產出不一樣類型的約束對象(MASViewConstraint和MASCompositeConstraint),利用多態。

動畫

相關文章
相關標籤/搜索