寫在前面
之前使用Storyboard拖拽約束時,可以看到比較的view有margin選項,來支持相對某view的margin進行布局。
那么在代碼中如何體現,就需要UIView的以下API:
- layoutMargins
- directionalLayoutMargins
- preservesSuperviewLayoutMargins
iOS11引入了Safe Area的概念,相應對UIView的Margin也增加以下API:
- insetsLayoutMarginsFromSafeArea
layoutMargins
這個屬性用于指定視圖和它的子視圖之間的邊距。和preservesSuperviewLayoutMargins一起在iOS8開始引入。
AutoLayout中NSLayoutAttribute的枚舉值也有相應的更新:
NSLayoutAttributeLeftMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeRightMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeTopMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeBottomMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeLeadingMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeTrailingMargin NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeCenterXWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
NSLayoutAttributeCenterYWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),
在VFL(Visual Format Language)語法中也有相應的引入,比如“|-[subview]-|”,設置Margin約束。
子視圖采用上面的約束與父視圖建立約束時,父視圖的layoutMarigin才會生效。
場景一:blueView占據全屏,它的子視圖orangeView相對它的margin布局
UIView *blueView = [[UIView alloc] init];
blueView.backgroundColor = [UIColor blueColor];
blueView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:blueView];
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
UIView *orangeView = [[UIView alloc] init];
orangeView.backgroundColor = [UIColor orangeColor];
orangeView.translatesAutoresizingMaskIntoConstraints = NO;
[blueView addSubview:orangeView];
[orangeView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(blueView.mas_topMargin);
make.bottom.mas_equalTo(blueView.mas_bottomMargin);
make.left.mas_equalTo(blueView.mas_leftMargin);
make.right.mas_equalTo(blueView.mas_rightMargin);
}];
blueView.layoutMargins = UIEdgeInsetsMake(50, 50, 50, 50);
效果:
可以看到orangeView相對上下左右有個比較大的margin。(這里肉左右下的margin是50,但是肉眼看距離上面的margin似乎要比左右下的margin大,這是因為iOS11的Safe Area,下面會講到)
除了layoutMargins,iOS11還增加了一個新屬性directionalLayoutMargins,這個屬性的類型不是UIEdgeInsets,而是NSDirectionalEdgeInsets,定義如下:
typedef struct UIEdgeInsets {
CGFloat top, left, bottom, right;
} UIEdgeInsets;
typedef struct NSDirectionalEdgeInsets {
CGFloat top, leading, bottom, trailing;
} NSDirectionalEdgeInsets API_AVAILABLE(ios(11.0),tvos(11.0),watchos(4.0));
從結構上看主要是將UIEdgeInsets結構的left和right調整為NSDirectionalEdgeInsets結構的leading和trailing。這一調整主要是為了Right To Left(RTL)語言下可以進行自動適配。
preservesSuperviewLayoutMargins
當這個屬性的值為YES的時候,一個視圖布局內容時其父視圖的margins也會被考慮在內。默認是NO。
場景二:blueView占據全屏,它的子視圖orangeView相對blueView的邊距布局,orangeView的子視圖redView相對orangeView的margin布局。
UIView *blueView = [[UIView alloc] init];
blueView.backgroundColor = [UIColor blueColor];
blueView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:blueView];
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.mas_equalTo(self.view);
}];
UIView *orangeView = [[UIView alloc] init];
orangeView.backgroundColor = [UIColor orangeColor];
orangeView.translatesAutoresizingMaskIntoConstraints = NO;
[blueView addSubview:orangeView];
[orangeView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.top.mas_equalTo(blueView);
make.width.mas_equalTo(blueView);
make.height.mas_equalTo(blueView).multipliedBy(0.5);
}];
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
[orangeView addSubview:redView];
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(orangeView.mas_topMargin);
make.bottom.mas_equalTo(orangeView.mas_bottomMargin);
make.left.mas_equalTo(orangeView.mas_leftMargin);
make.right.mas_equalTo(orangeView.mas_rightMargin);
}];
blueView.layoutMargins = UIEdgeInsetsMake(50, 50, 50, 50);
orangeView.preservesSuperviewLayoutMargins = YES;
orangeView.layoutMargins = UIEdgeInsetsZero;
效果:
其中orangeView.preservesSuperviewLayoutMargins = YES;
就設置了orangeView保持父視圖的layoutMargins,下面沒有保持父視圖的邊距是因為,orangeView不在父視圖的bottomMargin內。
如果把orangeView.preservesSuperviewLayoutMargins = YES;
這句代碼去掉或者設置為NO,效果如下:
這里忽略上邊距(iOS11 Safe Area引起,后面講),發現redView的左右邊距不再相對爺爺視圖,也就是blueView的Margin對齊了,這是因為視圖orangeView沒有保持blueView的layoutMargin。
接下來我們把代碼修改為:
blueView.layoutMargins = UIEdgeInsetsMake(50, 50, 50, 50);
orangeView.preservesSuperviewLayoutMargins = YES;
orangeView.layoutMargins = UIEdgeInsetsMake(0, 50, 0, 0);
也就是我們想要orangeView保持父視圖的Margin的基礎上,增加自己的左Margin,效果如下:
發現效果和orangeView沒有自己的Margin時一樣,這是因為視圖本身的Margin和從父視圖保持來的Margin是重合的。也就是說preservesSuperviewLayoutMargins=YES時,真正layoutMargins的值是,“手動設置layoutMargins值”與”在父視圖Margin范圍內區域“的最大值,偽代碼表示如下:
layoutMargins = Max(self.layoutMargins, Combine(self.superview.layoutMargins, self.frame));
iOS11的insetsLayoutMarginsFromSafeArea
從iOS 7以來,我們在整個操作系統中都有這些半透明的bars,蘋果鼓勵我們通過這些bars繪制內容,我們是通過viewController的edgesForExtendedLayout屬性來做這些的。
iOS11的Safe Area的出現,很快將iOS7出現的topLayoutGuide、bottomLayoutGuide廢棄。Safe Area定義了view中可視區域的部分,保證不被系統的狀態欄、或父視圖提供的view如導航欄覆蓋。
UIView的safeAreaInsets屬性反映了一個view距離該view的安全區域的邊距。對于一個Controller的根視圖而言,SafeAreaInsets值包括了被statusbar和其他可視的bars覆蓋的區域和其他通過additionalSafeAreaInsets自定義的insets值。對于view層次中得其他view,SafeAreaInsets值反映了view被覆蓋的部分。如果一個view全部在它父視圖的安全區域內,則SafeAreaInsets值為(0,0,0,0)。
說了這么多終于到insetsLayoutMarginsFromSafeArea了,從名字就可以看出來它和layoutMargins和safeAreaInsets有一定聯系。我們通過下面的場景來證明一下:
我們想要看一個view在真正布局時的safeAreaInsets值和layoutMargins值,這樣寫一個UIView的子類TestView,重寫父類的layoutSubviews方法,打印出這兩個值,并把上面的三個視圖改成TestView的實例:
- (void)layoutSubviews
{
[super layoutSubviews];
NSLog(@"%@", self);
NSLog(@"safeAreaInsets : %@", [NSValue valueWithUIEdgeInsets:self.safeAreaInsets]); //safeAreaInsets是iOS11才有的屬性,注意使用時判斷系統版本
NSLog(@"layoutMargins : %@", [NSValue valueWithUIEdgeInsets:self.layoutMargins]);
}
將上面三個視圖的layoutMargin設置為:
blueView.layoutMargins = UIEdgeInsetsMake(50, 50, 50, 50);
orangeView.layoutMargins = UIEdgeInsetsMake(0, 50, 0, 0);
控制臺打印:
//blueView
safeAreaInsets : UIEdgeInsets: {20, 0, 0, 0}
layoutMargins : UIEdgeInsets: {70, 50, 50, 50}
//orangeView
safeAreaInsets : UIEdgeInsets: {20, 0, 0, 0}
layoutMargins : UIEdgeInsets: {20, 50, 0, 0}
//redView
safeAreaInsets : UIEdgeInsets: {0, 0, 0, 0}
layoutMargins : UIEdgeInsets: {8, 8, 8, 8}
打印的layoutMargin和設置的layoutMargin值不一樣,視圖真正顯示時的layoutMargin其實是設置的layoutMargin和safeAreaInsets的累加。那么跟insetsLayoutMarginsFromSafeArea屬性有什么關系呢,這個值默認是YES,我們把它設置為NO:
blueView.layoutMargins = UIEdgeInsetsMake(50, 50, 50, 50);
orangeView.layoutMargins = UIEdgeInsetsMake(0, 50, 0, 0);
orangeView.insetsLayoutMarginsFromSafeArea = NO;
控制臺打印:
//orangeView
safeAreaInsets : UIEdgeInsets: {20, 0, 0, 0}
layoutMargins : UIEdgeInsets: {0, 50, 0, 0}
發現safeAreaInsets不再累加到layoutMargins上了,所以insetsLayoutMarginsFromSafeArea屬性也很簡單,就是控制safeAreaInsets是否加到layoutMargins上。
另外,從打印結果看,safeAreaInsets的值就是status bar的高度,也說明了我們之前的效果上面的邊距要多出一點的原因。
總結
本文主要講了UIView關于Margin的以下屬性:
- layoutMargins: iOS8開始引入,用于指定視圖和它的子視圖之間的邊距。
- directionalLayoutMargins:iOS11開始引入,可以根據語言的方向進行前后布局,與layoutMargins相比,能更好的適配RTL語言。
- preservesSuperviewLayoutMargins:iOS8開始引入,當這個屬性的值為YES的時候,一個視圖布局內容時其父視圖的margins也會被考慮在內。默認是NO。
- insetsLayoutMarginsFromSafeArea:iOS11開始引入,控制safeAreaInsets是否加到layoutMargins上。默認YES。
他們之間的關系:
第一步:一個視圖“真正的layoutMargins”是否受父視圖的layoutMargins影響,取決于preservesSuperviewLayoutMargins值,如果NO,則不考慮父視圖layoutMargins,如果YES,受影響值為視圖在父視圖的margin的區域,然后取“設置的layoutMargins”與“在父視圖的margin區域”的最大值。
第二步:再判斷視圖是否受Safe Area影響,判斷insetsLayoutMarginsFromSafeArea值,如果NO,直接使用,如果YES,則將從上面得到的layoutMargins加上safeAreaInsets(注意這里是加上,前面與父視圖margin的影響區域是取最大值),得到最終真正的layoutMargins。
偽代碼表示如下:
- (UIEdgeInsets)getRealLayoutMargins {
UIEdgeInsets layoutMargins = UIEdgeInsetsMake(8, 8, 8, 8); //默認是8
if (self.preservesSuperviewLayoutMargins) {
layoutMargins = Max(settingsLayoutMargins, Combine(self.superview.layoutMargins, self.frame));
}
if (self.insetsLayoutMarginsFromSafeArea) {
layoutMargins = Add(layoutMargins, self.safeAreaInsets);
}
return layoutMargins;
}