iOS學習筆記(5)-Auto Layout基本原理

之前在看MIT那個教學視頻時,對iOS的界面布局點到即止,一直對Auto Layout的原理不太明了。最近重新看了遍官方的文檔,終于對Auto Layout明白了一二。本文對iOS8加入的Size Class以及iOS9加入的Stack Views暫時不做過多討論,后續有時間再補上,我是剛開始學習iOS開發,難免有理解錯誤的地方,請大家指正。

1 UIView的層次結構

在討論Auto Layout前先來了解下UIView的層次結構,在iOS的視圖中,最底層的是UIWindow(UIWindow當然也是從UIView繼承而來),其上再是我們的View Controller的UIView,再上面則是我們自己拖拽的各種控件的UIView。要看到UIView的層次結構,可以通過Xcode的Debug View HieraHierarchy按鈕來查看。

圖1.1 UIView層次查看按鈕

下面是我創建的一個測試的工程代碼,選擇的是Single View Application,工程創建好后,Xcode就已經為我們創建了一個View Controller(本文后面用VC來指代View Controller),并設置好了VC對應的Class。我在Main.storyboard的VC對應的View上面加入了一個Button和一個Label。

我們可以看到這個測試應用的UIView層次結構如下,一共四層:其中最底層為UIWindow,一個應用通常只有一個UIWindow,它是所有子視圖的根視圖。之上是VC對應的UIView,再上一層就是UILabel和UIButton,最上面那層是UIButtonLabel(也就是我們通常見到的 button.titleLabel)。

圖1.2 UIView層次圖

這些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開發要布局視圖是基于frame的,如在我的筆記1中提到的那樣,即只要指定視圖的起始坐標(origin)以及寬度(width)和高度(height)即可確定視圖在superview中的位置。如下圖所示,第一個視圖起始坐標為(20,20),寬度是120,高度為80;第二次視圖起始坐標為(20,108),寬度高度與第一個視圖相同:

圖2.1 基于frame的布局

如果在程序運行過程中,如果有視圖的位置改變,則需要重新計算所有受影響的視圖的位置。通過編碼來實現位置定位固然有很大的靈活性,單頁帶來了很大的不便,比如我們屏幕尺寸發生變化,或者旋轉屏幕,為了保持之前的布局,就需要修改其中一些視圖的起始位置以及寬度高度等。雖然在UIView中有一個autoresizingMask的屬性,它對應的是一個枚舉的值,這個屬性能夠自動調整子控件與父控件中間的位置,寬高等,能夠在一定程度上減輕基于frame布局帶來的不便,但是autoresizingMask并只支持父子視圖之間進行約束,并不支持同級視圖和跨級視圖的布局。對于復雜的用戶界面同樣需要編碼進行控制。正是由于這些問題,才誕生了我們這篇文章中要討論的Auto Layout。

3 Auto Layout

3.1 Auto Layout基本原理

Auto Layout是一種全新的布局方式,它采用一系列約束(constraints)來實現自動布局,當你的屏幕尺寸發生變化或者屏幕發生旋轉時,可以不用添加代碼來保持原有布局不變,實現視圖的自動布局。

所謂約束,通常是定義了兩個視圖之間的關系(當然你也可以一個視圖自己跟自己設定約束)。如下圖就是一個約束的例子,當然要確定一個視圖的位置,跟基于frame一樣,也是需要確定視圖的橫縱坐標以及寬度和高度的,只是,這個橫縱坐標和寬度高度不再是寫死的數值,而是根據約束計算得來,從而達到自動布局的效果

圖3.1 view_formula

約束其實是一個兩個視圖之間的線性關系。如圖3.1所示,就是Blue View和Red View的一條約束。表示Red View的左邊緣等于Blue View的右邊緣(在從左到右書寫的系統里面,leading=left,trailing=right) + 8個Point,注意,在iOS代碼里面都是用的邏輯點,不是真正的物理像素點。其中關系可以是=、>=以及<=這三個的一種,當然我們的例子用的是=。

還有一個要注意的是,這里只是給出了一個約束來說明約束的基本范式,顯然一個約束是不能完成Blue View和Red View的自動布局的,下一節通過實例來看看自動布局具體應該怎么操作。

3.2 Auto Layout初體驗 & Fitting Size

新建一個Single View Application,然后添加一個View到視圖中,
我們什么約束都不加,發現Xcode是沒有任何錯誤和警告的。但是如果我們自己手動加了一條約束(見圖3.2),Xcode卻會有警告。一開始學習都會有這個困惑,為什么會出現這個情況呢?

圖3.1 layout缺少約束

原因其實就是,如果我們什么約束都不加,那么Xcode其實已經幫你自動加了約束信息了,這個約束稱之為prototyping constraints,也就是說,這個添加的Green View的橫縱坐標,寬度高度都已經設定為一個值了(這個值可以在屬性標簽里面看到),所以,Green View的位置已經固定,自然Xcode也就不會有錯誤或警告了。而如果我們手動加了一條約束,那么Xcode認為你要自己添加約束了,那么在Auto Layout引擎檢查約束完備性的時候自動添加的約束會被忽略,所以,這個時候因為我們只加了一個Y軸的約束條件,缺少X軸的約束條件,因此會報約束錯誤的提示(當然這個并不影響工程的運行,你要編譯運行還是可以的,而且自動添加的約束如果沒有被顯示添加的約束覆蓋,也還是會生效的,只是控件的位置可能會存在歧義,影響最終布局效果)。那么我們再加上其他的三個約束,好了,錯誤沒有了。最終添加的約束如下(約束還有優先級這個非常重要的屬性,后面再談):

