之前在看MIT那個教學視頻時逗扒,對iOS的界面布局點到即止,一直對Auto Layout的原理不太明了。最近重新看了遍官方的文檔吁伺,終于對Auto Layout明白了一二。本文對iOS8加入的Size Class以及iOS9加入的Stack Views暫時不做過多討論租谈,后續(xù)有時間再補上篮奄,我是剛開始學習iOS開發(fā),難免有理解錯誤的地方割去,請大家指正窟却。
1 UIView的層次結構
在討論Auto Layout前先來了解下UIView的層次結構,在iOS的視圖中呻逆,最底層的是UIWindow
(UIWindow當然也是從UIView繼承而來)夸赫,其上再是我們的View Controller
的UIView,再上面則是我們自己拖拽的各種控件的UIView咖城。要看到UIView的層次結構茬腿,可以通過Xcode的Debug View HieraHierarchy
按鈕來查看。
下面是我創(chuàng)建的一個測試的工程代碼宜雀,選擇的是Single View Application
,工程創(chuàng)建好后切平,Xcode就已經為我們創(chuàng)建了一個View Controller
(本文后面用VC來指代View Controller),并設置好了VC對應的Class辐董。我在Main.storyboard
的VC對應的View上面加入了一個Button和一個Label悴品。
我們可以看到這個測試應用的UIView層次結構如下,一共四層:其中最底層為UIWindow简烘,一個應用通常只有一個UIWindow他匪,它是所有子視圖的根視圖。之上是VC對應的UIView夸研,再上一層就是UILabel和UIButton邦蜜,最上面那層是UIButtonLabel(也就是我們通常見到的 button.titleLabel)。
這些UIView的層次關系是:
UIWindow.superview -> null
UIView.superview -> UIWindow
UIButton.superview -> UIView
UILabel.superview -> UIView
UIButtonLabel.superview -> UIButton
2 Frame-based Layout
在談論Auto Layout之前亥至,先看看Auto Layout出現前iOS是通過什么來實現視圖的布局的悼沈。在Auto Layout出現前贱迟,iOS開發(fā)要布局視圖是基于frame的,如在我的筆記1中提到的那樣絮供,即只要指定視圖的起始坐標(origin)以及寬度(width)和高度(height)即可確定視圖在superview中的位置衣吠。如下圖所示,第一個視圖起始坐標為(20,20)壤靶,寬度是120缚俏,高度為80;第二次視圖起始坐標為(20,108),寬度高度與第一個視圖相同:
如果在程序運行過程中贮乳,如果有視圖的位置改變忧换,則需要重新計算所有受影響的視圖的位置。通過編碼來實現位置定位固然有很大的靈活性向拆,單頁帶來了很大的不便亚茬,比如我們屏幕尺寸發(fā)生變化,或者旋轉屏幕浓恳,為了保持之前的布局刹缝,就需要修改其中一些視圖的起始位置以及寬度高度等。雖然在UIView中有一個autoresizingMask的屬性颈将,它對應的是一個枚舉的值梢夯,這個屬性能夠自動調整子控件與父控件中間的位置,寬高等晴圾,能夠在一定程度上減輕基于frame布局帶來的不便颂砸,但是autoresizingMask并只支持父子視圖之間進行約束,并不支持同級視圖和跨級視圖的布局疑务。對于復雜的用戶界面同樣需要編碼進行控制。正是由于這些問題梗醇,才誕生了我們這篇文章中要討論的Auto Layout知允。
3 Auto Layout
3.1 Auto Layout基本原理
Auto Layout是一種全新的布局方式,它采用一系列約束(constraints)來實現自動布局叙谨,當你的屏幕尺寸發(fā)生變化或者屏幕發(fā)生旋轉時温鸽,可以不用添加代碼來保持原有布局不變,實現視圖的自動布局手负。
所謂約束涤垫,通常是定義了兩個視圖之間的關系(當然你也可以一個視圖自己跟自己設定約束)。如下圖就是一個約束的例子竟终,當然要確定一個視圖的位置蝠猬,跟基于frame一樣,也是需要確定視圖的橫縱坐標以及寬度和高度的统捶,只是榆芦,這個橫縱坐標和寬度高度不再是寫死的數值柄粹,而是根據約束計算得來,從而達到自動布局的效果匆绣。
約束其實是一個兩個視圖之間的線性關系驻右。如圖3.1所示,就是Blue View和Red View的一條約束崎淳。表示Red View的左邊緣等于Blue View的右邊緣(在從左到右書寫的系統(tǒng)里面堪夭,leading=left,trailing=right) + 8個Point拣凹,注意森爽,在iOS代碼里面都是用的邏輯點,不是真正的物理像素點咐鹤。其中關系可以是=拗秘、>=以及<=
這三個的一種,當然我們的例子用的是=祈惶。
還有一個要注意的是雕旨,這里只是給出了一個約束來說明約束的基本范式,顯然一個約束是不能完成Blue View和Red View的自動布局的捧请,下一節(jié)通過實例來看看自動布局具體應該怎么操作狸吞。
3.2 Auto Layout初體驗 & Fitting Size
新建一個Single View Application
,然后添加一個View到視圖中潜必,
我們什么約束都不加段标,發(fā)現Xcode是沒有任何錯誤和警告的。但是如果我們自己手動加了一條約束(見圖3.2)可款,Xcode卻會有警告育韩。一開始學習都會有這個困惑,為什么會出現這個情況呢闺鲸?
原因其實就是筋讨,如果我們什么約束都不加,那么Xcode其實已經幫你自動加了約束信息了摸恍,這個約束稱之為prototyping constraints
悉罕,也就是說,這個添加的Green View的橫縱坐標立镶,寬度高度都已經設定為一個值了(這個值可以在屬性標簽里面看到)壁袄,所以,Green View的位置已經固定媚媒,自然Xcode也就不會有錯誤或警告了嗜逻。而如果我們手動加了一條約束,那么Xcode認為你要自己添加約束了缭召,那么在Auto Layout引擎檢查約束完備性的時候自動添加的約束會被忽略变泄,所以令哟,這個時候因為我們只加了一個Y軸的約束條件,缺少X軸的約束條件妨蛹,因此會報約束錯誤的提示(當然這個并不影響工程的運行屏富,你要編譯運行還是可以的,而且自動添加的約束如果沒有被顯示添加的約束覆蓋蛙卤,也還是會生效的狠半,只是控件的位置可能會存在歧義,影響最終布局效果)颤难。那么我們再加上其他的三個約束神年,好了,錯誤沒有了行嗤。最終添加的約束如下(約束還有優(yōu)先級這個非常重要的屬性已日,后面再談):
這四個約束可以用下面的四個等式來表示:
Green View.Trailing = Superview.Trailing Margin
Green View.Leading = Superview.Leading Margin
Green View.Bottom = Bottom Layout Guide.Top + 20
Green View.Top = Top Layout Guide.Bottom + 20
注意到這里引入了幾個變量,一個是Top/Bottom Layout Guide(頂部/底部導航)栅屏,一個是Superview.leading/Trailing Margin(左/右邊緣間距)飘千。Top Layout Guide其實是指的根視圖的頂部,模擬器在豎屏下有狀態(tài)欄栈雳,狀態(tài)欄默認高度為20(注:導航欄與狀態(tài)欄高度不同护奈,導航欄的豎屏默認高度為44,橫屏默認高度為32)哥纫,則Green View的Y坐標就是20 + 20 = 40霉旗。模擬器在橫屏下沒有狀態(tài)欄,則Top Layout Guide.Bottom為0蛀骇,則Green View的Y坐標就是20厌秒。Superview.leading Margin在豎屏時為16,橫屏是為20擅憔。這幾個結論可以通過打印Green View的frame值來驗證:
green view frame:{{16, 40}, {343, 607}} //iPhone6 豎屏
green view frame:{{20, 20}, {627, 335}} //iPhone6 橫屏
我們可以發(fā)現鸵闪,Green View在橫屏和豎屏的大小和位置都是不同的,但是整體布局是我們所希望的效果雕欺。這就是Auto Layout做的事情岛马,通過這些約束棉姐,根據屏幕大小不同屠列,屏幕方向不同來動態(tài)計算控件的大小和位置伞矩。計算方法也很簡單笛洛,比如我們的例子,因為iPhone6的邏輯像素點是375 X 667乃坤,因此可以通過上面的約束計算Green View的大小苛让。由于我們并沒有設置視圖的大小沟蔑,視圖最終呈現的大小是由Auto Layout引擎根據約束計算得到的,這個大小也稱之為視圖的Fitting Size狱杰,這也就是Auto Layout的便捷之處瘦材,我們不需要寫任何代碼去控制。
width = 375 - 16*2 = 343, height = 667 - 40 - 20 = 607 //iPhone6 豎屏
width = 667 - 20*2 = 627, height = 375 - 20*2 = 335 //iPhone6 橫屏
3.3 自身內容尺寸 & 抗壓縮抗拉伸效果
先簡化一下這兩個概念:
- 自身內容尺寸(Intrinsic Content Size仿畸,以下簡稱ICS)食棕。
- 抗壓縮抗拉伸(Compression-Resistance and Content-Hugging,以下簡稱CRCH)
自身內容尺寸
前面我們添加了一個View到根視圖中错沽,也初次體會到了Auto Layout的強大之處簿晓,接下來我們來添加一個按鈕。如下圖所示千埃,我們只添加了兩個約束憔儿,Xcode居然沒有報錯,這可能讓人納悶了放可,我們并沒有指定按鈕的寬度和高度谒臼,那最終按鈕是如何定位的呢?這就是這一節(jié)要討論的內容吴侦,一些iOS控件如按鈕控件屋休,文本控件等其實是有一個自身內容尺寸的,這類控件會根據自身內容尺寸添加布局約束备韧,如果我們沒有顯示指定控件的寬度和高度劫樟,則其自動添加的約束就會起作用。正如下圖中的按鈕织堂,我們只指定了橫縱坐標的約束叠艳,并沒有指定寬度和高度,但是Xcode并沒有報錯或者警告易阳。
下表列出了一些常用控件的ICS附较,由表中可以發(fā)現,label潦俺, button拒课, text fields等都是有ICS的,而UIView和NSView是沒有ICS的事示。
View | Intrinsic content size |
---|---|
UIView and NSView | No intrinsic content size. |
Sliders | Defines only the width (iOS). |
Labels, buttons, switches, and text fields | Defines both the height and the width. |
Text views and image views | Intrinsic content size can vary. |
控件的ICS基于視圖的當前內容早像。Button或者Label的ICS基于其展示的文字數目和字體大小,空的Image View是沒有ICS的肖爵,只有當你添加了圖片到Image View中卢鹦,這個時候才會有ICS,而且尺寸大小為圖片的尺寸劝堪。
Updated:視圖UIView也是沒有ICS的冀自,有時候想只指定位置而不指定UIView的大小揉稚,可以在Storyboard的Size inspector中設置Intrinsic Size為Placeholder,這樣便不會報錯了熬粗。注意一點的是搀玖,這個設置并不影響運行時UIView的Intrinsic Size。
抗壓縮和抗拉伸效果
抗壓縮(Compression-Resistance) 和抗拉伸(Content-Hugging)效果是跟自身內容尺寸關聯(lián)在一起的驻呐,如圖3.4所示巷怜,抗壓縮定義了視圖抗壓縮的優(yōu)先級,優(yōu)先級越大暴氏,表示越難壓縮延塑;抗拉伸則定義了視圖抗拉伸的優(yōu)先級,優(yōu)先級越大答渔,則越難被拉伸关带。抗壓縮和抗拉伸的優(yōu)先級是針對橫豎兩個方向的沼撕,每個方向都有一個優(yōu)先級宋雏。默認的View和Button的抗壓縮優(yōu)先級為750,抗拉伸優(yōu)先級為250务豺。從優(yōu)先級大小可以看出來磨总,拉伸一個View比壓縮一個View容易。這也符合我們的期望笼沥,比如我們期望拉伸一個按鈕大于其自身內容尺寸蚪燕,而不是縮小按鈕尺寸導致內容顯示不全。
// Compression Resistance
View.height >= 0.0 * NotAnAttribute + IntrinsicHeight
View.width >= 0.0 * NotAnAttribute + IntrinsicWidth
// Content Hugging
View.height <= 0.0 * NotAnAttribute + IntrinsicHeight
View.width <= 0.0 * NotAnAttribute + IntrinsicWidth
對于兩個控件來說奔浅,為了滿足Auto Layout的約束馆纳,通常會優(yōu)先壓縮那個抗壓縮優(yōu)先級小的控件來適應視圖的布局。
下面看一個例子汹桦,我們在視圖中添加一個Label和一個Text Field鲁驶。然后分別設置了Label的左上的約束和Text Field的右上約束,然后設置Label和Text Field的間距為20舞骆。約束關系我們可以看到左邊的5個等式钥弯,因為Label和Text Field都有自身內容尺寸,所以這5個等式已經可以完成布局了督禽。在這個例子中我們看到Text Field被拉伸了脆霎,而Label還是保持自身內容尺寸的,這是因為Label的默認抗拉伸優(yōu)先級為251大于Text Field的默認抗拉伸優(yōu)先級250赂蠢,因此Label更難被拉伸绪穆,所以看到的是Text Field被拉伸了辨泳。那如果我們把Text Field的抗拉伸優(yōu)先級改為252虱岂,則最終運行的界面如圖3.5.4所示玖院。
接下來再看一個Image View的例子,可以看看自身內容尺寸和CRCH對Image View的影響第岖。這里我在Image View里面加了個apple.jpg的圖片难菌,圖片原始尺寸為241*300
。開始的時候我設置Image View水平垂直居中蔑滓,不設置寬度高度郊酒,則Image View的寬度和高度為圖片原始尺寸241和300。然后再添加一個寬度約束键袱,設置圖片寬度為300燎窘。由于顯示添加的約束的默認優(yōu)先級為1000,而Image View的抗拉伸的優(yōu)先級為251蹄咖,所以會以顯示添加的約束為準褐健,圖片寬度會被拉升到300。而如果我們把顯示添加的寬度約束的優(yōu)先級改成250澜汤,則圖片寬度會被設置為原始寬度241蚜迅。
4 更多例子
4.1 兩個寬度相等的View
約束關系:
1.Yellow View.Leading = Superview.LeadingMargin
2.Green View.Leading = Yellow View.Trailing + Standard
3.Green View.Trailing = Superview.TrailingMargin
4.Yellow View.Top = Top Layout Guide.Bottom + 20.0
5.Green View.Top = Top Layout Guide.Bottom + 20.0
6.Bottom Layout Guide.Top = Yellow View.Bottom + 20.0
7.Bottom Layout Guide.Top = Green View.Bottom + 20.0
8.Yellow View.Width = Green View.Width
4.2 兩個寬度不等的View
約束關系:
1.Purple View.Leading = Superview.LeadingMargin
2.Orange View.Leading = Purple View.Trailing + Standard
3.Orange View.Trailing = Superview.TrailingMargin
4.Purple View.Top = Top Layout Guide.Bottom + 20.0
5.Orange View.Top = Top Layout Guide.Bottom + 20.0
6.Bottom Layout Guide.Top = Purple View.Bottom + 20.0
7.Bottom Layout Guide.Top = Orange View.Bottom + 20.0
8.Orange View.Width = 2.0 x Purple View.Width
4.3 自身內容尺寸
約束:
1.Name Label.Leading = Superview.LeadingMargin
2.Name Text Field.Trailing = Superview.TrailingMargin
3.Name Text Field.Leading = Name Label.Trailing + Standard
4.Name Text Field.Top = Top Layout Guide.Bottom + 20.0
5.Name label.Baseline = Name Text Field.Baseline
這個例子跟前面提到的類似,注意并不需要設置Label和Text Field的寬度和高度俊抵。而且默認設置中谁不,Label的抗拉伸的優(yōu)先級251比Text Field的250更高,所以最終看到的效果是Text Field被拉伸了徽诲。
4.4 自適應View
約束:
1.Blue View.Leading = Superview.LeadingMargin
2.Blue View.Trailing = Superview.TrailingMargin
3.Blue View.Top = Top Layout Guide.Bottom + Standard (Priority 750)
4.Blue View.Top >= Superview.Top + 20.0
5.Bottom Layout Guide.Top = Blue View.Bottom + Standard (Priority 750)
6.Superview.Bottom >= Blue View.Bottom + 20.0
前面的例子都是=
的約束刹帕,這個例子加了>=
的約束。
注意到我們設置的>=
的約束4優(yōu)先級比約束3要高谎替,約束6的優(yōu)先級比約束5的高轩拨,這樣如果顯示狀態(tài)欄(模擬器里面豎屏的時候),我們知道狀態(tài)欄的高度為20院喜,那么這時約束3滿足的時候亡蓉,也就是Blue View的y坐標為28(狀態(tài)欄高度20+標準距離8),這時約束4也滿足喷舀,因此會選擇約束3這個優(yōu)先級較低的約束砍濒。如果不顯示狀態(tài)欄(模擬器里面橫屏的時候),則此時只能滿足約束4硫麻,無法滿足約束3爸邢。不過Auto Layout引擎會選擇一個最接近的約束,也就是設置Blue View的y坐標為20拿愧。
更多例子:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSimpleConstraints.html#//apple_ref/doc/uid/TP40010853-CH12-SW1
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/ViewswithIntrinsicContentSize.html#//apple_ref/doc/uid/TP40010853-CH13-SW1
Size Class例子:
https://www.raywenderlich.com/113768/adaptive-layout-tutorial-in-ios-9-getting-started
使用代碼和VFL來添加約束可以參見:
http://blog.csdn.net/pucker/article/details/45070955
http://blog.csdn.net/pucker/article/details/45093483