只有 20% 的 iOS 程序員能看懂:詳解 intrinsicContentSize 及 約束優先級/content Hugging/content Compression Resistance


原文地址:http://blog.csdn.net/hard_man/article/details/50888377


在瞭解 intrinsicContentSize 之前,我們需要先了解 2 個概念:

  • AutoLayout 在做什麼
  • 約束優先級是什麼意思。

如果不瞭解這兩個概念,看 intinsic content size 沒有任何意義。 
注:由於上面這幾個概念都是針對 UIView 或其子類(UILabel,UIImageView等等)來說的。所以下文中都用 UIView 指代。

AutoLayout 在做什麼 – 一個 UIView 想要顯示在屏幕中,僅須有 2 個需要確定的元素,一是位置,二是大小。只要 2 者確定,UIView 就可以正確顯示,至於顯示的內容,則由 UIView 自己決定(drawRect)。

沒有 AutoLayout 的時候,我們需要通過 initWithFrame:(CGRect) 這種方式來指定 UIView 的位置和大小。

而使用 AutoLayout 的過程,就是通過約束來確定 UIView 的位置和大小的過程。

約束優先級 – 爲什麼約束需要優先級?因爲有的時候 2 個約束可能會有衝突。 比如:有一個 UIView 距離父 UIView 的左右距離都是 0,這是 2 個約束,此時再給此 UIView 加一個寬度約束,比如指定寬度爲 100,那麼就會產生約束衝突了。 

因爲,這兩種約束不可能同時存在,只能滿足一個,那麼滿足誰呢?默認情況下給 UIView 加的這幾個約束優先級都是 1000,屬於最高的優先級了,表示此約束必須滿足。

所以這種衝突不能被 iOS 所允許。此時就需要修改優先級了。把其中任意一個約束的優先級改爲小於 1000 的值即可。

iOS 可以通過比較兩個”相互衝突的約束”的優先級,從而忽略低優先級的某個約束,達到正確佈局的目的。

用鼠標選中寬度約束,然後在屏幕右側的菜單中,修改優先級,如下圖:

這樣就沒有約束衝突了。因爲如果一旦兩個約束衝突,系統會自動忽略優先級低的約束。

上面舉的這個例子有些極端,因爲上面兩個約束都是確定的值,而且是絕對沖突。所以如果遇到這種情況,可能選擇刪掉某個約束更爲合適。

而約束優先級更多的時候用於解決模糊約束(相對於上面的確定值約束來說)的衝突的問題。 
比如有這樣一個問題:

  • UIView1 有四個約束:距離父 UIView 左和上確定,寬和高也確定。
  • UIView2 在 UIView1 的下面,約束也有 4 個:上面距離 UIView1 確定,左面同 UIView1 對齊,同 UIView1 等高且等寬。 
    此時這兩個 UIView 應該像這樣:

這是一個很普通的應用場景,假設我希望有這樣一個效果: 我希望 UIView2 的寬度不能超過 50。當 UIView1 寬度小於 50 的時候,二者等寬;當 UIView1 寬度大於 50 的時候,UIView2 不受 UIView1 寬度的影響。 
於是我給 UIView2 加上一條約束:寬度 <=50。這時候衝突來了: 
因爲 UIView1 的寬度是定好的,而 UIView2 和 UIView1 等寬。那麼 UIView2 的寬度就是確定的。

很顯然,分爲兩種情況(根據 UIView1 的寬度不同):

  • 若 UIView1 的寬度大於 50,UIView2 的寬度也一定大於 50,這跟新加的限制寬度 <=50 的約束是衝突的。 
  • 否則不衝突。

更糟糕的是,實際情況中,UIView1 的寬度可能不是一個確定的值。它有可能會被頁面中的其他 View 所影響,可能還會在運行時產生變化,並不能保證它的實際寬度一定小於 50。所以,一旦產生約束衝突,可能就會對應用產生不確定的影響:可能顯示錯亂,也可能程序崩潰。

所以我們爲了得到正確的結果,應該這樣處理:

  1. 當 UIView1 寬度小於等於 50 的時候,約束不衝突,修改優先級與否都是一樣結果。
  2. 當 UIView1 寬度大於 50 的時候,忽略等寬約束,也就是降低等寬約束優先級。

所以我們把等寬約束的優先級修改爲 999。上面兩條都滿足,問題解決。

說到模糊約束,content Hugging/content Compression Resistance 就是 2 個 UIView 自帶的模糊約束。 
而這兩個約束存在的條件則是 UIView 必須指定了 Intrinsic Content Size。 
在瞭解這兩個模糊約束之前,必須瞭解 Intrinsic Content Size 是什麼東西。

