簡(jiǎn)介
Masonry是 Objective-C 中用于自動(dòng)布局的第三方框架, 我們一般使用它來代替冗長(zhǎng), 繁瑣的 AutoLayout 代碼,它同時(shí)支持iOS和OS X。Masonry是一種領(lǐng)域特定語言(DSL),為自動(dòng)布局的所有功能提供便捷的方法,包括建立和修改約束、存取屬性、設(shè)置優(yōu)先級(jí)以及調(diào)試支持。Masonry的安裝推薦使用cocoa Pod方式。有關(guān)于pod的使用,將會(huì)在另一篇文章中進(jìn)行說明。
分析
Masonry的使用還是相當(dāng)簡(jiǎn)潔的
// CODE1
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.top.equalTo(self.view).with.offset(40);
make.width.equalTo(@185); make.height.equalTo(@38);}];
上邊這條代碼實(shí)現(xiàn)的簡(jiǎn)要效果是將一個(gè)button的大小設(shè)置為185*38,居中并距頁(yè)面頂部為40。而這個(gè)效果也可以很容易的從代碼中看出,沒錯(cuò),這就是Masonry,是不是很簡(jiǎn)單?
從mas_makeConstraints:開始
// CODE2
// View+MASAdditions.h
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
這個(gè)方法主要是用于約束的第一次構(gòu)建,從CODE1我們可以看到它在block方法中實(shí)現(xiàn)了對(duì)調(diào)用該方法的的控件的約束。與之相同的,也有用于更新和重構(gòu)的約束的分類方法:
// CODE3
// View+MASAdditions.h
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;//更新
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;//重構(gòu)
mas_makeConstraints
只負(fù)責(zé)新增約束 Autolayout不能同時(shí)存在兩條針對(duì)于同一對(duì)象的約束 否則會(huì)報(bào)錯(cuò) mas_updateConstraints
針對(duì)上面的情況 會(huì)更新在block中出現(xiàn)的約束 不會(huì)導(dǎo)致出現(xiàn)兩個(gè)相同約束的情況mas_remakeConstraints
則會(huì)清除之前的所有約束 僅保留最新的約束三種函數(shù)善加利用 就可以應(yīng)對(duì)各種情況了.
Constraint Maker Block
我們以mas_makeConstraints:
方法為入口來分析一下 Masonry 以及類似的框架(SnapKit)是如何工作的. mas_makeConstraints:
方法位于 UIView
的分類 MASAdditions
中.
Provides constraint maker block and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs.
這個(gè)分類為我們提供一種非常便捷的方法來配置 MASConstraintMaker
, 并為視圖添加 mas_left
mas_right
等屬性.這個(gè)方法的主要實(shí)現(xiàn)方法為:
// CODE4
// View+MASAdditions.m- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
因?yàn)?Masonry 是封裝的蘋果的 AutoLayout 框架, 所以我們要在為視圖添加約束前將translatesAutoresizingMaskIntoConstraints
屬性設(shè)置為 NO
. 如果這個(gè)屬性沒有被正確設(shè)置, 那么視圖的約束不會(huì)被成功添加.在設(shè)置 translatesAutoresizingMaskIntoConstraints
屬性之后,
- 我們會(huì)初始化一個(gè)
MASConstraintMaker
的實(shí)例. - 然后將 maker 傳入 block 配置其屬性.
- 最后調(diào)用 maker 的
install
方法為視圖添加約束.
MASConstraintMaker
MASConstraintMaker 為我們提供了工廠方法來創(chuàng)建 MASConstraint. 所有的約束都會(huì)被收集直到它們最后調(diào)用 install 方法添加到視圖上.
Provides factory methods for creating MASConstraints. Constraints are collected until they are ready to be installed
在初始化 MASConstraintMaker 的實(shí)例時(shí), 它會(huì)持有一個(gè)對(duì)應(yīng) view 的弱引用, 并初始化一個(gè) constraints 的空可變數(shù)組用來之后配置屬性時(shí)持有所有的約束.
//CODE5
// MASConstraintMaker.m
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new; return self;
}
這里的 MAS_VIEW
是一個(gè)宏, 是 UIView
的 alias.
// CODE6
// MASUtilities.h
#define MAS_VIEW UIView
Setup MASConstraintMaker
在調(diào)用 block(constraintMaker)
時(shí), 實(shí)際上是對(duì) constraintMaker
的配置.
// CODE7
make.centerX.equalTo(self.view);
make.top.equalTo(self.view).with.offset(40);
make.width.equalTo(@185);
make.height.equalTo(@38);
make.left
訪問 make
的 left
right
top
bottom
等屬性時(shí), 會(huì)調(diào)用 constraint:addConstraintWithLayoutAttribute:
方法.
// CODE8
// MASViewConstraint.m- (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]) { ... }
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
在調(diào)用鏈上最終會(huì)達(dá)到 constraint:addConstraintWithLayoutAttribute:
這一方法, 在這里省略了一些暫時(shí)不需要了解的問題. 因?yàn)樵谶@個(gè)類中傳入該方法的第一個(gè)參數(shù)一直為 nil
, 所以這里省略的代碼不會(huì)執(zhí)行.這部分代碼會(huì)先以布局屬性 left
和視圖本身初始化一個(gè) MASViewAttribute
的實(shí)例, 之后使用 MASViewAttribute
的實(shí)例初始化一個(gè) constraint
并設(shè)置它的代理, 加入數(shù)組, 然后返回.這些工作就是你在輸入 make.left
進(jìn)行的全部工作, 它會(huì)返回一個(gè) MASConstraint
, 用于之后的繼續(xù)配置.
make.left.equalTo(@80)
在 make.left
返回 MASConstraint
之后, 我們會(huì)繼續(xù)在這個(gè)鏈?zhǔn)降恼Z法中調(diào)用下一個(gè)方法來指定約束的關(guān)系.
//CODE9
// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;
- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))lessThanOrEqualTo;
這三個(gè)方法是在 MASViewConstraint
的父類, MASConstraint
中定義的.MASConstraint
是一個(gè)抽象類, 其中有很多的方法都必須在子類中覆寫的. Masonry 中有兩個(gè) MASConstraint
的子類, 分別是 MASViewConstraint
和 MASCompositeConstraint
. 后者實(shí)際上是一些約束的集合. 這么設(shè)計(jì)的原因我們會(huì)在 post 的最后解釋.先來看一下這三個(gè)方法是怎么實(shí)現(xiàn)的:
// CODE10
// MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
該方法會(huì)導(dǎo)致 self.equalToWithRelation
的執(zhí)行, 而這個(gè)方法是定義在子類中的, 因?yàn)楦割愖鳛槌橄箢悰]有提供這個(gè)方法的具體實(shí)現(xiàn).
// CODE11
// MASConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
MASMethodNotImplemented();
}
MASMethodNotImplemented
也是一個(gè)宏定義, 用于在子類未繼承這個(gè)方法或者直接使用這個(gè)類時(shí)拋出異常.
// CODE12
// MASConstraint.m
#define MASMethodNotImplemented() \ @throw [NSException exceptionWithName:NSInternalInconsistencyException \ reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \ userInfo:nil]
因?yàn)槲覀優(yōu)?equalTo
提供了參數(shù) attribute
和布局關(guān)系 NSLayoutRelationEqual
, 這兩個(gè)參數(shù)會(huì)傳遞到 equalToWithRelation
中, 設(shè)置 constraint
的布局關(guān)系和 secondViewAttribute
屬性, 為即將 maker 的 install
做準(zhǔn)備.
// CODE13
// MASViewConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) { ... }
else { ... self.layoutRelation = relation;
self.secondViewAttribute = attribute; return self;
}
};
}
我們不得不提一下 setSecondViewAttribute:
方法, 它并不只是一個(gè)簡(jiǎn)單的 setter 方法, 它會(huì)根據(jù)你傳入的值的種類賦值.
// CODE14
// MASConstraintMaker.m
- (void)setSecondViewAttribute:(id)secondViewAttribute {
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
[self setLayoutConstantWithValue:secondViewAttribute];
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
_secondViewAttribute = secondViewAttribute;
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}
第一種情況對(duì)應(yīng)的就是:
// CODE15
make.left.equalTo(@40);
傳入 NSValue
的時(shí), 會(huì)直接設(shè)置 constraint
的 offset
, centerOffset
, sizeOffset
, 或者 insets
第二種情況一般會(huì)直接傳入一個(gè)視圖:
// CODE16
make.left.equalTo(view);
這時(shí), 就會(huì)初始化一個(gè) layoutAttribute
屬性與 firstViewArribute
相同的 MASViewAttribute
, 上面的代碼就會(huì)使視圖與 view
左對(duì)齊.第三種情況會(huì)傳入一個(gè)視圖的 MASViewAttribute:
// CODE17
make.left.equalTo(view.mas_right);
使用這種寫法時(shí), 一般是因?yàn)榧s束的方向不同. 這行代碼會(huì)使視圖的左側(cè)與 view
的右側(cè)對(duì)齊.到這里我們就基本完成了對(duì)一個(gè)約束的配置, 接下來可以使用相同的語法完成對(duì)一個(gè)視圖上所有約束進(jìn)行配置, 然后進(jìn)入了最后一個(gè)環(huán)節(jié).
Install MASConstraintMaker
我們會(huì)在 mas_makeConstraints:
方法的最后調(diào)用 [constraintMaker install]
方法來安裝所有存儲(chǔ)在 self.constraints
數(shù)組中的所有約束.
// CODE18
// MASConstraintMaker.m
- (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;
}
在這個(gè)方法會(huì)先判斷當(dāng)前的視圖的約束是否應(yīng)該要被 uninstall
, 如果我們?cè)谧铋_始調(diào)用 mas_remakeConstraints:
方法時(shí), 視圖中原來的約束就會(huì)全部被 uninstall
.然后就會(huì)遍歷 constraints
數(shù)組, 發(fā)送 install
消息.
MASViewConstraint install
MASViewConstraint 的 install
方法就是最后為當(dāng)前視圖添加約束的最后的方法, 首先這個(gè)方法會(huì)先獲取即將用于初始化 NSLayoutConstraint
的子類的幾個(gè)屬性.
// CODE19
// MASViewConstraint.m
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.view;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.view;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
Masonry 之后會(huì)判斷當(dāng)前即將添加的約束是否是 size 類型的約束
// CODE20
// MASViewConstraint.m
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = firstLayoutItem.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
如果不是 size 類型并且沒有提供第二個(gè) viewAttribute
, (e.g. make.left.equalTo(@10)
;) 會(huì)自動(dòng)將約束添加到 superview
上. 它等價(jià)于:
// CODE21
make.left.equalTo(superView.mas_left).with.offset(10);
然后就會(huì)初始化 NSLayoutConstraint
的子類 MASLayoutConstraint:
// CODE22
// MASViewConstraint.mMASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:
firstLayoutItem attribute:firstLayoutAttribute relatedBy:
self.layoutRelation toItem:
secondLayoutItem attribute:
secondLayoutAttribute multiplier:
self.layoutMultiplier constant:
self.layoutConstant];
layoutConstraint.priority = self.layoutPriority;
接下來它會(huì)尋找 firstLayoutItem
和 secondLayoutItem
兩個(gè)視圖的公共 superview
, 相當(dāng)于求兩個(gè)數(shù)的最小公倍數(shù).
// CODE23
// View+MASAdditions.m
- (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) {
closestCommonSuperview = secondViewSuperview;
}
firstViewSuperview = firstViewSuperview.superview;
}
secondViewSuperview = secondViewSuperview.superview;
}
return closestCommonSuperview;
}
如果需要升級(jí)當(dāng)前的約束就會(huì)獲取原有的約束, 并替換為新的約束, 這樣就不需要再次為 view 安裝約束.
// CODE24
// MASViewConstraint.m
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];
如果原來的 view 中不存在可以升級(jí)的約束, 或者沒有調(diào)用 mas_updateConstraint: 方法, 那么就會(huì)在上一步尋找到的 installedView 上面添加約束.
// CODE25
[self.installedView addConstraint:layoutConstraint];
方法及屬性
首先列舉一些Masonry的屬性:
// CODE26
@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
@property (nonatomic, strong, readonly) MASConstraint *bottom;
@property (nonatomic, strong, readonly) MASConstraint *leading;
@property (nonatomic, strong, readonly) MASConstraint *trailing;
@property (nonatomic, strong, readonly) MASConstraint *width;
@property (nonatomic, strong, readonly) MASConstraint *height;
@property (nonatomic, strong, readonly) MASConstraint *centerX;
@property (nonatomic, strong, readonly) MASConstraint *centerY;
@property (nonatomic, strong, readonly) MASConstraint *baseline;
這些屬性與NSLayoutAttrubute
的對(duì)照表如下:
Masonry | NSAutoLayout | 說明 |
---|---|---|
left | NSLayoutAttributeLeft | 左側(cè) |
top | NSLayoutAttributeTop | 上側(cè) |
right | NSLayoutAttributeRight | 右側(cè) |
bottom | NSLayoutAttributeBottom | 下側(cè) |
leading | NSLayoutAttributeLeading | 首部 |
trailing | NSLayoutAttributeTrailing | 尾部 |
width | NSLayoutAttributeWidth | 寬 |
height | NSLayoutAttributeHeight | 高 |
centerX | NSLayoutAttributeCenterX | 橫向中點(diǎn) |
centerY | NSLayoutAttributeCenterY | 縱向中點(diǎn) |
baseline | NSLayoutAttributeBaseline | 文本基線 |
其中l(wèi)eading與left trailing與right 在正常情況下是等價(jià)的,但是當(dāng)一些布局是從右至左時(shí)(比如阿拉伯文?沒有類似的經(jīng)驗(yàn)) 則會(huì)對(duì)調(diào),換句話說就是基本可以不理不用 用left和right就好了.
此外,除了上邊提到的mas_makeConstraints
、 mas_updateConstraints
和 mas_remakeConstraints
這三種構(gòu)建約束的方法,Masonry還有一些其他方法,下面將進(jìn)行簡(jiǎn)要介紹:
// CODE27
- (MASConstraint * (^)(id))greaterThanOrEqualTo;//大于等于
- (MASConstraint * (^)(id))mas_greaterThanOrEqualTo;//大于等于
- (MASConstraint * (^)(id))lessThanOrEqualTo;//小于等于
- (MASConstraint * (^)(id))mas_lessThanOrEqualTo;//小于等于
- (MASConstraint * (^)())priorityLow;//優(yōu)先級(jí)低
- (MASConstraint * (^)())priorityMedium;//優(yōu)先級(jí)中
- (MASConstraint * (^)())priorityHigh;//優(yōu)先級(jí)高
- (MASConstraint * (^)(CGFloat))multipliedBy;//比例
使用
說到使用,其實(shí)我還是認(rèn)為Masonry在github上給出的官方demo是最好的參考,并且完全達(dá)到了能使人靈活使用的程度。下面是下載鏈接:
https://github.com/SnapKit/Masonry
這里就不再進(jìn)行贅述。
注意事項(xiàng)
在使用Masonry的過程中,總會(huì)出現(xiàn)一些意想不到的問題導(dǎo)致我們的程序Crash,所以說,下邊總結(jié)出了一些注意事項(xiàng)。
equal和mas_equal的區(qū)別
equalTo
和 mas_equalTo
的區(qū)別在哪里呢? 其實(shí) mas_equalTo
是一個(gè)MACRO
// CODE28
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))
可以看到 mas_equalTo
只是對(duì)其參數(shù)進(jìn)行了一個(gè)BOX操作(裝箱) MASBoxValue的定義具體可以看看源代碼 太長(zhǎng)就不貼出來了。它所支持的類型 除了NSNumber
支持的那些數(shù)值類型之外 就只支持CGPoint
CGSize
UIEdgeInsets
。而equalTo
則主要是對(duì)對(duì)象及屬性的賦值。也就是說,當(dāng)我們括號(hào)內(nèi)的參數(shù)為某一具體數(shù)值時(shí),需要用mas_equalTo
, 當(dāng)參數(shù)為對(duì)象或者屬性時(shí),需要用equalTo
。
關(guān)于括號(hào)內(nèi)參數(shù)數(shù)值的正負(fù)
有時(shí)候我們會(huì)發(fā)現(xiàn)括號(hào)內(nèi)的參數(shù)為負(fù)數(shù),這是因?yàn)橛?jì)算的是絕對(duì)的數(shù)值,具體是取正還是取負(fù),這個(gè)由屏幕的坐標(biāo)增長(zhǎng)方向決定。
and&with
// CODE29
- (MASConstraint *)with { return self;}
- (MASConstraint *)and { return self;}
由此可以看出,這兩個(gè)函數(shù)其實(shí)什么都沒有做,加入這個(gè)的目的,就是讓代碼看起來更加的自然。
關(guān)于父視圖的問題
當(dāng)一個(gè)控件被添加到其父視圖上后才可以進(jìn)行約束,并且值得注意的是,在iOS7中,一旦子視圖利用了它爺爺(父視圖的父視圖)或者它叔伯(父視圖的同級(jí)視圖)進(jìn)行約束,那么,程序?qū)?huì)crash。
總結(jié)
Masonry 與其它的第三方開源框架一樣選擇了使用分類的方式為 UIKit 添加一個(gè)方法 mas_makeConstraint
, 這個(gè)方法接受了一個(gè) block, 這個(gè) block 有一個(gè) MASConstraintMaker
類型的參數(shù), 這個(gè) maker 會(huì)持有一個(gè)約束的數(shù)組, 這里保存著所有將被加入到視圖中的約束.
我們通過鏈?zhǔn)降恼Z法配置 maker, 設(shè)置它的 left
right
等屬性, 比如說 make.left.equalTo(view)
, 其實(shí)這個(gè) left equalTo
還有像 with offset
之類的方法都會(huì)返回一個(gè) MASConstraint
的實(shí)例, 所以在這里才可以用類似 Ruby 中鏈?zhǔn)降恼Z法.在配置結(jié)束后, 首先會(huì)調(diào)用 maker 的 install
方法, 而這個(gè) maker 的 install
方法會(huì)遍歷其持有的約束數(shù)組, 對(duì)其中的每一個(gè)約束發(fā)送 install
消息.
在這里就會(huì)使用到在上一步中配置的屬性, 初始化 NSLayoutConstraint
的子類 MASLayoutConstraint
并添加到合適的視圖上.視圖的選擇會(huì)通過調(diào)用一個(gè)方法 mas_closestCommonSuperview:
來返回兩個(gè)視圖的最近公共父視圖.