iOS界面布局的核心以及TangramKit介紹

前言

TangramKit是iOS系統(tǒng)下用Swift編寫的第三方界面布局框架。他集成了iOS的AutoLayout和SizeClass以及Android的五大容器布局體系以及HTML/CSS中的float和flex-box的布局功能和思想,目的是為iOS開發(fā)人員提供一套功能強(qiáng)大、多屏幕靈活適配、簡單易用的UI布局解決方案。Tangram的中文即七巧板的意思,取名的寓意表明這個(gè)布局庫可以非常靈巧和簡單的解決各種復(fù)雜界面布局問題。他的同胞框架:MyLayout是一套用objective-C實(shí)現(xiàn)的界面布局框架。二者的主體思想相同,實(shí)現(xiàn)原理則是通過擴(kuò)展UIView的屬性,以及重載layoutSubviews方法來完成界面布局,只不過在一些語法和屬性設(shè)置上略有一些差異。可以這么說TangramKit是MyLayout布局庫的一個(gè)升級版本。大家可以通過訪問下面的github站點(diǎn)去下載最新的版本:

TangramKit演示效果圖

所見即所得和編碼之爭以及屏幕的適配

在我10多年的開發(fā)生涯中,大部分時(shí)間都工作在客戶端上。從DOS到Windows再到UNIX再到2010年接觸iOS開發(fā)這6年多的時(shí)間中,總感覺一無所獲,原因呢是覺沒有什么積累。作為一個(gè)以編程為職業(yè)的人來說如果不留下什么可以值得為大家所知的東西的話,那將是一種職業(yè)上的遺憾。
就像每個(gè)領(lǐng)域都有工作細(xì)分一樣,現(xiàn)在的編程人員也有明確分工:有一部分人做的是后端開發(fā)的工作,而有一部分人做的是前端開發(fā)的工作。二者相輔相成而完成了整個(gè)系統(tǒng)。后端開發(fā)的重點(diǎn)在于實(shí)現(xiàn)高性能和高可用,在數(shù)據(jù)處理上通常都是一個(gè)輸入一個(gè)加工然后一個(gè)輸出;而前端開發(fā)的重點(diǎn)在于實(shí)現(xiàn)界面流暢性和美觀性,在數(shù)據(jù)處理上往往是多個(gè)輸入一個(gè)加工和多個(gè)輸出。在技術(shù)層面上后端處理的對象是多線程多進(jìn)程以及數(shù)據(jù),而前端處理的對象則是圖形繪制和以及界面布局和動畫特效。
這篇文章的重點(diǎn)是介紹界面布局的核心,因此其他部分就不再展開去說了。對于一個(gè)UI界面來說,好的界面布局體系往往能起到事半工倍的作用。PC設(shè)備上因?yàn)槠聊豢偸菈虼螅热鏥B,VF,PB,Dephi,AWT,Swing等語言或者環(huán)境下的應(yīng)用開發(fā)非常方便,IDE環(huán)境中提供一個(gè)所見即所得的開發(fā)面板(form),人們只要使用簡單的拖拉拽動作就可把各種界面元素加入到form中就可以形成一個(gè)小程序了。而開發(fā)VC程序則相對麻煩,系統(tǒng)的IDE環(huán)境對可視化編程的支持沒有那么的完善,因此大部分界面的構(gòu)建都需要通過編碼來完成。同時(shí)因?yàn)镻C設(shè)備屏幕較大而且標(biāo)準(zhǔn)統(tǒng)一,因此幾乎不存在界面要在各種屏幕尺寸適配的問題。唯一引起爭議是可視化編程和純代碼編程的方式之爭,這種爭議也體現(xiàn)在iOS應(yīng)用的開發(fā)身上,那就是用XIB和SB以及純代碼編寫界面的好壞爭議。關(guān)于這個(gè)問題個(gè)人的意見是各有各好:XIB/SB進(jìn)行布局時(shí)容易上手且所見即所得,但缺乏靈活性和可定制化;而純代碼則靈活性高可定制化強(qiáng),缺點(diǎn)是不能所見即所得和代碼維護(hù)以及系統(tǒng)分層模糊。
再回到屏幕適配的話題來說,如果說PC時(shí)代編程屏幕尺寸適配不是很重要的工作,那么到了移動設(shè)備時(shí)代則不一樣了,適配往往成為整個(gè)工作的重點(diǎn)和難點(diǎn)。主要的原因是設(shè)備的屏幕尺寸和設(shè)備分辨率的多樣性的差異,而且要求在這么小的屏幕上布局眾多的要素,同時(shí)又要求界面美觀和友好的用戶體驗(yàn),這就非常考驗(yàn)產(chǎn)品以及UI/UE人員和開發(fā)人員的水平,同時(shí)這部分工作也占用了開發(fā)者的大部分時(shí)間。在現(xiàn)有的兩個(gè)主流的移動平臺上,Android系統(tǒng)因?yàn)楸旧碛布脚_差異性的原因,為了解決這些差異性而設(shè)計(jì)了一套非常方便的和友好的界面布局體系。它提出了布局容器的概念,也就是有專門職責(zé)的布局容器視圖來管理和排列里面的子視圖,根據(jù)實(shí)際中的應(yīng)用場景而把這些負(fù)責(zé)布局的容器視圖分類抽象出了線性布局、相對布局、框架布局、表格布局、絕對布局這5大容器布局,而這些也就構(gòu)成了Android系統(tǒng)布局體系的核心實(shí)現(xiàn)。也正是這套布局機(jī)制使得Android系統(tǒng)能夠方便的勝任多種屏幕尺寸和分辨率在不同硬件設(shè)備上的UI界面展示。而對于iOS的開發(fā)人員來說,早期的設(shè)備只有單一的3.5in大小且分辨率也只有480x320和960x640這兩種類型的設(shè)備,因此開發(fā)人員只需要采用絕對定位的方式通過視圖的frame屬性設(shè)置來實(shí)現(xiàn)界面的布局,根本不需要考慮到屏幕的適配問題。但是這一切從蘋果后續(xù)依次發(fā)布iPhone4/5/6/7系列的設(shè)備后被打破了,整個(gè)iOS應(yīng)用的開發(fā)也需要考慮到多屏幕尺寸和多分辨率的問題了,這樣原始的frame方法進(jìn)行布局設(shè)置將不能滿足這些多屏幕的適配問題了,因此iOS提出了一套新的界面布局體系:AutoLayout以及SizeClass. 這套機(jī)制通過設(shè)置視圖之間的位置和尺寸的約束以及對屏幕尺寸進(jìn)行分類的方式來完成界面的布局和屏幕的適配工作。
盡管如此, 雖然兩個(gè)移動端平臺都提供了自己獨(dú)有且豐富的界面布局體系,但對于移動客戶端開發(fā)人員來說界面布局和適配仍然是我們在開發(fā)中需要重點(diǎn)關(guān)注的因素之一。

布局的核心

我們知道,在界面開發(fā)中我們直接操作的對象是視圖,視圖可以理解為一個(gè)具有特定功能的矩形區(qū)塊,因此所謂的布局的本質(zhì)就是為視圖指定某個(gè)具體的尺寸以及指定其排列在屏幕上的位置。因此布局的動作就分為兩個(gè)方面:一個(gè)是指定視圖的尺寸,一個(gè)是指定視圖的位置。

視圖的尺寸和位置

視圖的尺寸