Intrinsic Contenet Size – Intrinsic Content Size:固有大小。顧名思義,在 AutoLayout 中,它作爲 UIView 的屬性(不是語法上的屬性),意思就是說我知道自己的大小,如果你沒有爲我指定大小,我就按照這個大小來。 比如:大家都知道在使用 AutoLayout 的時候,UILabel 是不用指定尺寸大小的,只需指定位置即可,就是因爲,只要確定了文字內容,字體等信息,它自己就能計算出大小來。

UILabel,UIImageView,UIButton 等這些組件及某些包含它們的系統組件都有 Intrinsic Content Size 屬性。 
也就是說,遇到這些組件,你只需要爲其指定位置即可。大小就使用 Intrinsic Content Size 就行了。

在代碼中,上述系統控件都重寫了 UIView 中的 -(CGSize)intrinsicContentSize: 方法。 
並且在需要改變這個值的時候調用:invalidateIntrinsicContentSize 方法,通知系統這個值改變了。

所以當我們在編寫繼承自 UIView 的自定義組件時,也想要有 Intrinsic Content Size 的時候,就可以通過這種方法來輕鬆實現。

Intrinsic 衝突 – 一個 UIView 有了 Intrinsic Content Size 之後,纔可以只指定位置,而不用指定大小。並且纔可能會觸發上述兩個約束。 但是問題又來了,對於上述這種 UIView 來說,只指定位置而不指定大小,有的時候會有問題。 我們用 UILabel 來舉例吧(所有支持Intrinsic Content Size 的組件都有此問題)。 2 個 UILabel,UILabel1(文字內容:UILabel1)和 UILabel2(文字內容:UILabel2),其內容按照下面說明佈局: - 2 個 UILabel 距離上邊欄爲 50 點。 - UILabel1 與左邊欄距離爲 10,UILabel2 左面距離 UILabel1 爲 10 點。 因爲都具有 Intrinsic 屬性,所以不需要指定 size。位置應該也明確了。

現在問題來了,再給 UILabel2 加一條約束,右側距離右邊欄爲 10 點。

很明顯,如果按照約束來佈局,則沒辦法滿足 2 個 UIlabel 都使用 Intrinsic Content Size,至少某個 UILabel 的寬度大於 Intrinsic Content Size。這種情況,我們稱之爲 2 個組件之間的“ Intrinsic 衝突”。

解決“ Intrinsic 衝突”的方案有 2 種:

  1. 兩個 UIlabel 都不使用 Intrinsic Content Size。爲兩個 UIlabel 增加新的約束,來顯式指定它們的大小。如:給 2 個 UIlabel 增加寬度和高度約束或等寬等高約束等等。
  1. 可以讓其中一個 UIlabel 使用 Intrinsic Content Size,另一個 label 則自動佔用剩餘的空間。這時候就需要用到 Content Hugging 和 Content Compression Resistance 了!具體做法在下面介紹。

一句話總結“ Intrinsic 衝突”:兩個或多個可以使用 Intrinsic Content Size 的組件,因爲組件中添加的其他約束,而無法同時使用  intrinsic Content Size 了。

content Hugging/content Compression Resistance – 首先,這兩個概念都是 UIView 的屬性。 假設兩個組件產生了“ Intrinsic 衝突”: 1. Content Hugging 約束(不想變大約束)表示:如果組件的此屬性優先級比另一個組件此屬性優先級高的話,那麼這個組件就保持不變,另一個可以在需要拉伸的時候拉伸。屬性分橫向和縱向 2 個方向。 2. Content Compression Resistance 約束(不想變小約束)表示:如果組件的此屬性優先級比另一個組件此屬性優先級高的話,那麼這個組件就保持不變,另一個可以在需要壓縮的時候壓縮。屬性分橫向和縱向 2 個方向。 意思很明顯。上面 UIlabel 這個例子中,很顯然,如果某個 UILabel 使用 Intrinsic Content Size 的時候,另一個需要拉伸。 所以我們需要調整兩個 UILabel 的 Content Hugging 約束的優先級就可以啦。 在這個頁面可以調整優先級(拉到最下面)。

分別調整兩個 UILabel 的 Content Hugging 的優先級可以得到不同的結果:

Content Compression Resistance 的情況就不多說了,原理相同。

在代碼中修改UIView的這兩個優先級

    [label setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal];
    [label setContentCompressionResistancePriority: UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]


Priority是個enum:

