UIView的Margins

寫在前面

之前使用Storyboard拖拽約束時,可以看到比較的view有margin選項,來支持相對某view的margin進行布局。

storyboard-constraints-margin.png

那么在代碼中如何體現,就需要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);

效果:

layoutMargin.png

可以看到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;

效果:

preservesSuperviewLayoutMargins-1.png

其中orangeView.preservesSuperviewLayoutMargins = YES;就設置了orangeView保持父視圖的layoutMargins,下面沒有保持父視圖的邊距是因為,orangeView不在父視圖的bottomMargin內。

如果把orangeView.preservesSuperviewLayoutMargins = YES;這句代碼去掉或者設置為NO,效果如下:

preservesSuperviewLayoutMargins-2.png

這里忽略上邊距(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,效果如下:

preservesSuperviewLayoutMargins-3.png

發現效果和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;
}

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容