視圖的尺寸就是指視圖矩形塊的大小,為了表征視圖的大小我們稱在屏幕水平方向的尺寸大小為寬度,而稱在屏幕垂直方向的尺寸大小為高度,因此一個(gè)視圖的尺寸我們就可以用寬度和高度兩個(gè)維度的值來描述了,寬度和高度的單位我們稱之為點(diǎn)。UIView中用bounds屬性的size部分來描述視圖的尺寸(bounds屬性的origin部分后面會介紹到)。 對于屏幕尺寸來說同樣也用寬度和高度來描述。在視圖層次體系結(jié)構(gòu)中的頂層視圖的尺寸和屏幕的尺寸是一致的,為了描述這個(gè)特殊的頂層視圖我們將這個(gè)頂層根視圖稱之為窗口,窗口的尺寸和屏幕的尺寸一樣大,同時(shí)窗口是一切視圖的容器視圖。一個(gè)視圖的尺寸我們可以用一個(gè)具體的數(shù)值來描述,比如某個(gè)視圖的寬度和高度分別為:100x200。我們稱這種定義的方式為絕對值類型的尺寸。但是在實(shí)際中我們的一些視圖的尺寸并不能夠一開始就被明確,原因是這些視圖的尺寸大小和其他視圖的尺寸大小有關(guān),也就是說視圖的尺寸依賴于另外一個(gè)視圖或者另外一組視圖。比如說有A和B兩個(gè)視圖,我們定義A視圖的寬度和B視圖的寬度相等,而A視圖的高度則是B視圖高度的一半。也就是可以表述為如下:

A.bounds.size.width = B.bounds.size.width
A.bounds.size.height = B.bounds.size.height /2

//父視圖S的高度等于里面子視圖A,B的高度的總和
S.bounds.size.height = A.bounds.size.height + B.bounds.size.height 

我們稱為這種尺寸的定義方式為相對值類型的尺寸。在相對值類型的尺寸中, 視圖某個(gè)維度的尺寸所依賴的另外一個(gè)視圖可以是它的兄弟視圖,也可以是它的父視圖,也可以是它的子視圖,甚至可以是它自身的其他維度。 這種視圖尺寸的依賴關(guān)系是可以傳遞和遞歸的,比如A依賴于B,而B右依賴于C。 但是這種遞歸和傳遞關(guān)系不能形成一個(gè)閉環(huán)依賴,也就是說在依賴關(guān)系的最終節(jié)點(diǎn)視圖的尺寸的值必須是一個(gè)絕對值類型或者特定的相對值類型(wrap包裹值),否則的話我們將形成約束沖突而進(jìn)入死循環(huán)的場景。

兩種尺寸約束依賴

視圖的尺寸之間的依賴關(guān)系還有兩種特定的場景:

  • 某個(gè)視圖的尺寸依賴于里面所有子視圖的尺寸的大小或者依賴于視圖內(nèi)所展示的內(nèi)容的尺寸,我們稱這種依賴為包裹(wrap)
  • 某個(gè)視圖的尺寸依賴于所在父視圖的尺寸減去其他兄弟視圖所占用的尺寸的剩余尺寸也就是說尺寸等于父視圖的尺寸和其兄弟視圖尺寸的差集,我們稱這種依賴為填充(fill)

可以看出包裹和填充尺寸是相對值類型中的兩種特殊的類型,他所依賴的視圖并不是某個(gè)具體的視圖,而是一些相關(guān)的視圖的集合。

為了表征視圖的尺寸以及尺寸可以設(shè)置的值的類型,我們就需要對尺寸進(jìn)行建模,在TangramKit框架中TGLayoutSize類就是一個(gè)尺寸類,這個(gè)類里面的equal方法則是用來設(shè)置視圖尺寸的各種類型的值:包括絕對值類型,相對值類型,以及包裹和填充的值類型等等。同時(shí)我們對UIView擴(kuò)展出了兩個(gè)屬性tg_width, tg_height分別用來表示視圖的布局寬度和布局高度。他其實(shí)是對原生的視圖bounds屬性中的size部分進(jìn)行了擴(kuò)充和延展。原始的bounds屬性中的size部分只能設(shè)置絕對值類型的尺寸,而不能設(shè)置相對值類型的尺寸。

視圖的位置

當(dāng)一個(gè)視圖的尺寸確定后,接下來我們就需要確定視圖所在的位置了。所謂位置就是指視圖在屏幕中的坐標(biāo)位置,屏幕中的坐標(biāo)分為水平坐標(biāo)也就是x軸坐標(biāo),和垂直坐標(biāo)也就是y軸坐標(biāo)。而這個(gè)坐標(biāo)原點(diǎn)在不同的系統(tǒng)中有區(qū)別:iOS系統(tǒng)采用左手坐標(biāo)系,原點(diǎn)都是在左上角,并且規(guī)定y軸在原點(diǎn)以下是正坐標(biāo)軸,而原點(diǎn)以上是負(fù)坐標(biāo)軸,而x軸則在原點(diǎn)右邊是正坐標(biāo)軸,原點(diǎn)左邊是負(fù)坐標(biāo)軸。OSX系統(tǒng)則采用右手坐標(biāo)系,原點(diǎn)在左下角,并且規(guī)定y軸在原點(diǎn)以上是正坐標(biāo)軸,而在原點(diǎn)以下是負(fù)坐標(biāo)軸,而x軸則在原點(diǎn)右邊是正坐標(biāo)軸,原點(diǎn)左邊是負(fù)坐標(biāo)軸。

不同的坐標(biāo)系

因此視圖位置的確定我們需要考慮兩個(gè)方面的問題:一個(gè)是位置是相對于哪個(gè)坐標(biāo)系?一個(gè)是視圖內(nèi)部的哪個(gè)部位來描述這個(gè)位置?

確定一個(gè)視圖的位置時(shí)總是應(yīng)該有一個(gè)參照物,在現(xiàn)有的布局體系中一般分為三種參照物:屏幕、父視圖、兄弟視圖

  • 第一種以屏幕坐標(biāo)系作為參照來確定的位置稱為絕對位置,也就是以屏幕的左上角作為原點(diǎn),每個(gè)視圖的位置都是距離屏幕左上角原點(diǎn)的一個(gè)偏移值。這種絕對位置的設(shè)置方式的優(yōu)點(diǎn)是所有視圖的參照物都是一致的,便于比較和計(jì)算,但缺點(diǎn)是對于那些多層次結(jié)構(gòu)的視圖以及帶滾動效果的視圖來說位置的確定則總是需要進(jìn)行動態(tài)的變化和計(jì)算。比如某個(gè)滾動視圖內(nèi)的所有子視圖在滾動時(shí)都需要重新去計(jì)算自己的位置。

  • 第二種以父視圖坐標(biāo)系作為參照來確定的位置稱為相對位置,每個(gè)子視圖的位置都是距離父視圖左上角原點(diǎn)的一個(gè)偏移值。這樣的好處就是每個(gè)子視圖都不再需要關(guān)心屏幕的原點(diǎn),而只需要以自己的父視圖為原點(diǎn)進(jìn)行位置的計(jì)算就可以了,這種方式是目前大部分布局體系里面采用的定位方式,也是最方便的定位方式,缺點(diǎn)是不同層次之間的視圖的位置在進(jìn)行比較時(shí)需要一步步的往上進(jìn)行轉(zhuǎn)換,直到轉(zhuǎn)換到在窗口中的位置為止。我們稱這種以父視圖坐標(biāo)系為原點(diǎn)進(jìn)行定位的位置稱為邊距,也就是離父視圖邊緣的距離。

  • 第三種以兄弟視圖坐標(biāo)系作為參照來確定的位置稱為偏移位置,子視圖的位置是在關(guān)聯(lián)的兄弟視圖的位置的基礎(chǔ)之上的一個(gè)偏移值。比如A視圖在B視圖的右邊偏移5個(gè)點(diǎn),則表示為A視圖的左邊距離B視圖的右邊5個(gè)點(diǎn)的距離。我們稱這種坐標(biāo)體系下的位置為間距,也就是指定的是視圖之間的距離作為視圖的位置。采用間距的方式進(jìn)行定位只適合于同一個(gè)父視圖之間的兄弟視圖之間的定位方式。

    各種坐標(biāo)系下的定位值

