【OC梳理】自動佈局

gzhongcheng發表於2018-12-17

自動佈局基礎篇

關於自動佈局的基本使用,參考網上的文章即可,如: iOS開發-自動佈局篇:史上最牛的自動佈局教學!

自動佈局進階篇

抗拉伸與抗壓縮

相信許多比較少使用自動佈局的同學對下面的引數都感覺比較頭疼:

【OC梳理】自動佈局

其實不難,請往下看: #####Content Hugging Priority : 抗拉伸優先順序 最常見的情況是兩個label並排放置,並設定水平間距:

【OC梳理】自動佈局

報錯了?莫慌,當兩個label的文字長度加上水平間距不足以填滿父檢視時,需要設定抗拉伸優先順序。 解決辦法:假設我們不希望Label1被拉伸,則將其Hugging Priority值調大(預設為251)

【OC梳理】自動佈局

調整後,修改label1的文字,其長度隨之變化:

【OC梳理】自動佈局

【OC梳理】自動佈局

【OC梳理】自動佈局

然後我們在調整的過程中會發現:

【OC梳理】自動佈局

又報錯了? 接著往下看: #####Content Compression Resistance Priority : 抗壓縮優先順序 當兩個label的文字長度加上水平間距超出了父檢視寬度時,需要設定抗壓縮優先順序。 解決辦法:假設我們希望Label1的內容儘可能地展示完全,則將其Resistance Priority值調大(預設為750)

【OC梳理】自動佈局

調整後:

【OC梳理】自動佈局

可以看到,Label2直接被壓沒了。 我們把Label2的Resistance Priority值調大:

【OC梳理】自動佈局

調整後:

【OC梳理】自動佈局

經過上面的過程後,對於如何使用自動佈局做出下圖的效果,是不是就心裡有數了呢:

【OC梳理】自動佈局

PS: 抗壓縮、抗拉伸優先順序同樣適用於與父檢視的約束優先順序(預設值1000) 熟悉了xib中的優先順序設定後,在Masonry中對應優先順序的思路相同,使用方法- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis;進行對應設定(系統自帶方法,不是Masonry加的)。

內容尺寸(Intrinsic Content Size)

某些用來展現內容的使用者控制元件,例如文字控制元件UILabel、按鈕UIButton、圖片檢視UIImageView等,它們具有自身內容尺寸(對於UIImageView,其自身內容尺寸就是圖片(1倍圖)的尺寸)。

如何設定自定義控制元件的內容尺寸?

我們可以通過重寫- (CGSize)intrinsicContentSize方法來指定內容尺寸。 先來看看UIView預設的返回值:

- (CGSize)intrinsicContentSize{
    return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
}
複製程式碼

這個方法的作用是當使用已有約束不能夠計算出內容寬度/高度時,自動為檢視新增寬度/高度約束,其值為返回值中對應的寬/高。 因此我們可以在這個方法中返回計算好的內容大小。

我們在xib中也可以看到下面的引數(需要注意的是,在這裡設定的size僅僅只用於xib中展示效果,實際執行時並沒有什麼卵用):

【OC梳理】自動佈局

知道了intrinsicContentSize的用法後,我們考慮一下下面的需求:

寫一個自定義的View,滿足下面的功能:

  • 如果未使用約束,直接顯示frame的大小即可;
  • 如果新增了能夠定位左上角點座標的約束,則使用預設的內容大小。
  • 如果新增了能夠定位左上角點座標以及能夠計算出寬/高其中一個數值時,使用寬/高的另一個預設值。
  • 如果新增了能夠計算出全部frame值的約束,則不使用預設內容大小。

整體效果類似UILabel,但是這裡簡化成固定的預設尺寸。

【OC梳理】自動佈局

這裡給出一個簡單的demo僅供參考:

@interface DemoView()

@property (nonatomic,assign) CGSize defaultSize;

@end