圖3.2 添加完整的約束

這四個約束可以用下面的四個等式來表示:

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其實是指的根視圖的頂部,模擬器在豎屏下有狀態欄,狀態欄默認高度為20(注:導航欄與狀態欄高度不同,導航欄的豎屏默認高度為44,橫屏默認高度為32),則Green View的Y坐標就是20 + 20 = 40。模擬器在橫屏下沒有狀態欄,則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 橫屏

我們可以發現,Green View在橫屏和豎屏的大小和位置都是不同的,但是整體布局是我們所希望的效果。這就是Auto Layout做的事情,通過這些約束,根據屏幕大小不同,屏幕方向不同來動態計算控件的大小和位置。計算方法也很簡單,比如我們的例子,因為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居然沒有報錯,這可能讓人納悶了,我們并沒有指定按鈕的寬度和高度,那最終按鈕是如何定位的呢?這就是這一節要討論的內容,一些iOS控件如按鈕控件,文本控件等其實是有一個自身內容尺寸的,這類控件會根據自身內容尺寸添加布局約束,如果我們沒有顯示指定控件的寬度和高度,則其自動添加的約束就會起作用。正如下圖中的按鈕,我們只指定了橫縱坐標的約束,并沒有指定寬度和高度,但是Xcode并沒有報錯或者警告。

圖3.3 自身內容尺寸完成完整約束

下表列出了一些常用控件的ICS,由表中可以發現,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)效果是跟自身內容尺寸關聯在一起的,如圖3.4所示,抗壓縮定義了視圖抗壓縮的優先級,優先級越大,表示越難壓縮;抗拉伸則定義了視圖抗拉伸的優先級,優先級越大,則越難被拉伸。抗壓縮和抗拉伸的優先級是針對橫豎兩個方向的,每個方向都有一個優先級。默認的View和Button的抗壓縮優先級為750,抗拉伸優先級為250。從優先級大小可以看出來,拉伸一個View比壓縮一個View容易。這也符合我們的期望,比如我們期望拉伸一個按鈕大于其自身內容尺寸,而不是縮小按鈕尺寸導致內容顯示不全。

圖3.4 CRCH圖示
// 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的約束,通常會優先壓縮那個抗壓縮優先級小的控件來適應視圖的布局。

下面看一個例子,我們在視圖中添加一個Label和一個Text Field。然后分別設置了Label的左上的約束和Text Field的右上約束,然后設置Label和Text Field的間距為20。約束關系我們可以看到左邊的5個等式,因為Label和Text Field都有自身內容尺寸,所以這5個等式已經可以完成布局了。在這個例子中我們看到Text Field被拉伸了,而Label還是保持自身內容尺寸的,這是因為Label的默認抗拉伸優先級為251大于Text Field的默認抗拉伸優先級250,因此Label更難被拉伸,所以看到的是Text Field被拉伸了。那如果我們把Text Field的抗拉伸優先級改為252,則最終運行的界面如圖3.5.4所示。

圖3.5.1 默認的CRCH效果
圖3.5.2 Label的CRCH優先級
圖3.5.3 Text Field的CRCH優先級
圖3.5.4 增大了Text Field的CRCH效果

接下來再看一個Image View的例子,可以看看自身內容尺寸和CRCH對Image View的影響。這里我在Image View里面加了個apple.jpg的圖片,圖片原始尺寸為241*300。開始的時候我設置Image View水平垂直居中,不設置寬度高度,則Image View的寬度和高度為圖片原始尺寸241和300。然后再添加一個寬度約束,設置圖片寬度為300。由于顯示添加的約束的默認優先級為1000,而Image View的抗拉伸的優先級為251,所以會以顯示添加的約束為準,圖片寬度會被拉升到300。而如果我們把顯示添加的寬度約束的優先級改成250,則圖片寬度會被設置為原始寬度241。

圖3.5.5 Image View的CRCH效果

4 更多例子

4.1 兩個寬度相等的View

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

圖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 自身內容尺寸

圖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的抗拉伸的優先級251比Text Field的250更高,所以最終看到的效果是Text Field被拉伸了。

4.4 自適應View

圖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優先級比約束3要高,約束6的優先級比約束5的高,這樣如果顯示狀態欄(模擬器里面豎屏的時候),我們知道狀態欄的高度為20,那么這時約束3滿足的時候,也就是Blue View的y坐標為28(狀態欄高度20+標準距離8),這時約束4也滿足,因此會選擇約束3這個優先級較低的約束。如果不顯示狀態欄(模擬器里面橫屏的時候),則此時只能滿足約束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

Stack View布局例子:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/LayoutUsingStackViews.html#//apple_ref/doc/uid/TP40010853-CH11-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

5 參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容