上面的三種定位方式各有優(yōu)缺點(diǎn),我們可以在實(shí)際中結(jié)合各種定位方式來完成視圖的位置設(shè)定。

上面我們介紹了定位時(shí)位置所基于的坐標(biāo)系,因?yàn)橐晥D并不是一個(gè)點(diǎn)而是一個(gè)矩形區(qū)塊,所以我們必須要明確的是視圖本身這個(gè)區(qū)塊的哪個(gè)點(diǎn)來進(jìn)行位置的設(shè)定。 在這里我們就要介紹視圖內(nèi)的坐標(biāo)系。我們知道視圖是一個(gè)矩形的區(qū)域,里面由無數(shù)個(gè)點(diǎn)構(gòu)成。假如我們以視圖左上角作為坐標(biāo)原點(diǎn)的話,那么視圖內(nèi)的任何一點(diǎn)都可以用水平方向的坐標(biāo)值和垂直方向的坐標(biāo)值來表示。對于水平方向的坐標(biāo)值來說最左邊位置的點(diǎn)的坐標(biāo)值是0,最右邊位置的點(diǎn)的坐標(biāo)值是視圖的寬度,中間位置的坐標(biāo)點(diǎn)的值是寬度的一半,對于垂直方向的坐標(biāo)值來說最上邊位置的點(diǎn)的坐標(biāo)值是0,最下邊位置的點(diǎn)的坐標(biāo)值是視圖的高度,中間位置的坐標(biāo)點(diǎn)的值是高度的一半。我們稱這幾個(gè)特殊的坐標(biāo)點(diǎn)為方位。因此一個(gè)視圖一共有9個(gè)方位點(diǎn)分別是:左上、左中、左下、中上、中中、中下、右上、右中、右下。

視圖的九個(gè)方位

通過對方位點(diǎn)的定義,我們就不再需要去關(guān)心這些點(diǎn)的具體的坐標(biāo)值了,因?yàn)樗枋隽艘晥D的某個(gè)特定的部位。而為了方便計(jì)算和處理,我們一般只需要指出視圖內(nèi)某個(gè)方位點(diǎn)在參照視圖的坐標(biāo)系里面的水平坐標(biāo)軸和垂直坐標(biāo)軸中的位置就可以完成視圖的位置定位了,因?yàn)橹灰_定了這個(gè)方位點(diǎn)的在參照視圖坐標(biāo)系里面的位置,就可以計(jì)算出這個(gè)視圖內(nèi)的任意的一個(gè)點(diǎn)在參照視圖坐標(biāo)軸里面的位置。所謂的位置定位就是把一個(gè)視圖內(nèi)坐標(biāo)系的某個(gè)點(diǎn)的坐標(biāo)值映射為參照視圖坐標(biāo)系里面的坐標(biāo)值的過程

視圖的坐標(biāo)轉(zhuǎn)換

iOS中UIView提供了一個(gè)屬性centercenter屬性的意義就是定義視圖內(nèi)中心點(diǎn)這個(gè)方位在父視圖坐標(biāo)系中的坐標(biāo)值。UIView還提供一個(gè)屬性frameframe屬性的意義則是用來描述視圖左上角這個(gè)方位在父視圖坐標(biāo)系中的坐標(biāo)值和在父視圖中的顯示尺寸(坐標(biāo)變換后除外)。 我們再來考察一下UIView的bounds屬性,上面的章節(jié)中我們有介紹bounds中的size部分用來描述一個(gè)視圖的尺寸,而origin部分又是用來描述什么呢? 我們知道在左手坐標(biāo)系里面,一個(gè)視圖內(nèi)的左上角方位的坐標(biāo)值就是原點(diǎn)的坐標(biāo)值,默認(rèn)情況下原點(diǎn)的坐標(biāo)值是(0,0)。但是這個(gè)定義不是一成不變的,也就是說原點(diǎn)的坐標(biāo)值不一定是(0,0)。一個(gè)視圖bounds里面的origin部分所表達(dá)的意義就是視圖自身坐標(biāo)系左上角原點(diǎn)方位的坐標(biāo)值。這個(gè)值的設(shè)定將會影響到里面所有子視圖的定位和顯示。假如我們設(shè)置某個(gè)視圖的bounds.origin為(0,-64)時(shí),那么表示視圖左上角x軸的坐標(biāo)原點(diǎn)值是0,而左上角y軸的坐標(biāo)原點(diǎn)值是-64,當(dāng)這個(gè)視圖內(nèi)的某個(gè)子視圖的frame屬性的origin為(0,0)時(shí)這個(gè)子視圖的左上角的x軸部分和視圖原點(diǎn)x軸一致,而y軸部分則會往下偏移64個(gè)單位。 可以看出bounds屬性是對視圖自身的原點(diǎn)坐標(biāo)值和尺寸的描述,不會受到其他視圖以及坐標(biāo)變換的影響。 我們還可以通過下面的公式得出一個(gè)視圖內(nèi)9個(gè)方位(再次強(qiáng)調(diào)方位的概念是一個(gè)視圖內(nèi)的坐標(biāo)點(diǎn)的位置)的坐標(biāo)值:

左上方位 = (A.bounds.origin.x, A.bounds.origin.y)
左中方位 = (A.bounds.origin.x,  A.bounds.origin.y + A.bounds.size.height / 2)
左下方位 = (A.bounds.origin.x, A.bounds.origin.y + A.bounds.size.height)
中上方位 = (A.bounds.origin.x + A.bounds.size.width/2, A.bounds.origin.y)
中中方位 = (A.bounds.origin.x + A.bounds.size.width/2, A.bounds.origin.y + A.bounds.size.height/2)
中下方位 = (A.bounds.origin.x + A.bounds.size.width/2, A.bounds.origin.y + A.bounds.size.height)
右上方位 = (A.bounds.origin.x + A.bounds.size.width, A.bounds.origin.y)
右中方位 = (A.bounds.origin.x + A.bounds.size.width,A.bounds.origin.y + A.bounds.size.height/2)
右下方位 = (A.bounds.origin.x + A.bounds.size.width,A.bounds.origin.y + A.bounds.size.height)

對于位置定義來說TangramKit中的TGLayoutPos類就是一個(gè)對位置進(jìn)行建模的類。TGLayoutPos類同時(shí)支持采用父視圖作為參考系和以兄弟視圖作為參考系的定位方式,這可以通過為其中的equal方法設(shè)置不同類型的值來決定其定位方式。為了實(shí)現(xiàn)視圖定位我們也為UIView擴(kuò)展出了3個(gè)水平方位的屬性:tg_left, tg_centerX,tg_right來表示左中右三個(gè)方位對象。3垂直方位的屬性:tg_top, tg_centerY,tg_bottom來表示上、中、下三個(gè)方位。這6個(gè)方位對象將比原生的center屬性提供更加強(qiáng)大和豐富的位置定位能力。

iOS系統(tǒng)的原生布局體系里面是通過bounds屬性和center屬性來進(jìn)行視圖的尺寸設(shè)置和位置設(shè)置的。bounds用來指定視圖內(nèi)的左上角方位的坐標(biāo)值,以及視圖的尺寸,而center則用來指定視圖的中心點(diǎn)方位在父視圖這個(gè)坐標(biāo)體系里面的坐標(biāo)值。為了簡化設(shè)置UIView提供了一個(gè)簡易的屬性frame可以用來直接設(shè)置一個(gè)視圖的尺寸和位置,frame中的origin部分指定視圖左上角方位在父視圖坐標(biāo)系里面的坐標(biāo)值,而size部分則指定了視圖本身的尺寸frame屬性并不是一個(gè)實(shí)體屬性而是一個(gè)計(jì)算類型的屬性,在我們沒有對視圖進(jìn)行坐標(biāo)變換時(shí)(視圖的transform未設(shè)置時(shí))我們可以得到如下的frame屬性的偽代碼實(shí)現(xiàn):