@implementation DemoView {
    BOOL _widthConstraintAdded;
    BOOL _heightConstraintAdded;
    float _widthConstrant;
    float _heightConstrant;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder{
    self = [super initWithCoder:aDecoder];
    if (self) {
        _widthConstraintAdded = _heightConstraintAdded = NO;
        _widthConstrant = self.defaultSize.width;
        _heightConstrant = self.defaultSize.height;
    }
    return self;
}

- (void)layoutSubviews{
    [super layoutSubviews];
    // 是否使用了自動佈局
    if (self.constraints.count > 0) {
        // 判斷是否新增了可以計算出寬度/高度的約束
        if (self.frame.size.width != _widthConstrant) {
            _widthConstraintAdded = YES;
        }
        if (self.frame.size.height != _heightConstrant) {
            _heightConstraintAdded = YES;
        }
        // 計算缺少的寬/高
        // 計算時可以使用self.frame.size,這個size是自動佈局調整後的size了
        if (!_widthConstraintAdded) {
            // 計算寬度的程式碼...
            _widthConstrant = 10;
        }
        if (!_heightConstraintAdded) {
            // 計算高度的程式碼...
            _heightConstrant = 10;
        }
        // 新增約束
        if (!CGSizeEqualToSize(self.frame.size, [self intrinsicContentSize])) {
            [self invalidateIntrinsicContentSize];
        }
    }
}

- (CGSize)intrinsicContentSize{
    return CGSizeMake(_widthConstrant, _heightConstrant);
}

- (CGSize)defaultSize{
    if (CGSizeEqualToSize(_defaultSize, CGSizeZero)) {
        // 計算預設內容寬高的程式碼
        float width = 100;
        float height = 20;
        _defaultSize = CGSizeMake(width, height);
    }
    return _defaultSize;
}
@end
複製程式碼

ScrollView的自動內容大小

我們看下面的佈局:

【OC梳理】自動佈局

當我們需要Label2可以顯示多行資料時,僅僅設定右邊距是不夠的:

【OC梳理】自動佈局

我們可以讓Label2的寬度等於Scrollview的寬度減去左右邊距之和:

【OC梳理】自動佈局

看看效果:

【OC梳理】自動佈局

效果出來了,但是僅僅這樣我們發現不能上下滾動,因為沒有設定底部間距,scrollview無法計算出內容高度,需要給最下面的view(Label2)新增底部間距:

【OC梳理】自動佈局

看看效果:

【OC梳理】自動佈局

下面考慮更進一步的需求:Label1也要可以顯示多行時的情況。 我們把Label1右邊與Label2對齊,然後改一下內容文字:

【OC梳理】自動佈局

OK大功告成。

PS:如果使用Tablview時用到Cell的自動高度(不管是用系統自帶還是UITableView+FDTemplateLayoutCell),也都需要設定好約束,讓它能夠計算出豎直方向的內容高度(水平方向tableview不需要,collectionView需要),具體過程和上面類似。

自定義View與自動佈局

我們看一個樣式:

【OC梳理】自動佈局

要實現這一樣式的方式有很多種,這裡我們考慮用UILabel的子類直接實現的情況(儘可能少的View):

【OC梳理】自動佈局

UIView中有這一個類方法:+ (Class)layerClass;,該方法返回的就是view.layer的型別,因此我們可以使用自定義的Layer替換掉預設的Layer,然後裁剪出對應的形狀(原理比較簡單,直接上程式碼了):

@interface DemoLabelLayer : CAShapeLayer

@end

@implementation DemoLabelLayer

- (void)layoutSublayers{
    [super layoutSublayers];
    [self setBackgroundPath];
}

- (void)setBackgroundPath{
    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.fillColor = [UIColor whiteColor].CGColor;
    shapeLayer.fillRule = kCAFillRuleEvenOdd;
    shapeLayer.path = [self backgroundPathFrame:self.bounds].CGPath;
    self.mask = shapeLayer;
}

- (UIBezierPath *)backgroundPathFrame:(CGRect)frame{
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:frame byRoundingCorners:UIRectCornerTopRight|UIRectCornerBottomRight cornerRadii:CGSizeMake(self.cornerRadius, self.cornerRadius)];
    return path;
}

@end

@interface DemoView()

@end

@implementation DemoView

- (void)awakeFromNib{
    [super awakeFromNib];
}

- (void)layoutSubviews{
    [super layoutSubviews];
    DemoLabelLayer *layer = (DemoLabelLayer *)self.layer;
    layer.backgroundColor = self.backgroundColor.CGColor;
    layer.cornerRadius = self.frame.size.height/2.f;
}

+ (Class)layerClass{
    return [DemoLabelLayer class];
}

@end
複製程式碼

自動佈局的動畫

對於需要進行frame動態調整的介面,許多小夥伴都不知道該如何使用自動佈局進行動畫而放棄,其實不難:

如果使用xib或者程式碼建立,最簡單的方式就是將約束設定成對應屬性,直接修改約束的constant即可。

如果使用Masonry建立佈局,那麼只需要使用下面的程式碼就可以使用動畫效果了:

// 如果其約束還沒有生成的時候需要動畫的話,就請先強制重新整理後才寫動畫,否則所有沒生成的約束會直接跑動畫
[view.superview layoutIfNeeded];
[UIView animateWithDuration:3 animations:^{
  [view mas_updateConstraints:^(MASConstraintMaker *make) {
    make.height.mas_equalTo(123);
  }];
}];
// 強制繪製
[view.superview layoutIfNeeded];
複製程式碼

記得動畫前後需要重新整理。

相關文章