iOS頁面的佈局方式
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的約束,如果再進行約束的設定,由於多次累加可能會造成衝突