public var frame:CGRect
{
   get {
       let x = self.center.x  - self.bounds.size.width / 2
       let y = self.center.y  - self.bounds.size.height / 2
       let width = self.bounds.size.width
       let height = self.bounds.size.height
       return CGRect(x:x, y:y, width:width, height:height) 
  }
  set {
       self.center = CGPoint(x:newValue.origin.x  +  newValue.size.width / 2, y: newValue.origin.y +  newValue.size.height / 2)
       self.bounds.size  = newValue.size
  }
}

綜上所述,我們可以看出,所謂視圖布局的核心,就是確定一個(gè)視圖的尺寸,和確定視圖在參考視圖坐標(biāo)系里面的坐標(biāo)位置。為了靈活處理和計(jì)算,視圖的尺寸可以設(shè)置為絕對值類型,也可以設(shè)置為相對值類型,也可以設(shè)置為特殊的包裹或者填充值類型;視圖的位置則可以指定視圖中的任意的方位,以及設(shè)置這個(gè)方位的點(diǎn)在窗口坐標(biāo)系或者父視圖坐標(biāo)系或者兄弟坐標(biāo)系中的坐標(biāo)值。正是提供的這些多樣的設(shè)置方式,我們就可以在不同的場景中使用不同的設(shè)置來完成各種復(fù)雜界面的布局。

Android的布局體系

屏幕尺寸、PPI、DPI

布局框架結(jié)構(gòu)

layout布局文件。

5大布局類

...敬請期待

HTML/CSS的布局體系

CSS定位方式

浮動float

flex-box bootstrap

...敬請期待

iOS布局體系

frame,bounds,center

XIB和storyboard

AutoLayout和SizeClass

...敬請期待

TangramKit布局框架

在您不了解TangramKit之前,可以先通過下面一個(gè)例子來感受和體驗(yàn)一下TangramKit的布局構(gòu)建語法:

  • 有一個(gè)容器視圖S的寬度是100而高度則等于由四個(gè)從上到下依次排列的子視圖A,B,C,D的高度總和。
  • 子視圖A的左邊距占用父視圖寬度的20%,而右邊距則占用父視圖寬度的30%,高度則等于自身的寬度。
  • 子視圖B的左邊距是40,寬度則占用父視圖的剩余寬度,高度是40。
  • 子視圖C的寬度占用父視圖的所有寬度,高度是40。
  • 子視圖D的右邊距是20,寬度是父視圖寬度的50%,高度是40。
演示效果圖

代碼實(shí)現(xiàn)如下:

    let S = TGLinearLayout(.vert)
    S.tg_vspace = 10
    S.tg_width.equal(100)
    S.tg_height.equal(.wrap)

    let A = UIView()
    A.tg_left.equal(20%)
    A.tg_right.equal(30%)
    A.tg_height.equal(A.tg_width)
    S.addSubview(A)

    let B = UIView()
    B.tg_left.equal(40)
    B.tg_width.equal(.fill)
    B.tg_height.equal(40)
    S.addSubview(B)

    let C = UIView()
    C.tg_width.equal(.fill)
    C.tg_height.equal(40)
    S.addSubview(C)

    let D = UIView()
    D.tg_right.equal(20)
    D.tg_width.equal(50%)
    D.tg_height.equal(40)
    S.addSubview(D)

因?yàn)門angramKit對布局位置類和布局尺寸類的方法重載了運(yùn)算符:~=、>=、<=、+=、-=、*=、/= 所以您可以用更加簡潔的代碼進(jìn)行編寫:

    let S = TGLinearLayout(.vert)
    S.tg_vspace = 10
    S.tg_width ~=100
    S.tg_height ~=.wrap

    let A = UIView()
    A.tg_left ~=20%
    A.tg_right ~=30%
    A.tg_height ~=A.tg_width
    S.addSubview(A)

    let B = UIView()
    B.tg_left ~=40
    B.tg_width ~=.fill
    B.tg_height ~=40
    S.addSubview(B)

    let C = UIView()
    C.tg_width ~=.fill
    C.tg_height ~=40
    S.addSubview(C)

    let D = UIView()
    D.tg_right ~=20
    D.tg_width ~=50%
    D.tg_height ~=40
    S.addSubview(D)

通過上面的代碼,您可以看出用TangramKit實(shí)現(xiàn)的布局代碼和上面場景描述文本幾乎相同,非常的利于閱讀和理解。那么這些系統(tǒng)又是如何實(shí)現(xiàn)的呢?

實(shí)現(xiàn)原理

我們知道在對任何一個(gè)視圖進(jìn)行布局時(shí),最終都是通過設(shè)置視圖的尺寸和視圖的位置來完成的。在iOS中我們可以通過UIView的bounds屬性來完成視圖的尺寸設(shè)置,而通過center屬性來完成視圖的位置設(shè)置。為了進(jìn)行簡單的操作,系統(tǒng)提供了frame這個(gè)屬性來簡化對尺寸和位置的設(shè)置。這個(gè)過程不管是原始的方法還是后續(xù)的AutoLayout其實(shí)現(xiàn)的最終機(jī)制都是一致的。每當(dāng)一個(gè)視圖的尺寸改變或者要求重新布局時(shí),系統(tǒng)都會調(diào)用視圖的方法:

open func layoutSubviews()

