iOS有三種基本的界面布局的方法,分別是手寫UI,xib和storyboard。手寫UI是最早進行UI界面布局的方法,優點是靈活自由,缺點是需要寫大段的代碼進行布局。xib也是比較早出現的UI布局的方式,優點是不需要手寫代碼,但是每個界面對應一個xib,管理起來復雜。而storyboard則是在iOS5以后出現的,是蘋果官方主推的一個代替xib的策略,不僅能將xib匯總統一管理,還可以描述各種場景之間的過渡,缺點是多人協作開發時容易產生沖突。
下面主要介紹的是手寫頁面布局。
一、AutoresizingMasks
可以使用 AutoresizingMasks 進行頁面布局,在 UIView 中有一個autoresizingMask的屬性,它對應的是一個枚舉的值,屬性的意思就是自動調整子控件與父控件中間的位置,寬高。默認值是UIViewAutoresizingNone,控件不會隨父視圖的改變而改變。
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0, // 自動調整view與父視圖左邊距,以保證右邊距不變
UIViewAutoresizingFlexibleWidth = 1 << 1, // 自動調整view的寬度,保證左邊距和右邊距不變
UIViewAutoresizingFlexibleRightMargin = 1 << 2, // 自動調整view與父視圖右邊距,以保證左邊距不變
UIViewAutoresizingFlexibleTopMargin = 1 << 3, // 自動調整view與父視圖上邊距,以保證下邊距不變
UIViewAutoresizingFlexibleHeight = 1 << 4, // 自動調整view的高度,以保證上邊距和下邊距不變
UIViewAutoresizingFlexibleBottomMargin = 1 << 5 // 自動調整view與父視圖的下邊距,以保證上邊距不變
}
AutoresizingMasks是對未來變化的一種預期,系統會生成frame的布局,當遇到需要使用到多個值的場景時,支持使用|操作符。
例如,需要設置播放器浮層隨播放器大小變化:
UIView *overlay = [[UIView alloc] init];
overlay.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self addSubview:overlay];
Autoresizing需要注意的是,storyboard中設置的約束和手寫代碼中設置的約束是相反的。storyboard 圖形頁面里點的右邊的線和下邊的線的意思是“固定”。
二、Frame
frame指的是當前視圖在其父視圖中的位置和大小。
在初始化 view 的時候,可以設置 view 的frame
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(10, 20, 30, 40)];
初始化一個距離父視圖左邊距10,上邊距20,寬30,高40的視圖。也可以修改聲明View的位置以及大小。
view.frame = CGRectMake(20, 10, 40, 30);
設置和修改視圖的 frame 可以完成對界面的布局。
2.1 bounds
提到 frame 不得不提 bounds,bounds指的是前視圖在其自身坐標系統中的位置和大小。可以看到兩者的區別在于坐標系不同。
2.2 layoutSubviews
需要重新布局視圖可以使用 layoutSubviews
1)可以在view里重寫layoutSubviews
2)可以在view controller里使用
viewWillLayoutSubviews 在autoresizingMasks前調用
viewDidLayoutSubviews 在autoresizingMasks后調用,肯定會覆蓋autoresizingMasks的結果
layoutSubviews可能會在不需要調用的時候調用,如果layoutSubviews的比較復雜,可能會卡頓
三、自動布局AutoLayout
前面講到的 frame 主要用于視圖的絕對位置,但是 iOS 設備有多個尺寸,如何對不同尺寸進行適應,蘋果的解決方案是使用 AutoLayout。
如果是從代碼層面開始使用 Autolayout,需要對使用的 View 的translatesAutoresizingMaskIntoConstraints 的屬性設置為NO。即可開始通過代碼添加Constraint,否則View還是會按照以往的autoresizingMask進行計算。而在 Interface Builder 中勾選了Use Auto layout,translatesAutoresizingMaskIntoConstraints 屬性都會被默認設置NO。
3.1 約束
自動布局里最重要的組成部分就是約束。分別可以設置視圖相對于另一個視圖的 leading、trailing、top、bottom、CenterX、CenterY 等關系。根據這些約束來確定視圖的相對位置。
視圖的約束之間的關系為線型關系。例如,視圖Y 相對于 視圖X 的位置可以表示為一個線性變換,即
Y = kX + b
即 Y 是 X 某個方向坐標或大小的 k 倍并偏移 b。k 和 b 的大小可以是0。如果 k = 1, b = 0, 則表示 Y 和 X 分別表示視圖的寬,則等式表示 Y 和 X 的寬度相等。
AutoLayout 的核心是:Every view requires at least two constraints along each axis to set position and size. 即在每個坐標軸上至少需要2個約束來確定視圖位置。
3.2 Ambiguous Layout
在開發過程中,你可以通過調用hasAmbiguousLayout 來測試你的view約束是否足夠的。函數會返回布爾值。如果有一個不同的frame就會返回yes,如果view的約束完全指定了就會返回no。
一個設定了完全約束的view的子view也可能存在ambiguous layout,需要為每一個view單獨測試layout是否存在ambiguous layout。
3.3 Intrinsic Content Size
使用autolayout時,view的content扮演著非常重要的角色。每個view的intrinsicContentSize描述了不會剪切的顯示完整view content的最小空間。例如一個image view,content size根據image顯示的size設置。一個大的image需要一個大的固有的content size。image的大小提供給了view。
對于button,固有的content size根據他的title而有不同。隨著title增長或者縮短,button的固有的content size也會調節來做適應,可以根據你自定義的font size和title text而有變化。
3.4 Compression Resistance and Content Hugging
3.4.1 compression resistance
壓縮阻力表示一個視圖的抗壓縮性。一個有高compression resistance的視圖會防止被壓縮。也不會允許content被裁剪,而會嘗試保存他的最小固有content size。
autolayout經常遇到兩個沖突的請求。當只有一個請求會成功時,他就會滿足高優先級的那個。可以分別設置水平和垂直方向的Compression Resistance。value從1(最低)到1,000(請求的優先級)不等。默認的是750。
[button setContentCompressionResistancePriority:500 forAxis:UILayoutConstraintAxisHorizontal];
3.4.2 content hugging
抗拉屬性表示view防止被拉伸的屬性,和壓縮阻力類似。默認值為250。
[button setContentHuggingPriority:501 forAxis:UILayoutConstraintAxisHorizontal];
3.5 VFL
Visual Format Language,即“可視化格式語言”。直接手寫約束很復雜,使用VFL相對簡單很多,但比較難進行調試。
[self.view addConstraints: [NSLayoutConstraint
constraintsWithVisualFormat:@"V:[view1]-8-[view2]"
options:NSLayoutFormatAlignAllLeading
metrics:nil
views:NSDictionaryOfVariableBindings(view1, view2)]];
3.6 Masonry
VFL的寫法也相當復雜,可以使用第三方框架 Masonry,Masonry 是一個輕量級的布局框架,Masonry 源碼:https://github.com/Masonry/Masonry
例如,設置view1相對父View的每個邊距離為padding:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];
需要注意的是,在結構一樣的情況下用mas_updateConstraints,會更新當前的約束,但是如果要覆蓋緣由約束重新添加,則需要使用方法用mas_remakeConstraints
constraint加到兩個view的公共父view上,因此有一個奇怪的現象是一個view不持有自己的約束,而被其他view持有。在 Masonry 中,實際添加constraint的不一定是約束的持有者
四、更新布局方法
設置好約束以后,布局是如何更新的呢?
Constraints
- (void)updateConstraintsIfNeeded // 立即重新計算約束,如果在這之前addConstraints,就可以更新約束
- (void)setNeedsUpdateConstraints // 立即返回,標記說需要改變約束值,在當前update cycle結束后更新之前所有標記過要改變的約束,調用updateConstraints方法
Layout
- (void)layoutIfNeeded // 立即更新布局,重新計算約束,如果在這之前addConstraints就會立即反應在頁面上
- (void)setNeedsLayout // 同Constraints,不過是更新布局
- (void)layoutSubviews // 布局當前頁面的子頁面
Draw
- (void)setNeedsDisplay // 同Constraints,不過是重新渲染
4.1 Constraints,Layout,Draw調用順序
一個頁面更新的順序一般為
調用約束計算出frame(Constraints)→ 根據計算出的frame重新布局(Layout) → 根據重新布局的結果進行圖像渲染(Draw)
layout的改變會導致重新計算Constraints
layoutIfNeeded會調用updateConstraintsIfNeeded
Constraints的計算順序是低到上 (從subview到superview)
layout的更新順序是從頂到下(從superview到subview)
4.2 frame和約束區別
1、frame是不可以累加的,只能被替換掉,但是constraint可以累加
2、frame不可以跨級添加,但是contraint可以跨層級
3、contraint可以設定priority
另外,需要注意的是,在autolayout下使用frame,會把frame轉化成autolayout的約束,如果再進行約束的設置,由于多次累加可能會造成沖突
github博客:https://wf96390.github.io/blog/2016/03/16/autolayout/