iOS學(xué)習(xí)筆記(5)-Auto Layout基本原理

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

1 UIView的層次結(jié)構(gòu)

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

圖1.1 UIView層次查看按鈕

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

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

圖1.2 UIView層次圖

這些UIView的層次關(guān)系是:

UIWindow.superview -> null
UIView.superview -> UIWindow
UIButton.superview -> UIView
UILabel.superview -> UIView
UIButtonLabel.superview -> UIButton

2 Frame-based Layout

在談?wù)揂uto Layout之前,先看看Auto Layout出現(xiàn)前iOS是通過什么來實(shí)現(xiàn)視圖的布局的。在Auto Layout出現(xiàn)前,iOS開發(fā)要布局視圖是基于frame的,如在我的筆記1中提到的那樣,即只要指定視圖的起始坐標(biāo)(origin)以及寬度(width)和高度(height)即可確定視圖在superview中的位置。如下圖所示,第一個(gè)視圖起始坐標(biāo)為(20,20),寬度是120,高度為80;第二次視圖起始坐標(biāo)為(20,108),寬度高度與第一個(gè)視圖相同:

圖2.1 基于frame的布局

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

3 Auto Layout

3.1 Auto Layout基本原理

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

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

圖3.1 view_formula

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

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

3.2 Auto Layout初體驗(yàn) & Fitting Size

新建一個(gè)Single View Application,然后添加一個(gè)View到視圖中,
我們什么約束都不加,發(fā)現(xiàn)Xcode是沒有任何錯(cuò)誤和警告的。但是如果我們自己手動(dòng)加了一條約束(見圖3.2),Xcode卻會(huì)有警告。一開始學(xué)習(xí)都會(huì)有這個(gè)困惑,為什么會(huì)出現(xiàn)這個(gè)情況呢?

圖3.1 layout缺少約束

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

圖3.2 添加完整的約束

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

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

注意到這里引入了幾個(gè)變量,一個(gè)是Top/Bottom Layout Guide(頂部/底部導(dǎo)航),一個(gè)是Superview.leading/Trailing Margin(左/右邊緣間距)。Top Layout Guide其實(shí)是指的根視圖的頂部,模擬器在豎屏下有狀態(tài)欄,狀態(tài)欄默認(rèn)高度為20(注:導(dǎo)航欄與狀態(tài)欄高度不同,導(dǎo)航欄的豎屏默認(rèn)高度為44,橫屏默認(rèn)高度為32),則Green View的Y坐標(biāo)就是20 + 20 = 40。模擬器在橫屏下沒有狀態(tài)欄,則Top Layout Guide.Bottom為0,則Green View的Y坐標(biāo)就是20。Superview.leading Margin在豎屏?xí)r為16,橫屏是為20。這幾個(gè)結(jié)論可以通過打印Green View的frame值來驗(yàn)證:

green view frame:{{16, 40}, {343, 607}} //iPhone6 豎屏
green view frame:{{20, 20}, {627, 335}} //iPhone6 橫屏

我們可以發(fā)現(xiàn),Green View在橫屏和豎屏的大小和位置都是不同的,但是整體布局是我們所希望的效果。這就是Auto Layout做的事情,通過這些約束,根據(jù)屏幕大小不同,屏幕方向不同來動(dòng)態(tài)計(jì)算控件的大小和位置。計(jì)算方法也很簡單,比如我們的例子,因?yàn)閕Phone6的邏輯像素點(diǎn)是375 X 667,因此可以通過上面的約束計(jì)算Green View的大小。由于我們并沒有設(shè)置視圖的大小,視圖最終呈現(xiàn)的大小是由Auto Layout引擎根據(jù)約束計(jì)算得到的,這個(gè)大小也稱之為視圖的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 自身內(nèi)容尺寸 & 抗壓縮抗拉伸效果

先簡化一下這兩個(gè)概念:

  • 自身內(nèi)容尺寸(Intrinsic Content Size,以下簡稱ICS)。
  • 抗壓縮抗拉伸(Compression-Resistance and Content-Hugging,以下簡稱CRCH)

自身內(nèi)容尺寸

前面我們添加了一個(gè)View到根視圖中,也初次體會(huì)到了Auto Layout的強(qiáng)大之處,接下來我們來添加一個(gè)按鈕。如下圖所示,我們只添加了兩個(gè)約束,Xcode居然沒有報(bào)錯(cuò),這可能讓人納悶了,我們并沒有指定按鈕的寬度和高度,那最終按鈕是如何定位的呢?這就是這一節(jié)要討論的內(nèi)容,一些iOS控件如按鈕控件,文本控件等其實(shí)是有一個(gè)自身內(nèi)容尺寸的,這類控件會(huì)根據(jù)自身內(nèi)容尺寸添加布局約束,如果我們沒有顯示指定控件的寬度和高度,則其自動(dòng)添加的約束就會(huì)起作用。正如下圖中的按鈕,我們只指定了橫縱坐標(biāo)的約束,并沒有指定寬度和高度,但是Xcode并沒有報(bào)錯(cuò)或者警告。