而我們可以在UIView的派生類中重載上面的方法來實(shí)現(xiàn)對這個(gè)視圖里面的所有子視圖的重新布局,至于如何布局子視圖則是需要根據(jù)應(yīng)用場景而定。在編程時(shí)我們經(jīng)常會用到一些視圖,這種視圖只是負(fù)責(zé)將里面的子視圖按照某種規(guī)則進(jìn)行排列和布局,而別無其他的作用。因此我們稱這種視圖為容器視圖或者稱為布局視圖。TangramKit框架對種視圖進(jìn)行了建模而提供了一個(gè)從UIView派生的布局視圖基類TGBaseLayout。這個(gè)類的作用就是專門負(fù)責(zé)對加入到其中的所有子視圖進(jìn)行布局排列,它是通過重載layoutSubviews方法來完成這個(gè)工作的。剛才我們說過如何排列容器視圖中的子視圖是要根據(jù)具體的應(yīng)用場景而定, 比如有可能是所有子視圖從上往下按照添加的順序依次排列,或者子視圖按照某種約束依賴關(guān)系來進(jìn)行布局排列,或者子視圖需要多行多列的排列等等。因此我們對常見的布局應(yīng)用場景進(jìn)行了抽象,通過建立不同的TGBaseLayout的派生類來實(shí)現(xiàn)不同的布局處理:

  • 線性布局TGLinearLayout:線性布局里面的所有子視圖都按照添加的順序依次從上到下或者依次從左到右進(jìn)行排列。根據(jù)排列的方向可以分為垂直線性布局和水平線性布局。線性布局和iOS9上的UIStackView以及Android中的線性布局LinearLayout提供一樣的功能。

  • 框架布局TGFrameLayout: 框架布局里面的所有子視圖布局時(shí)和添加的順序無關(guān),而是按照設(shè)定的位置停靠在布局視圖的:左上、左中、左下、中上、中中、中下、右上、右中、右下、填充這個(gè)10個(gè)方位中的任何一個(gè)位置上。框架布局里面的子視圖只跟框架布局視圖的邊界建立約束關(guān)系。框架布局和Android中的框架布局FrameLayout提供一樣的功能。

  • 表格布局TGTableLayout:表格布局里面的子視圖可以進(jìn)行多行多列的排列。在使用時(shí)要先添加行,然后再在行里面添加列,每行的列數(shù)可以隨意確定。因?yàn)楸砀癫季质蔷€性布局TGLinearLayout的派生類,所以表格布局也分為垂直表格布局和水平表格布局。垂直表格布局中的行是從上到下,而列則是從左到右排列;水平表格布局中的行是從左到右,而列是從上到下排列的。表格布局和Android中的表格布局TableLayout以及HTML中的table,tr,td元素提供一樣的功能。

  • 相對布局TGRelativeLayout: 相對布局里面的子視圖和添加的順序無關(guān),而是按照子視圖之間設(shè)定的尺寸約束依賴和位置約束依賴進(jìn)行布局排列。因此相對布局里面的所有子視圖都要設(shè)置位置和尺寸的約束和依賴關(guān)系。相對布局和iOS的AutoLayout以及Android中的相對布局RelativeLayout提供一樣的功能。

  • 流式布局TGFlowLayout: 流式布局里面的子視圖按照添加的順序依次從某個(gè)方向排列,而當(dāng)遇到了這個(gè)方向上的排列數(shù)量限制或者容器的尺寸限制后將會另起一行,而重新按照原先的方向依次排列。最終這個(gè)布局中的子視圖將形成多行多列的排列展示。流式布局和線性布局的區(qū)別是,線性布局只是單行或者單列的,而流式布局則是多行多列。流式布局和表格布局的區(qū)別是,表格布局有明確行的概念,在使用前要添加行再添加列,而流式布局則沒有明確行的概念,由布局自動生成行和列。根據(jù)排列的方向和限制的規(guī)則,流式布局分為垂直數(shù)量約束布局、垂直內(nèi)容約束布局、水平數(shù)量約束布局、水平內(nèi)容約束布局四種布局。流式布局實(shí)現(xiàn)了HTML/CSS3中的flex-box的子集的功能。

  • 浮動布局TGFloatLayout:浮動布局里面的子視圖按照添加的順序,并且按照每個(gè)子視圖自身設(shè)定的浮動規(guī)則向某個(gè)方向進(jìn)行浮動停靠。當(dāng)子視圖的尺寸無法容納到布局視圖的剩余空間時(shí),則會自動尋找一個(gè)能夠容納自身尺寸的最佳位置進(jìn)行浮動停靠。浮動布局里面的子視圖并不是有規(guī)則的多行多列的排列。根據(jù)子視圖可以浮動的方向浮動布局分為垂直浮動布局和水平浮動布局。浮動布局和HTML/CSS中的float定位實(shí)現(xiàn)了相同的功能。

  • 路徑布局TGPathLayout: 路徑布局里面的子視圖按照一個(gè)提供的數(shù)學(xué)函數(shù)得到的曲線路徑等距離的根據(jù)添加的順序依次排列。所有的子視圖的位置都是根據(jù)函數(shù)曲線中距離相等的點(diǎn)而確定的。路徑布局提供了直角坐標(biāo)系、參數(shù)方式、極坐標(biāo)系三種曲線的構(gòu)建方法。路徑布局是TangramKit中的獨(dú)有的一種布局。

上述的7個(gè)派生類分別的實(shí)現(xiàn)了大部分的不同的應(yīng)用場景。在每個(gè)派生類的layoutSubviews的實(shí)現(xiàn)中都按照描述的規(guī)則來設(shè)置子視圖的尺寸bounds和位置center屬性。也就是說最終的子視圖的尺寸和位置是在布局視圖中的layoutSubviews中進(jìn)行設(shè)置的。那么我們就必須要提供另外一套子視圖的布局尺寸和布局位置的設(shè)置方法,以便在布局視圖布局時(shí)將子視圖設(shè)置好的布局尺寸和布局位置轉(zhuǎn)化為真實(shí)的視圖尺寸和視圖位置。為此TangramKit專門提供了一個(gè)視圖的布局尺寸類TGLayoutSize用來進(jìn)行子視圖的布局尺寸的設(shè)置,一個(gè)視圖的布局位置類TGLayoutPos用來進(jìn)行子視圖的布局位置的設(shè)置。我們對UIView建立了一個(gè)extension。分別擴(kuò)展出了2個(gè)布局尺寸對象和6個(gè)布局位置對象:


extension UIView
{
  
   //左邊位置
  var tg_left:TGLayoutPos{get}
   //上邊位置   
  var tg_top:TGLayoutPos{get}
   //右邊位置    
  var tg_right:TGLayoutPos{get}
   //下邊位置    
  var tg_bottom:TGLayoutPos{get}
   //水平中心點(diǎn)位置    
  var tg_centerX:TGLayoutPos{get}
   //垂直中心點(diǎn)位置    
  var tg_centerY:TGLayoutPos{get}
    
  //寬度尺寸
  var tg_width:TGLayoutSize{get}
  //高度尺寸    
  var tg_height:TGLayoutSize{get}
}

也就是說我們將不再直接設(shè)置子視圖的boundscenter(這兩個(gè)屬性只會在布局視圖中的layoutSubviews中設(shè)置)屬性了,而是直接操作UIView擴(kuò)展出來的布局位置對象和布局尺寸對象。如果把布局視圖的layoutSubviews比作一個(gè)數(shù)學(xué)函數(shù)的話,那么我們就能得到如下的方程式:

UIView.center = TGXXXLayout.layoutSubviews(UIView.tg_left, UIView.tg_top, UIView.tg_right, UIView.tg_bottom,UIView.tg_centerX,UIView.tg_centerY)
UIView.bounds = TGXXXLayout.layoutSubviews(UIView.tg_width, UIView.tg_height)

因此我們可以看出不同的TGBaseLayout的派生類因?yàn)槔锩娴牟季址椒ú幌嗤鴮?dǎo)致子視圖的位置和尺寸的計(jì)算方法不同,從而得到了我們想要的效果。那么為什么要用6個(gè)布局位置對象和2個(gè)布局尺寸對象來設(shè)置子視圖的位置和尺寸而不直接用boundscenter呢? 原因在于bounds和center只提供了有限的設(shè)置方法而布局位置對象和布局尺寸對象則提供了功能更加強(qiáng)大的設(shè)置方法,而這些方法又可以簡化我們的編程,以及可以很方便的適配各種不同尺寸的屏幕。(還記得我們上面的例子里面,尺寸和位置可以設(shè)置為數(shù)值,.wrap, .fill,以及百分比的值嗎?)。

TangramKit為了存儲這些擴(kuò)展的布局位置和布局尺寸對象,內(nèi)部是使用了objc的runtime機(jī)制提供的動態(tài)屬性創(chuàng)建的方法:

public func objc_getAssociatedObject(_ object: Any!, _ key: UnsafeRawPointer!) -> Any!

系統(tǒng)通過這個(gè)方法來關(guān)聯(lián)視圖對象的那6個(gè)布局位置和2個(gè)布局尺寸對象。

