寫在前面
之前使用Storyboard拖拽約束時佛嬉,可以看到比較的view有margin選項,來支持相對某view的margin進行布局。
那么在代碼中如何體現(xiàn),就需要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占據(jù)全屏,它的子視圖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調(diào)整為NSDirectionalEdgeInsets結構的leading和trailing官硝。這一調(diào)整主要是為了Right To Left(RTL)語言下可以進行自動適配矗蕊。
preservesSuperviewLayoutMargins
當這個屬性的值為YES的時候,一個視圖布局內(nèi)容時其父視圖的margins也會被考慮在內(nèi)氢架。默認是NO傻咖。
場景二:blueView占據(jù)全屏,它的子視圖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內(nèi)害淤。
如果把orangeView.preservesSuperviewLayoutMargins = YES;
這句代碼去掉或者設置為NO,效果如下:
這里忽略上邊距(iOS11 Safe Area引起拓售,后面講)窥摄,發(fā)現(xiàn)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鸽凶,效果如下:
發(fā)現(xiàn)效果和orangeView沒有自己的Margin時一樣币砂,這是因為視圖本身的Margin和從父視圖保持來的Margin是重合的。也就是說preservesSuperviewLayoutMargins=YES時玻侥,真正layoutMargins的值是决摧,“手動設置layoutMargins值”與”在父視圖Margin范圍內(nèi)區(qū)域“的最大值,偽代碼表示如下:
layoutMargins = Max(self.layoutMargins, Combine(self.superview.layoutMargins, self.frame));
iOS11的insetsLayoutMarginsFromSafeArea
從iOS 7以來,我們在整個操作系統(tǒng)中都有這些半透明的bars蜜徽,蘋果鼓勵我們通過這些bars繪制內(nèi)容祝懂,我們是通過viewController的edgesForExtendedLayout屬性來做這些的。
iOS11的Safe Area的出現(xiàn)拘鞋,很快將iOS7出現(xiàn)的topLayoutGuide砚蓬、bottomLayoutGuide廢棄。Safe Area定義了view中可視區(qū)域的部分盆色,保證不被系統(tǒng)的狀態(tài)欄灰蛙、或父視圖提供的view如導航欄覆蓋。
UIView的safeAreaInsets屬性反映了一個view距離該view的安全區(qū)域的邊距隔躲。對于一個Controller的根視圖而言摩梧,SafeAreaInsets值包括了被statusbar和其他可視的bars覆蓋的區(qū)域和其他通過additionalSafeAreaInsets自定義的insets值。對于view層次中得其他view宣旱,SafeAreaInsets值反映了view被覆蓋的部分仅父。如果一個view全部在它父視圖的安全區(qū)域內(nèi),則SafeAreaInsets值為(0,0,0,0)浑吟。
說了這么多終于到insetsLayoutMarginsFromSafeArea了笙纤,從名字就可以看出來它和layoutMargins和safeAreaInsets有一定聯(lián)系。我們通過下面的場景來證明一下:
我們想要看一個view在真正布局時的safeAreaInsets值和layoutMargins值组力,這樣寫一個UIView的子類TestView省容,重寫父類的layoutSubviews方法,打印出這兩個值燎字,并把上面的三個視圖改成TestView的實例:
- (void)layoutSubviews
{
[super layoutSubviews];
NSLog(@"%@", self);
NSLog(@"safeAreaInsets : %@", [NSValue valueWithUIEdgeInsets:self.safeAreaInsets]); //safeAreaInsets是iOS11才有的屬性腥椒,注意使用時判斷系統(tǒng)版本
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}
發(fā)現(xiàn)safeAreaInsets不再累加到layoutMargins上了,所以insetsLayoutMarginsFromSafeArea屬性也很簡單榨为,就是控制safeAreaInsets是否加到layoutMargins上惨好。
另外,從打印結果看随闺,safeAreaInsets的值就是status bar的高度日川,也說明了我們之前的效果上面的邊距要多出一點的原因。
總結
本文主要講了UIView關于Margin的以下屬性:
- layoutMargins: iOS8開始引入矩乐,用于指定視圖和它的子視圖之間的邊距龄句。
- directionalLayoutMargins:iOS11開始引入回论,可以根據(jù)語言的方向進行前后布局,與layoutMargins相比分歇,能更好的適配RTL語言傀蓉。
- preservesSuperviewLayoutMargins:iOS8開始引入,當這個屬性的值為YES的時候职抡,一個視圖布局內(nèi)容時其父視圖的margins也會被考慮在內(nèi)葬燎。默認是NO。
- insetsLayoutMarginsFromSafeArea:iOS11開始引入缚甩,控制safeAreaInsets是否加到layoutMargins上谱净。默認YES。
他們之間的關系:
第一步:一個視圖“真正的layoutMargins”是否受父視圖的layoutMargins影響擅威,取決于preservesSuperviewLayoutMargins值壕探,如果NO,則不考慮父視圖layoutMargins郊丛,如果YES李请,受影響值為視圖在父視圖的margin的區(qū)域,然后取“設置的layoutMargins”與“在父視圖的margin區(qū)域”的最大值宾袜。
第二步:再判斷視圖是否受Safe Area影響捻艳,判斷insetsLayoutMarginsFromSafeArea值,如果NO庆猫,直接使用,如果YES绅络,則將從上面得到的layoutMargins加上safeAreaInsets(注意這里是加上月培,前面與父視圖margin的影響區(qū)域是取最大值),得到最終真正的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;
}