typedef float UILayoutPriority;
static const UILayoutPriority UILayoutPriorityRequired NS_AVAILABLE_IOS(6_0) = 1000; // A required constraint.  Do not exceed this.
static const UILayoutPriority UILayoutPriorityDefaultHigh NS_AVAILABLE_IOS(6_0) = 750; // This is the priority level with which a button resists compressing its content.
static const UILayoutPriority UILayoutPriorityDefaultLow NS_AVAILABLE_IOS(6_0) = 250; // This is the priority level at which a button hugs its contents horizontally.
static const UILayoutPriority UILayoutPriorityFittingSizeLevel NS_AVAILABLE_IOS(6_0) = 50; // When you send -[UIView systemLayoutSizeFittingSize:], the size fitting most closely to the target size (the argument) is computed.  UILayoutPriorityFittingSizeLevel is the priority level with which the view wants to conform to the target size in that computation.  It's quite low.  It is generally not appropriate to make a constraint at exactly this priority.  You want to be higher or lower.

Axis表示橫向及縱向:

typedef NS_ENUM(NSInteger, UILayoutConstraintAxis) {
    UILayoutConstraintAxisHorizontal = 0,
    UILayoutConstraintAxisVertical = 1
};

創建自定義具有 Intrinsic Content Size 功能的組件

另外一篇文章更爲詳細的文章請查閱 實現具有 intrinsic content size 功能的自定義視圖類

下面也給出一個比較簡單的純代碼使用的自定義視圖代碼:

//IntrinsicView.h
#import <UIKit/UIKit.h>

@interface IntrinsicView : UIView
@property (nonatomic) CGSize extendSize;
@end

//IntrinsicView.m
#import "IntrinsicView.h"

static bool closeIntrinsic = false;//測試關閉Intrinsic的影響

@implementation IntrinsicView

- (instancetype)init
{
    self = [super init];
    if (self) {
        //不兼容舊版Autoreizingmask,只使用AutoLayout
        //如果爲YES,在AutoLayout中則會自動將view的frame和bounds屬性轉換爲約束。
        self.translatesAutoresizingMaskIntoConstraints = NO;
    }
    return self;
}

//當用戶設置extendSize時,提示系統IntrinsicContentSize變化了。
-(void)setExtendSize:(CGSize)extendSize{
    _extendSize = extendSize;
    //如果不加這句話,在view顯示之後(比如延時幾秒),再設置extendSize不會有效果。
    //本例中也就是testInvalidateIntrinsic的方法不會產生預期效果。
    [self invalidateIntrinsicContentSize];
}

//通過覆蓋intrinsicContentSize函數修改View的Intrinsic的大小
-(CGSize)intrinsicContentSize{
    if (closeIntrinsic) {
        return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
    } else {
        return CGSizeMake(_extendSize.width, _extendSize.height);
    }
}
@end

//測試代碼
#import "ViewController.h"
#import "newViewCtlViewController.h"
#import "IntrinsicView.h"

@interface ViewController ()
@end

@implementation ViewController

-(void)viewDidLoad{
    [super viewDidLoad];
    [self testIntrinsicView];
}+
-(void) testIntrinsicView{
    IntrinsicView *intrinsicView1 = [[IntrinsicView alloc] init];
    intrinsicView1.extendSize = CGSizeMake(100, 100);
    intrinsicView1.backgroundColor = [UIColor greenColor];
    [self.view addSubview:intrinsicView1];
    [self.view addConstraints:@[
                                //距離superview上方100點
                                  [NSLayoutConstraint constraintWithItem:intrinsicView1 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:100],
                                  //距離superview左面10點
                                  [NSLayoutConstraint constraintWithItem:intrinsicView1 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:10],
    ]];


    IntrinsicView *intrinsicView2 = [[IntrinsicView alloc] init];
    intrinsicView2.extendSize = CGSizeMake(100, 30);
    intrinsicView2.backgroundColor = [UIColor redColor];
    [self.view addSubview:intrinsicView2];
    [self.view addConstraints:@[
                                //距離superview上方220點
                                [NSLayoutConstraint constraintWithItem:intrinsicView2 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:220],
                                //距離superview左面10點
                                [NSLayoutConstraint constraintWithItem:intrinsicView2 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:10],
                                ]];

    [self performSelector:@selector(testInvalidateIntrinsic:) withObject:intrinsicView2 afterDelay:2];
}

-(void) testInvalidateIntrinsic:(IntrinsicView *)view{
    view.extendSize = CGSizeMake(100, 80);
}
@end
代碼效果如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章