圖3.3 自身內(nèi)容尺寸完成完整約束

下表列出了一些常用控件的ICS,由表中可以發(fā)現(xiàn),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基于視圖的當(dāng)前內(nèi)容。Button或者Label的ICS基于其展示的文字?jǐn)?shù)目和字體大小,空的Image View是沒有ICS的,只有當(dāng)你添加了圖片到Image View中,這個(gè)時(shí)候才會(huì)有ICS,而且尺寸大小為圖片的尺寸。

Updated:視圖UIView也是沒有ICS的,有時(shí)候想只指定位置而不指定UIView的大小,可以在Storyboard的Size inspector中設(shè)置Intrinsic Size為Placeholder,這樣便不會(huì)報(bào)錯(cuò)了。注意一點(diǎn)的是,這個(gè)設(shè)置并不影響運(yùn)行時(shí)UIView的Intrinsic Size。

抗壓縮和抗拉伸效果

抗壓縮(Compression-Resistance) 和抗拉伸(Content-Hugging)效果是跟自身內(nèi)容尺寸關(guān)聯(lián)在一起的,如圖3.4所示,抗壓縮定義了視圖抗壓縮的優(yōu)先級(jí),優(yōu)先級(jí)越大,表示越難壓縮;抗拉伸則定義了視圖抗拉伸的優(yōu)先級(jí),優(yōu)先級(jí)越大,則越難被拉伸??箟嚎s和抗拉伸的優(yōu)先級(jí)是針對橫豎兩個(gè)方向的,每個(gè)方向都有一個(gè)優(yōu)先級(jí)。默認(rèn)的View和Button的抗壓縮優(yōu)先級(jí)為750,抗拉伸優(yōu)先級(jí)為250。從優(yōu)先級(jí)大小可以看出來,拉伸一個(gè)View比壓縮一個(gè)View容易。這也符合我們的期望,比如我們期望拉伸一個(gè)按鈕大于其自身內(nèi)容尺寸,而不是縮小按鈕尺寸導(dǎo)致內(nèi)容顯示不全。

圖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

對于兩個(gè)控件來說,為了滿足Auto Layout的約束,通常會(huì)優(yōu)先壓縮那個(gè)抗壓縮優(yōu)先級(jí)小的控件來適應(yīng)視圖的布局。

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

圖3.5.1 默認(rèn)的CRCH效果
圖3.5.2 Label的CRCH優(yōu)先級(jí)
圖3.5.3 Text Field的CRCH優(yōu)先級(jí)
圖3.5.4 增大了Text Field的CRCH效果

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

圖3.5.5 Image View的CRCH效果

4 更多例子

4.1 兩個(gè)寬度相等的View

4.1兩個(gè)寬度相等的View
約束關(guān)系:
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 兩個(gè)寬度不等的View

圖4.2 兩個(gè)寬度不等的View
約束關(guān)系:
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 自身內(nèi)容尺寸

圖4.3 自身內(nèi)容尺寸布局
約束:
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

這個(gè)例子跟前面提到的類似,注意并不需要設(shè)置Label和Text Field的寬度和高度。而且默認(rèn)設(shè)置中,Label的抗拉伸的優(yōu)先級(jí)251比Text Field的250更高,所以最終看到的效果是Text Field被拉伸了。

4.4 自適應(yīng)View

圖4.4 自適應(yīng)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

前面的例子都是=的約束,這個(gè)例子加了>=的約束。
注意到我們設(shè)置的>=的約束4優(yōu)先級(jí)比約束3要高,約束6的優(yōu)先級(jí)比約束5的高,這樣如果顯示狀態(tài)欄(模擬器里面豎屏的時(shí)候),我們知道狀態(tài)欄的高度為20,那么這時(shí)約束3滿足的時(shí)候,也就是Blue View的y坐標(biāo)為28(狀態(tài)欄高度20+標(biāo)準(zhǔn)距離8),這時(shí)約束4也滿足,因此會(huì)選擇約束3這個(gè)優(yōu)先級(jí)較低的約束。如果不顯示狀態(tài)欄(模擬器里面橫屏的時(shí)候),則此時(shí)只能滿足約束4,無法滿足約束3。不過Auto Layout引擎會(huì)選擇一個(gè)最接近的約束,也就是設(shè)置Blue View的y坐標(biāo)為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 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容