上面的代碼中我們看到了布局容器視圖通過layoutSubviews方法來實(shí)現(xiàn)對子視圖的重新布局。而且也提到了當(dāng)容器視圖的尺寸發(fā)生變化時(shí)也會激發(fā)對layoutSubviews的調(diào)用。除了自動激發(fā)外,我們可以通過手動調(diào)用布局視圖的setNeedLayout方法來實(shí)現(xiàn)布局視圖的layoutSubviews調(diào)用。當(dāng)我們在設(shè)置子視圖的布局位置和布局尺寸時(shí),系統(tǒng)內(nèi)部會在設(shè)置完成后調(diào)用布局視圖的setNeedLayout的方法,因此只要對子視圖的布局位置和布局尺寸進(jìn)行設(shè)置都會重新激發(fā)布局視圖的布局視圖。那么對子視圖的frame,bounds,center真實(shí)位置和尺寸的改變呢?我們也要激發(fā)布局視圖的重新布局。為了解決這個(gè)問題,我們引入了KVO的機(jī)制。布局視圖在添加子視圖時(shí)會監(jiān)聽加入到其中的子視圖的frame,bounds,center的變化,并在其變化時(shí)調(diào)用布局視圖的setNeedLayout來激發(fā)布局視圖的重新布局。我們知道每次當(dāng)一個(gè)視圖調(diào)用addSubview添加子視圖時(shí)都會激發(fā)調(diào)用者的方法:didAddSubview。為了實(shí)現(xiàn)對子視圖的變化的監(jiān)控,布局視圖重載了這個(gè)方法并對子視圖的isHidden,frame,center進(jìn)行監(jiān)控:

 override open func didAddSubview(_ subview: UIView) {
        super.didAddSubview(subview)
        
        subview.addObserver(self, forKeyPath:"isHidden", options: NSKeyValueObservingOptions.new, context: nil)
        subview.addObserver(self, forKeyPath:"frame", options: NSKeyValueObservingOptions.new, context: nil)
        subview.addObserver(self, forKeyPath:"center", options: NSKeyValueObservingOptions.new, context: nil)

    }
    
    override open func willRemoveSubview(_ subview: UIView) {
        super.willRemoveSubview(subview)        
        subview.removeObserver(self, forKeyPath: "isHidden")
        subview.removeObserver(self, forKeyPath: "frame")
        subview.removeObserver(self, forKeyPath: "center")

    }

當(dāng)子視圖的frame或者center變更時(shí),將會激發(fā)布局視圖的重新布局。上面曾經(jīng)說過,在布局視圖重新布局子視圖時(shí)最終會調(diào)整子視圖的bounds和center.那么這樣就有可能會形成循環(huán)的重新布局,為了解決這種循環(huán)遞歸的情況,布局視圖在layoutSubviews調(diào)用進(jìn)行布局前設(shè)置了一個(gè)布局中的標(biāo)志,而在所有子視圖布局完成后將恢復(fù)這個(gè)布局中的標(biāo)志。因此當(dāng)我們布局視圖通過KVO監(jiān)控到子視圖的位置和尺寸變化時(shí),則會判斷那個(gè)布局中的標(biāo)志,如果當(dāng)前是在布局中則不會再次激發(fā)布局視圖的重新布局,從而防止了死循環(huán)的發(fā)生。

這就是TangramKit布局實(shí)現(xiàn)的原理,下面的圖表列出了TangramKit的整個(gè)布局框架的類體系結(jié)構(gòu):

TangramKit布局框架體系架構(gòu)

布局位置類和布局尺寸類

在前面的介紹布局核心的章節(jié)以及布局實(shí)現(xiàn)原理的章節(jié)里面我們有說道布局位置類和布局尺寸類。之所以系統(tǒng)不直接操作視圖的bounds和center屬性而是通過擴(kuò)展視圖的2個(gè)布局尺寸屬性和6個(gè)布局位置屬性來進(jìn)行子視圖的布局設(shè)置。原因是后者能夠提供豐富和多樣的設(shè)置。而且我們在編程時(shí)也不再需要通過設(shè)置視圖的frame來實(shí)現(xiàn)布局了,即使設(shè)置也可能會失效。

比重類TGWeight

TGWeight類的值表示尺寸或者位置的大小是父布局視圖的尺寸或者剩余空間的尺寸的比例值,也就是說值的大小依賴于父布局視圖的尺寸或者剩余空間的尺寸的大小而確定,這樣子視圖就不需要明確的指定位置和尺寸的大小了,非常適合那些需要適配屏幕的尺寸和位置的場景。 至于是父視圖的尺寸還是父視圖剩余空間的尺寸則要根據(jù)其所在的布局視圖的上下文而確定。比如:

//假如A,b是在一個(gè)垂直線性布局下的子視圖
A.tg_width.equal(TGWeight(20))   //A的寬度是父布局視圖寬度的20%
A.tg_height.equal(TGWeight(30))  //A的高度是父布局視圖剩余高度的30%
B.tg_left.equal(TGWeight(40))  //B的左邊距是父視圖寬度的40%
B.tg_top.equal(TGWeight(10))  //B的頂部間距時(shí)父視圖的剩余高度的10%

為了簡化和更加直觀的表示比重類型的值,我們重載%運(yùn)算符,這樣上面的代碼就可以簡寫為如下更加直觀的方式:

//假如A是在一個(gè)垂直線性布局下的子視圖
A.tg_width.equal(20%)   //A的寬度是父布局視圖寬度的20%
A.tg_height.equal(30%)  //A的高度是父布局視圖剩余高度的30%
B.tg_left.equal(40%)  //B的左邊距是父視圖寬度的40%
B.tg_top.equal(10%)  //B的頂部間距時(shí)父視圖的剩余高度的10%

下面的列表中列出了在各種布局下視圖的尺寸和位置的TGWeight類型值所代表的意義:

為了表示方便,我們把:

  • 線性布局簡稱L
    • 垂直線性布局簡稱為LV
    • 水平線性布局簡稱為LH
  • 框架布局簡稱為FR
  • 垂直表格布局簡稱為TV
  • 水平表格布局簡稱為TH
  • 相對布局簡稱為R
  • 浮動布局簡稱FO
  • 流式布局FL
  • 路徑布局簡稱P
  • 布局視圖的非布局父視圖S
  • 所有布局簡稱ALL
位置尺寸\類型 父視圖尺寸 父視圖剩余空間尺寸
tg_left LV/FR/S/TH LH/TV
tg_top LH/FR/S/TV LV/TH
tg_right LV/FR/S/TH LH/TV
tg_bottom LH/FR/S/TV LV/TH
tg_centerX LV/FR/TH -
tg_centerY LH/FR/TV -
tg_width LV/FR/S/R/TH/P LH/TV/FO/FL
tg_height LH/FR/S/R/TV/P LV/TH/FO/FL

布局尺寸類TGLayoutSize

布局尺寸類用來描述視圖布局核心中的視圖尺寸。我們對UIView擴(kuò)展出了2個(gè)布局尺寸對象 :

    public var tg_width:TGLayoutSize
    public var tg_height:TGLayoutSize

分別用來實(shí)現(xiàn)視圖的寬度和高度的布局尺寸設(shè)置。在TGLayoutSize類中,我們可以通過方法equal來設(shè)置視圖尺寸的多種類型的值,類中是通過重載equal方法來實(shí)現(xiàn)多種類型的值的設(shè)置的。


    public func equal(_ size:CGFloat, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func equal(_ weight:TGWeight, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func equal(_ array:[TGLayoutSize], increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func equal(_ view:UIView,increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func equal(_ dime:TGLayoutSize!, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize

上面的方法中我們可以通過equal方法來設(shè)置:

  • CGFloat類型的值表示視圖的尺寸是一個(gè)絕對值類型的尺寸值。比如:
A.tg_width.equal(100)  //A的寬度為100
A.tg_height.equal(200) //A的高度為200
  • TGWeight類型的值表示視圖的尺寸是一個(gè)依賴于父視圖尺寸的相對比例值。(具體見上面TGWeight類型值的定義和使用)
//假如A是在一個(gè)垂直線性布局下的子視圖
A.tg_width.equal(20%)   //A的寬度是父布局視圖寬度的20%
A.tg_height.equal(30%)  //A的高度是父布局視圖剩余高度的30%
  • TGLayoutSize類型的值表示視圖的尺寸和另外一個(gè)尺寸對象的值相等,這也是一種相對值類型的尺寸值,通過設(shè)置這種尺寸的依賴我們就可以不必要明確的指定一個(gè)具體的值,而是會隨著所以依賴的尺寸變化而變化。設(shè)置為TGLayoutSize類型的值通常用于在相對布局中的子視圖,當(dāng)然也可以在其他類型的布局中使用。下面是一個(gè)展示的例子:
  A.tg_width.equal(B.tg_width)  //A的寬度等于B的寬度
  A.tg_height.equal(A.tg_width)  //A的高度等于A的寬度
  • UIView類型的值其實(shí)就是TGLayoutSize的簡化版本設(shè)置,表示某個(gè)維度的尺寸值等于指定視圖的相同維度的尺寸值。比如:
   A.tg_width.equal(B)   //表示A視圖的寬度等于B視圖的寬度
   A.tg_height.equal(A.superview)  //表示A視圖的高度等于父視圖的高度。
  • [TGLayoutSize]數(shù)組類型的值,只用在相對布局里面的子視圖設(shè)置才有意義,其他的類型的布局中設(shè)置這種類型的值無效。他表示子視圖的尺寸和數(shù)組里面的所有子視圖來等分父布局視圖的尺寸。比如:
//A,B,C,D都是相對布局視圖里面的子視圖,我們希望A,B,C,D這四個(gè)子視圖來均分父視圖的寬度,這樣A,B,C,D都不需要明確的指定寬度了。
A.tg_width.equal([B.tg_width, C.tg_width, D.tg_width])
A.tg_width.equal(B.tg_width)  //A和B的寬度相等
A.tg_width.equal([B.tg_width]) //A和B的寬度相等并且平分布局視圖的寬度,也就是A,B的寬度都是布局視圖的寬度的一半
  • 特殊類型的值。為了簡化尺寸的設(shè)置我們定義了三種特殊類型的尺寸值:

    • wrap: 他表示尺寸的值由布局視圖的所有子視圖的尺寸或者由子視圖的內(nèi)容包裹而成。也就是尺寸的大小是由子視圖或者視圖的內(nèi)容共同決定的,這樣視圖的尺寸將依賴其內(nèi)部的子視圖的尺寸或者子視圖內(nèi)容的大小。
  • fill: 他表示視圖的尺寸的值將會填充滿父視圖的剩余空間,也就是說視圖的尺寸值是依賴于父視圖的尺寸的大小。

  • average:他表示視圖的尺寸將和其兄弟視圖一起來均分父視圖的尺寸,這樣所有兄弟視圖的尺寸都將相等。

下面是這三個(gè)特殊值使用的例子:

A.tg_width.equal(.wrap)  //A視圖的寬度由里面的所有子視圖或者內(nèi)容包裹而確定。
A.tg_height.equal(.fill)   //A視圖的高度填充滿父視圖的剩余高度空間。
B.tg_width.equal(.average)  //B視圖的寬度將會和其他兄弟視圖均分父視圖的寬度。

上面列出了布局尺寸類中的equal方法可以設(shè)置的值的類型,我們還看到了方法中存在著另外兩個(gè)默認(rèn)的參數(shù):increment 和multiple 這兩個(gè)參數(shù)的意義表示在尺寸等于上述類型的值的基礎(chǔ)上的增量值和倍數(shù)值。增量值默認(rèn)是0,而倍數(shù)值則默認(rèn)是1。比如某個(gè)子視圖的寬度等于另外一個(gè)子視圖的寬度值加20的時(shí),可以通過equal方法設(shè)置如下:

   A.tg_width.equal(B.tg_width, increment:20)  //A的寬度等于B的寬度加20

除了可以在equal方法中指定增量值外,布局尺寸類還單獨(dú)提供一個(gè)add方法來實(shí)現(xiàn)增量值的設(shè)置:

    public func add(_ val:CGFloat) ->TGLayoutSize

這樣上述的代碼也可以用如下的方式設(shè)置:

A.tg_width.equal(B.tg_width).add(20)

在equal方法中的multiple值則是指定尺寸等于另外一個(gè)尺寸的倍數(shù)值。比如某個(gè)子視圖的高度等于另外一個(gè)子視圖的高度的一半時(shí),可以通過equal方法設(shè)置如下:

A.tg_height.equal(B.tg_height, multiple:0.5);  //A的高度等于B的高度的一半。

除了可以在equal方法中指定倍數(shù)值外,布局尺寸類還單獨(dú)提供一個(gè)multiply方法來實(shí)現(xiàn)倍數(shù)值的設(shè)置:

    public func multiply(_ val:CGFloat) ->TGLayoutSize

這樣上述的代碼也可以用如下的方式設(shè)置:

  A.tg_height.equal(B.tg_height).multiply(0.5)

在布局尺寸類中我們除了可以用equal, add, multiply 方法來設(shè)置視圖的尺寸依賴值以及增量和倍數(shù)外,我們還可以對視圖尺寸的最大最小值進(jìn)行控制處理。比如在實(shí)踐中我們希望某個(gè)視圖的寬度等于另外一個(gè)兄弟視圖的寬度,但是最小不能小于20,而最大則不能超過父視圖的寬度的一半。 這時(shí)候我們就需要用到布局尺寸類的另外兩個(gè)方法了:

    public func min(_ size:CGFloat, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func min(_ dime:TGLayoutSize!, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func min(_ view:UIView, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func max(_ size:CGFloat, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func max(_ view:UIView, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize
    public func max(_ dime:TGLayoutSize!, increment:CGFloat = 0, multiple:CGFloat = 1) ->TGLayoutSize

上述的兩個(gè)方法min,max分別用來設(shè)置視圖尺寸最小不能小于的值以及最大不能超過的值。方法中我們可以看出最大最小值除了可以設(shè)置具體的數(shù)值外還可以設(shè)置為另外一個(gè)布局尺寸對象,同樣我們還可以設(shè)置增量和倍數(shù)值。因此我們可以通過對minmax方法的使用來解決上述的問題:


  //A的寬度等于B的寬度,最小為20,最大為父視圖寬度的一半。
  A.tg_width.equal(B.tg_width).min(20).max(A.superview,multiple:0.5)

最后我們列出視圖的擴(kuò)展屬性tg_width, tg_height在各布局視圖下equal方法能夠設(shè)置的值的類型,我們這里設(shè)置B為一個(gè)兄弟視圖,S為父視圖

屬性/值 CGFloat/TGWeight/wrap/fill A.tg_width A.tg_height B.tg_width B.tg_height S.tg_width S.tg_height [TGLayoutSize]
A.tg_width ALL - FR/R/FLH/FO FR/R/FO/P R ALL R R
A.tg_height ALL FR/R/FLV/FO/LV - R FR/R/FO/P R ALL R

布局位置類TGLayoutPos

布局位置類用來描述視圖布局核心中的視圖的位置。我們對UIView擴(kuò)展出了6個(gè)布局位置對象:

    public var tg_left:TGLayoutPos       //視圖左邊布局位置
    public var tg_top:TGLayoutPos      //視圖上邊布局位置
    public var tg_right:TGLayoutPos   //視圖右邊布局位置
    public var tg_bottom:TGLayoutPos  //視圖下邊布局位置
    public var tg_centerX:TGLayoutPos  //視圖水平中心點(diǎn)布局位置
    public var tg_centerY:TGLayoutPos   //視圖垂直中心點(diǎn)布局位置

分別用來實(shí)現(xiàn)視圖的水平維度的左、中、右三個(gè)方位以及視圖垂直維度的上、中、下三個(gè)方位的布局位置設(shè)置。在TGLayoutPos類中,我們可以通過方法equal來設(shè)置視圖位置的多種類型的值,類中是通過重載equal方法來實(shí)現(xiàn)多種類型的值的設(shè)置的。

    public func equal(_ origin:CGFloat, offset:CGFloat = 0) ->TGLayoutPos
    public func equal(_ weight:TGWeight, offset:CGFloat = 0) ->TGLayoutPos
    public func equal(_ array:[TGLayoutPos], offset:CGFloat = 0) ->TGLayoutPos
    public func equal(_ view: UIView, offset:CGFloat = 0) ->TGLayoutPos
    public func equal(_ pos:TGLayoutPos!, offset:CGFloat = 0) ->TGLayoutPos

我們可以通過上面定義的equal方法來設(shè)置:

  • CGFloat類型的值表示視圖的位置是一個(gè)絕對值類型的位置值。 比如:
    A.tg_left.equal(10)          //A視圖的左邊位置是10
    A.tg_right.equal(20)        //A視圖的右邊位置是20
    A.tg_centerX.equal(5)     //A視圖的水平中心點(diǎn)的偏移位置是5

我們知道在視圖定位時(shí)位置的概念根據(jù)參考坐標(biāo)系不同而不同:

  • 定位的值如果是以父視圖作為參考系坐標(biāo)那么視圖的位置就叫做邊距 ,邊距描述的是視圖距離父視圖的距離。
  • 定位的值如果是以兄弟視圖作為參考系坐標(biāo)那么視圖的位置就叫做間距,間距描述的是視圖距離兄弟視圖的距離(垂直線性布局中雖然第一個(gè)子視圖的頂部是距離父視圖但是我們?nèi)匀环Q為間距)。**

對于絕對值類型的位置值,他所表示的意義是邊距還是間距這個(gè)要看他所加入的布局視圖的類型而不同。下面的列表中展示了位置在不同的布局中描述的是間距還是邊距:

位置/布局 邊距 間距
tg_left/tg_right LV/FR/R/TH/S LH/FO/FL/P/TV
tg_top/tg_bottom LH/FR/R/TV/S LV/FO/FL/P/TH
tg_centerX LV/FR/R/TH/S -
tg_centerY LH/FR/R/TV/S -
  • TGWeight類型的值表示視圖的位置是一個(gè)依賴于父視圖尺寸的相對比例值。目前只有在線性布局、框架布局、和非布局父視圖中才支持這種類型的值的設(shè)置(具體見上面TGWeight類型值的定義和使用)
    //假如A視圖是在一個(gè)垂直線性布局里面,垂直線性布局的寬度為50
    A.tg_left.equal(20%)   //A視圖的左邊距占用父視圖寬度的20%也就是10
    A.tg_right.equal(30%)  //A視圖的右邊距占用父視圖寬度的30%也就是15
  • TGLayoutPos類型的值表示視圖的位置依賴另外一個(gè)視圖的位置。這種類型的值大部分用于在相對布局中使用的子視圖,但是有幾個(gè)特殊的位置就是父視圖的位置是幾乎在所有布局視圖中都支持。比如:
   A.tg_left.equal(B.tg_right)   //A視圖在B視圖的右邊
   A.tg_top.equal(A.superview.tg_top)  //A視圖的頂部和父視圖對齊
   A.tg_centerX.equal(B.tg_right)      //A視圖的水平中心點(diǎn)和B視圖的右邊對齊
  • UIView類型的值其實(shí)就是TGLayoutPos的簡化版本設(shè)置,標(biāo)識某個(gè)方位的位置等于指定視圖的相同方法的位置值。比如:
     A.tg_left.equal(B)   //A的左邊位置和B的左邊位置相等
    
  • [TGLayoutPos]數(shù)組類型的值,只能用在相對布局里面的子視圖的tg_centerX,tg_centerY這兩個(gè)屬性的equal方法中才有意義,他表示子視圖和數(shù)組里面其他所有子視圖的位置在相對布局中整體水平居中或者垂直居中。比如:
     //相對布局里面有A,B,C,D四個(gè)子視圖,想讓這四個(gè)子視圖在布局視圖里面整體水平居中。
   A.tg_centerX.equal([B.tg_centerX,C.tg_centerX,D.tg_centerX])

   A.tg_centerX.equal(B.tg_centerX) //這個(gè)意義和上面是不同的,他表示A視圖的水平中心點(diǎn)和B視圖的水平中心點(diǎn)是對齊的。
   A.tg_centerX.equal([B.tg_centerX]) //這個(gè)表示A,B在布局視圖里面整體水平居中

上面列出了布局位置類中的equal方法可以設(shè)置的值的類型,我們還看到了方法中存在著另外一個(gè)默認(rèn)的參數(shù):offset 這個(gè)參數(shù)的意義表示在位置等于上述類型的值的基礎(chǔ)上的偏移值。偏移默認(rèn)是0。比如某個(gè)子視圖的左邊位置等于另外一個(gè)子視圖的右邊的位置再往右偏移20時(shí),可以通過equal方法設(shè)置如下:

  A.tg_left.equal(B.tg_right, offset:20)  //A在B視圖的右邊再往右偏移20
  A.tg_top.equal(A.superview.tg_top, offset:20) //A在父視圖頂部往下偏移20的位置

除了可以在equal方法中指定偏移量值外,布局位置類還單獨(dú)提供了一個(gè)offset方法來實(shí)現(xiàn)偏移量的設(shè)置:

    public func offset(_ val:CGFloat) ->TGLayoutPos

這樣上述的代碼也可以用如下方法設(shè)置:

  A.tg_left.equal(B.tg_right).offset(20)
  A.tg_top.equal(A.superview.tg_top).offset(20)

通過偏移量的設(shè)置,我們可以發(fā)現(xiàn)那些表示的是邊距意義的位置值,其實(shí)就是等于位置依賴于父視圖對應(yīng)位置的偏移值。比如某個(gè)子視圖的左邊距是20,其實(shí)就是等價(jià)于子視圖的左邊等于父視圖的左邊再偏移20。下面的代碼其實(shí)是等價(jià)的。

  //A是一個(gè)相對布局里面的子視圖
  A.tg_left.equal(20)
  A.tg_left.equal(A.superview.tg_left).offset(20)   //這句代碼和上句是等價(jià)的

  A.tg_centerY.equal(0)
  A.tg_centerY.equal(A.superview.tg_centerY).offset(0) //這句代碼和上句是等價(jià)的

  A.tg_bottom.equal(20)
  A.tg_bottom.equal(A.superview.tg_bottom).offset(20) //這句代碼和上句是等價(jià)的 

在布局位置類中我們除了可以用equal,offset方法設(shè)置視圖的位置依賴及偏移量外,我們還可以對視圖位置的最大最小值進(jìn)行控制處理。比如在實(shí)踐中我們希望某個(gè)子視圖的左邊距等于父視圖的寬度的20%,但是最小不能小于20,最大不能超過30。 這時(shí)候我們就需要用到布局位置類的另外兩個(gè)方法了:

    public func min(_ val:CGFloat, offset:CGFloat = 0) ->TGLayoutPos
    public func max(_ val:CGFloat, offset:CGFloat = 0) ->TGLayoutPos

上述的兩個(gè)方法min,max分別用來設(shè)置視圖位置最小不能小于的值以及最大不能超過的值。方法中我們可以設(shè)置一個(gè)具體的數(shù)值以及偏移量,因此我們可以通過對minmax 方法的使用來解決上述的問題:

   //A的左邊距等于父視圖的寬度的20%,最小為20,最大為30
  A.tg_left.equal(20%).min(20).max(30)

最后我們列出子視圖的6個(gè)擴(kuò)展屬性在各布局視圖下equal方法能夠設(shè)置的值的類型:

屬性/值 CGFloat TGWeight TGLayoutPos [TGLayoutPos]
tg_left ALL L/FR/T/R/S R -
tg_top ALL L/FR/T/R/S R -
tg_right ALL L/FR/T/R/S R -
tg_bottom ALL L/FR/T/R/S R -
tg_centerX ALL L/FR/T/R/S R R
tg_centerY ALL L/FR/T/R/S R R

線性布局

表格布局

框架布局

相對布局

流式布局

浮動布局

路徑布局

SizeClass

MyLayout和TangramKit之間的語法差異

后記

...敬請期待

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,016評論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內(nèi)容