前言
TangramKit是iOS系統下用Swift編寫的第三方界面布局框架。他集成了iOS的AutoLayout和SizeClass以及Android的五大容器布局體系以及HTML/CSS中的float和flex-box的布局功能和思想,目的是為iOS開發人員提供一套功能強大、多屏幕靈活適配、簡單易用的UI布局解決方案。Tangram的中文即七巧板的意思,取名的寓意表明這個布局庫可以非常靈巧和簡單的解決各種復雜界面布局問題。他的同胞框架:MyLayout是一套用objective-C實現的界面布局框架。二者的主體思想相同,實現原理則是通過擴展UIView的屬性,以及重載layoutSubviews
方法來完成界面布局,只不過在一些語法和屬性設置上略有一些差異。可以這么說TangramKit是MyLayout布局庫的一個升級版本。大家可以通過訪問下面的github站點去下載最新的版本:
- Swift版本TangramKit: https://github.com/youngsoft/TangramKit
- OC版本MyLayout: https://github.com/youngsoft/MyLinearLayout
所見即所得和編碼之爭以及屏幕的適配
在我10多年的開發生涯中,大部分時間都工作在客戶端上。從DOS到Windows再到UNIX再到2010年接觸iOS開發這6年多的時間中,總感覺一無所獲,原因呢是覺沒有什么積累。作為一個以編程為職業的人來說如果不留下什么可以值得為大家所知的東西的話,那將是一種職業上的遺憾。
就像每個領域都有工作細分一樣,現在的編程人員也有明確分工:有一部分人做的是后端開發的工作,而有一部分人做的是前端開發的工作。二者相輔相成而完成了整個系統。后端開發的重點在于實現高性能和高可用,在數據處理上通常都是一個輸入一個加工然后一個輸出;而前端開發的重點在于實現界面流暢性和美觀性,在數據處理上往往是多個輸入一個加工和多個輸出。在技術層面上后端處理的對象是多線程多進程以及數據,而前端處理的對象則是圖形繪制和以及界面布局和動畫特效。
這篇文章的重點是介紹界面布局的核心,因此其他部分就不再展開去說了。對于一個UI界面來說,好的界面布局體系往往能起到事半工倍的作用。PC設備上因為屏幕總是夠大,比如VB,VF,PB,Dephi,AWT,Swing等語言或者環境下的應用開發非常方便,IDE環境中提供一個所見即所得的開發面板(form),人們只要使用簡單的拖拉拽動作就可把各種界面元素加入到form中就可以形成一個小程序了。而開發VC程序則相對麻煩,系統的IDE環境對可視化編程的支持沒有那么的完善,因此大部分界面的構建都需要通過編碼來完成。同時因為PC設備屏幕較大而且標準統一,因此幾乎不存在界面要在各種屏幕尺寸適配的問題。唯一引起爭議是可視化編程和純代碼編程的方式之爭,這種爭議也體現在iOS應用的開發身上,那就是用XIB和SB以及純代碼編寫界面的好壞爭議。關于這個問題個人的意見是各有各好:XIB/SB進行布局時容易上手且所見即所得,但缺乏靈活性和可定制化;而純代碼則靈活性高可定制化強,缺點是不能所見即所得和代碼維護以及系統分層模糊。
再回到屏幕適配的話題來說,如果說PC時代編程屏幕尺寸適配不是很重要的工作,那么到了移動設備時代則不一樣了,適配往往成為整個工作的重點和難點。主要的原因是設備的屏幕尺寸和設備分辨率的多樣性的差異,而且要求在這么小的屏幕上布局眾多的要素,同時又要求界面美觀和友好的用戶體驗,這就非常考驗產品以及UI/UE人員和開發人員的水平,同時這部分工作也占用了開發者的大部分時間。在現有的兩個主流的移動平臺上,Android系統因為本身硬件平臺差異性的原因,為了解決這些差異性而設計了一套非常方便的和友好的界面布局體系。它提出了布局容器的概念,也就是有專門職責的布局容器視圖來管理和排列里面的子視圖,根據實際中的應用場景而把這些負責布局的容器視圖分類抽象出了線性布局、相對布局、框架布局、表格布局、絕對布局這5大容器布局,而這些也就構成了Android系統布局體系的核心實現。也正是這套布局機制使得Android系統能夠方便的勝任多種屏幕尺寸和分辨率在不同硬件設備上的UI界面展示。而對于iOS的開發人員來說,早期的設備只有單一的3.5in大小且分辨率也只有480x320和960x640這兩種類型的設備,因此開發人員只需要采用絕對定位的方式通過視圖的frame
屬性設置來實現界面的布局,根本不需要考慮到屏幕的適配問題。但是這一切從蘋果后續依次發布iPhone4/5/6/7系列的設備后被打破了,整個iOS應用的開發也需要考慮到多屏幕尺寸和多分辨率的問題了,這樣原始的frame
方法進行布局設置將不能滿足這些多屏幕的適配問題了,因此iOS提出了一套新的界面布局體系:AutoLayout以及SizeClass. 這套機制通過設置視圖之間的位置和尺寸的約束以及對屏幕尺寸進行分類的方式來完成界面的布局和屏幕的適配工作。
盡管如此, 雖然兩個移動端平臺都提供了自己獨有且豐富的界面布局體系,但對于移動客戶端開發人員來說界面布局和適配仍然是我們在開發中需要重點關注的因素之一。
布局的核心
我們知道,在界面開發中我們直接操作的對象是視圖,視圖可以理解為一個具有特定功能的矩形區塊,因此所謂的布局的本質就是為視圖指定某個具體的尺寸以及指定其排列在屏幕上的位置。因此布局的動作就分為兩個方面:一個是指定視圖的尺寸,一個是指定視圖的位置。
視圖的尺寸
視圖的尺寸就是指視圖矩形塊的大小,為了表征視圖的大小我們稱在屏幕水平方向的尺寸大小為寬度,而稱在屏幕垂直方向的尺寸大小為高度,因此一個視圖的尺寸我們就可以用寬度和高度兩個維度的值來描述了,寬度和高度的單位我們稱之為點。UIView中用bounds
屬性的size部分來描述視圖的尺寸(bounds屬性的origin部分后面會介紹到)。 對于屏幕尺寸來說同樣也用寬度和高度來描述。在視圖層次體系結構中的頂層視圖的尺寸和屏幕的尺寸是一致的,為了描述這個特殊的頂層視圖我們將這個頂層根視圖稱之為窗口,窗口的尺寸和屏幕的尺寸一樣大,同時窗口是一切視圖的容器視圖。一個視圖的尺寸我們可以用一個具體的數值來描述,比如某個視圖的寬度和高度分別為:100x200。我們稱這種定義的方式為絕對值類型的尺寸。但是在實際中我們的一些視圖的尺寸并不能夠一開始就被明確,原因是這些視圖的尺寸大小和其他視圖的尺寸大小有關,也就是說視圖的尺寸依賴于另外一個視圖或者另外一組視圖。比如說有A和B兩個視圖,我們定義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
我們稱為這種尺寸的定義方式為相對值類型的尺寸。在相對值類型的尺寸中, 視圖某個維度的尺寸所依賴的另外一個視圖可以是它的兄弟視圖,也可以是它的父視圖,也可以是它的子視圖,甚至可以是它自身的其他維度。 這種視圖尺寸的依賴關系是可以傳遞和遞歸的,比如A依賴于B,而B右依賴于C。 但是這種遞歸和傳遞關系不能形成一個閉環依賴,也就是說在依賴關系的最終節點視圖的尺寸的值必須是一個絕對值類型或者特定的相對值類型(wrap包裹值),否則的話我們將形成約束沖突而進入死循環的場景。
視圖的尺寸之間的依賴關系還有兩種特定的場景:
- 某個視圖的尺寸依賴于里面所有子視圖的尺寸的大小或者依賴于視圖內所展示的內容的尺寸,我們稱這種依賴為包裹(wrap)。
- 某個視圖的尺寸依賴于所在父視圖的尺寸減去其他兄弟視圖所占用的尺寸的剩余尺寸也就是說尺寸等于父視圖的尺寸和其兄弟視圖尺寸的差集,我們稱這種依賴為填充(fill)。
可以看出包裹和填充尺寸是相對值類型中的兩種特殊的類型,他所依賴的視圖并不是某個具體的視圖,而是一些相關的視圖的集合。
為了表征視圖的尺寸以及尺寸可以設置的值的類型,我們就需要對尺寸進行建模,在TangramKit框架中TGLayoutSize
類就是一個尺寸類,這個類里面的equal方法則是用來設置視圖尺寸的各種類型的值:包括絕對值類型,相對值類型,以及包裹和填充的值類型等等。同時我們對UIView擴展出了兩個屬性tg_width, tg_height
分別用來表示視圖的布局寬度和布局高度。他其實是對原生的視圖bounds
屬性中的size部分進行了擴充和延展。原始的bounds
屬性中的size部分只能設置絕對值類型的尺寸,而不能設置相對值類型的尺寸。
視圖的位置
當一個視圖的尺寸確定后,接下來我們就需要確定視圖所在的位置了。所謂位置就是指視圖在屏幕中的坐標位置,屏幕中的坐標分為水平坐標也就是x軸坐標,和垂直坐標也就是y軸坐標。而這個坐標原點在不同的系統中有區別:iOS系統采用左手坐標系,原點都是在左上角,并且規定y軸在原點以下是正坐標軸,而原點以上是負坐標軸,而x軸則在原點右邊是正坐標軸,原點左邊是負坐標軸。OSX系統則采用右手坐標系,原點在左下角,并且規定y軸在原點以上是正坐標軸,而在原點以下是負坐標軸,而x軸則在原點右邊是正坐標軸,原點左邊是負坐標軸。
因此視圖位置的確定我們需要考慮兩個方面的問題:一個是位置是相對于哪個坐標系?一個是視圖內部的哪個部位來描述這個位置?
確定一個視圖的位置時總是應該有一個參照物,在現有的布局體系中一般分為三種參照物:屏幕、父視圖、兄弟視圖。
第一種以屏幕坐標系作為參照來確定的位置稱為絕對位置,也就是以屏幕的左上角作為原點,每個視圖的位置都是距離屏幕左上角原點的一個偏移值。這種絕對位置的設置方式的優點是所有視圖的參照物都是一致的,便于比較和計算,但缺點是對于那些多層次結構的視圖以及帶滾動效果的視圖來說位置的確定則總是需要進行動態的變化和計算。比如某個滾動視圖內的所有子視圖在滾動時都需要重新去計算自己的位置。
第二種以父視圖坐標系作為參照來確定的位置稱為相對位置,每個子視圖的位置都是距離父視圖左上角原點的一個偏移值。這樣的好處就是每個子視圖都不再需要關心屏幕的原點,而只需要以自己的父視圖為原點進行位置的計算就可以了,這種方式是目前大部分布局體系里面采用的定位方式,也是最方便的定位方式,缺點是不同層次之間的視圖的位置在進行比較時需要一步步的往上進行轉換,直到轉換到在窗口中的位置為止。我們稱這種以父視圖坐標系為原點進行定位的位置稱為邊距,也就是離父視圖邊緣的距離。
-
第三種以兄弟視圖坐標系作為參照來確定的位置稱為偏移位置,子視圖的位置是在關聯的兄弟視圖的位置的基礎之上的一個偏移值。比如A視圖在B視圖的右邊偏移5個點,則表示為A視圖的左邊距離B視圖的右邊5個點的距離。我們稱這種坐標體系下的位置為間距,也就是指定的是視圖之間的距離作為視圖的位置。采用間距的方式進行定位只適合于同一個父視圖之間的兄弟視圖之間的定位方式。
各種坐標系下的定位值
上面的三種定位方式各有優缺點,我們可以在實際中結合各種定位方式來完成視圖的位置設定。
上面我們介紹了定位時位置所基于的坐標系,因為視圖并不是一個點而是一個矩形區塊,所以我們必須要明確的是視圖本身這個區塊的哪個點來進行位置的設定。 在這里我們就要介紹視圖內的坐標系。我們知道視圖是一個矩形的區域,里面由無數個點構成。假如我們以視圖左上角作為坐標原點的話,那么視圖內的任何一點都可以用水平方向的坐標值和垂直方向的坐標值來表示。對于水平方向的坐標值來說最左邊位置的點的坐標值是0,最右邊位置的點的坐標值是視圖的寬度,中間位置的坐標點的值是寬度的一半,對于垂直方向的坐標值來說最上邊位置的點的坐標值是0,最下邊位置的點的坐標值是視圖的高度,中間位置的坐標點的值是高度的一半。我們稱這幾個特殊的坐標點為方位。因此一個視圖一共有9個方位點分別是:左上、左中、左下、中上、中中、中下、右上、右中、右下。
通過對方位點的定義,我們就不再需要去關心這些點的具體的坐標值了,因為他描述了視圖的某個特定的部位。而為了方便計算和處理,我們一般只需要指出視圖內某個方位點在參照視圖的坐標系里面的水平坐標軸和垂直坐標軸中的位置就可以完成視圖的位置定位了,因為只要確定了這個方位點的在參照視圖坐標系里面的位置,就可以計算出這個視圖內的任意的一個點在參照視圖坐標軸里面的位置。所謂的位置定位就是把一個視圖內坐標系的某個點的坐標值映射為參照視圖坐標系里面的坐標值的過程。
iOS中UIView提供了一個屬性center
,center
屬性的意義就是定義視圖內中心點這個方位在父視圖坐標系中的坐標值。UIView還提供一個屬性frame
,frame
屬性的意義則是用來描述視圖左上角這個方位在父視圖坐標系中的坐標值和在父視圖中的顯示尺寸(坐標變換后除外)。 我們再來考察一下UIView的bounds
屬性,上面的章節中我們有介紹bounds
中的size部分用來描述一個視圖的尺寸,而origin部分又是用來描述什么呢? 我們知道在左手坐標系里面,一個視圖內的左上角方位的坐標值就是原點的坐標值,默認情況下原點的坐標值是(0,0)。但是這個定義不是一成不變的,也就是說原點的坐標值不一定是(0,0)。一個視圖bounds
里面的origin部分所表達的意義就是視圖自身坐標系左上角原點方位的坐標值。這個值的設定將會影響到里面所有子視圖的定位和顯示。假如我們設置某個視圖的bounds.origin為(0,-64)時,那么表示視圖左上角x軸的坐標原點值是0,而左上角y軸的坐標原點值是-64,當這個視圖內的某個子視圖的frame屬性的origin為(0,0)時這個子視圖的左上角的x軸部分和視圖原點x軸一致,而y軸部分則會往下偏移64個單位。 可以看出bounds屬性是對視圖自身的原點坐標值和尺寸的描述,不會受到其他視圖以及坐標變換的影響。 我們還可以通過下面的公式得出一個視圖內9個方位(再次強調方位的概念是一個視圖內的坐標點的位置)的坐標值:
左上方位 = (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
類就是一個對位置進行建模的類。TGLayoutPos類同時支持采用父視圖作為參考系和以兄弟視圖作為參考系的定位方式,這可以通過為其中的equal方法設置不同類型的值來決定其定位方式。為了實現視圖定位我們也為UIView擴展出了3個水平方位的屬性:tg_left, tg_centerX,tg_right來表示左中右三個方位對象。3垂直方位的屬性:tg_top, tg_centerY,tg_bottom來表示上、中、下三個方位。這6個方位對象將比原生的center
屬性提供更加強大和豐富的位置定位能力。
iOS系統的原生布局體系里面是通過bounds
屬性和center
屬性來進行視圖的尺寸設置和位置設置的。bounds用來指定視圖內的左上角方位的坐標值,以及視圖的尺寸,而center則用來指定視圖的中心點方位在父視圖這個坐標體系里面的坐標值。為了簡化設置UIView提供了一個簡易的屬性frame
可以用來直接設置一個視圖的尺寸和位置,frame中的origin部分指定視圖左上角方位在父視圖坐標系里面的坐標值,而size部分則指定了視圖本身的尺寸。frame
屬性并不是一個實體屬性而是一個計算類型的屬性,在我們沒有對視圖進行坐標變換時(視圖的transform未設置時)我們可以得到如下的frame
屬性的偽代碼實現:
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
}
}
綜上所述,我們可以看出,所謂視圖布局的核心,就是確定一個視圖的尺寸,和確定視圖在參考視圖坐標系里面的坐標位置。為了靈活處理和計算,視圖的尺寸可以設置為絕對值類型,也可以設置為相對值類型,也可以設置為特殊的包裹或者填充值類型;視圖的位置則可以指定視圖中的任意的方位,以及設置這個方位的點在窗口坐標系或者父視圖坐標系或者兄弟坐標系中的坐標值。正是提供的這些多樣的設置方式,我們就可以在不同的場景中使用不同的設置來完成各種復雜界面的布局。
Android的布局體系
屏幕尺寸、PPI、DPI
布局框架結構
layout布局文件。
5大布局類
...敬請期待
HTML/CSS的布局體系
CSS定位方式
浮動float
flex-box bootstrap
...敬請期待
iOS布局體系
frame,bounds,center
XIB和storyboard
AutoLayout和SizeClass
...敬請期待
TangramKit布局框架
在您不了解TangramKit之前,可以先通過下面一個例子來感受和體驗一下TangramKit的布局構建語法:
- 有一個容器視圖S的寬度是100而高度則等于由四個從上到下依次排列的子視圖A,B,C,D的高度總和。
- 子視圖A的左邊距占用父視圖寬度的20%,而右邊距則占用父視圖寬度的30%,高度則等于自身的寬度。
- 子視圖B的左邊距是40,寬度則占用父視圖的剩余寬度,高度是40。
- 子視圖C的寬度占用父視圖的所有寬度,高度是40。
- 子視圖D的右邊距是20,寬度是父視圖寬度的50%,高度是40。
代碼實現如下:
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)
因為TangramKit對布局位置類和布局尺寸類的方法重載了運算符:~=、>=、<=、+=、-=、*=、/=
所以您可以用更加簡潔的代碼進行編寫:
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實現的布局代碼和上面場景描述文本幾乎相同,非常的利于閱讀和理解。那么這些系統又是如何實現的呢?
實現原理
我們知道在對任何一個視圖進行布局時,最終都是通過設置視圖的尺寸和視圖的位置來完成的。在iOS中我們可以通過UIView的bounds
屬性來完成視圖的尺寸設置,而通過center
屬性來完成視圖的位置設置。為了進行簡單的操作,系統提供了frame
這個屬性來簡化對尺寸和位置的設置。這個過程不管是原始的方法還是后續的AutoLayout其實現的最終機制都是一致的。每當一個視圖的尺寸改變或者要求重新布局時,系統都會調用視圖的方法:
open func layoutSubviews()
而我們可以在UIView的派生類中重載上面的方法來實現對這個視圖里面的所有子視圖的重新布局,至于如何布局子視圖則是需要根據應用場景而定。在編程時我們經常會用到一些視圖,這種視圖只是負責將里面的子視圖按照某種規則進行排列和布局,而別無其他的作用。因此我們稱這種視圖為容器視圖或者稱為布局視圖。TangramKit框架對種視圖進行了建模而提供了一個從UIView派生的布局視圖基類TGBaseLayout。這個類的作用就是專門負責對加入到其中的所有子視圖進行布局排列,它是通過重載layoutSubviews方法
來完成這個工作的。剛才我們說過如何排列容器視圖中的子視圖是要根據具體的應用場景而定, 比如有可能是所有子視圖從上往下按照添加的順序依次排列,或者子視圖按照某種約束依賴關系來進行布局排列,或者子視圖需要多行多列的排列等等。因此我們對常見的布局應用場景進行了抽象,通過建立不同的TGBaseLayout的派生類來實現不同的布局處理:
線性布局TGLinearLayout:線性布局里面的所有子視圖都按照添加的順序依次從上到下或者依次從左到右進行排列。根據排列的方向可以分為垂直線性布局和水平線性布局。線性布局和iOS9上的UIStackView以及Android中的線性布局LinearLayout提供一樣的功能。
框架布局TGFrameLayout: 框架布局里面的所有子視圖布局時和添加的順序無關,而是按照設定的位置停靠在布局視圖的:左上、左中、左下、中上、中中、中下、右上、右中、右下、填充這個10個方位中的任何一個位置上。框架布局里面的子視圖只跟框架布局視圖的邊界建立約束關系。框架布局和Android中的框架布局FrameLayout提供一樣的功能。
表格布局TGTableLayout:表格布局里面的子視圖可以進行多行多列的排列。在使用時要先添加行,然后再在行里面添加列,每行的列數可以隨意確定。因為表格布局是線性布局TGLinearLayout的派生類,所以表格布局也分為垂直表格布局和水平表格布局。垂直表格布局中的行是從上到下,而列則是從左到右排列;水平表格布局中的行是從左到右,而列是從上到下排列的。表格布局和Android中的表格布局TableLayout以及HTML中的table,tr,td元素提供一樣的功能。
相對布局TGRelativeLayout: 相對布局里面的子視圖和添加的順序無關,而是按照子視圖之間設定的尺寸約束依賴和位置約束依賴進行布局排列。因此相對布局里面的所有子視圖都要設置位置和尺寸的約束和依賴關系。相對布局和iOS的AutoLayout以及Android中的相對布局RelativeLayout提供一樣的功能。
流式布局TGFlowLayout: 流式布局里面的子視圖按照添加的順序依次從某個方向排列,而當遇到了這個方向上的排列數量限制或者容器的尺寸限制后將會另起一行,而重新按照原先的方向依次排列。最終這個布局中的子視圖將形成多行多列的排列展示。流式布局和線性布局的區別是,線性布局只是單行或者單列的,而流式布局則是多行多列。流式布局和表格布局的區別是,表格布局有明確行的概念,在使用前要添加行再添加列,而流式布局則沒有明確行的概念,由布局自動生成行和列。根據排列的方向和限制的規則,流式布局分為垂直數量約束布局、垂直內容約束布局、水平數量約束布局、水平內容約束布局四種布局。流式布局實現了HTML/CSS3中的flex-box的子集的功能。
浮動布局TGFloatLayout:浮動布局里面的子視圖按照添加的順序,并且按照每個子視圖自身設定的浮動規則向某個方向進行浮動停靠。當子視圖的尺寸無法容納到布局視圖的剩余空間時,則會自動尋找一個能夠容納自身尺寸的最佳位置進行浮動停靠。浮動布局里面的子視圖并不是有規則的多行多列的排列。根據子視圖可以浮動的方向浮動布局分為垂直浮動布局和水平浮動布局。浮動布局和HTML/CSS中的float定位實現了相同的功能。
路徑布局TGPathLayout: 路徑布局里面的子視圖按照一個提供的數學函數得到的曲線路徑等距離的根據添加的順序依次排列。所有的子視圖的位置都是根據函數曲線中距離相等的點而確定的。路徑布局提供了直角坐標系、參數方式、極坐標系三種曲線的構建方法。路徑布局是TangramKit中的獨有的一種布局。
上述的7個派生類分別的實現了大部分的不同的應用場景。在每個派生類的layoutSubviews
的實現中都按照描述的規則來設置子視圖的尺寸bounds
和位置center
屬性。也就是說最終的子視圖的尺寸和位置是在布局視圖中的layoutSubviews
中進行設置的。那么我們就必須要提供另外一套子視圖的布局尺寸和布局位置的設置方法,以便在布局視圖布局時將子視圖設置好的布局尺寸和布局位置轉化為真實的視圖尺寸和視圖位置。為此TangramKit專門提供了一個視圖的布局尺寸類TGLayoutSize用來進行子視圖的布局尺寸的設置,一個視圖的布局位置類TGLayoutPos用來進行子視圖的布局位置的設置。我們對UIView建立了一個extension。分別擴展出了2個布局尺寸對象和6個布局位置對象:
extension UIView
{
//左邊位置
var tg_left:TGLayoutPos{get}
//上邊位置
var tg_top:TGLayoutPos{get}
//右邊位置
var tg_right:TGLayoutPos{get}
//下邊位置
var tg_bottom:TGLayoutPos{get}
//水平中心點位置
var tg_centerX:TGLayoutPos{get}
//垂直中心點位置
var tg_centerY:TGLayoutPos{get}
//寬度尺寸
var tg_width:TGLayoutSize{get}
//高度尺寸
var tg_height:TGLayoutSize{get}
}
也就是說我們將不再直接設置子視圖的bounds
和center
(這兩個屬性只會在布局視圖中的layoutSubviews
中設置)屬性了,而是直接操作UIView擴展出來的布局位置對象和布局尺寸對象。如果把布局視圖的layoutSubviews
比作一個數學函數的話,那么我們就能得到如下的方程式:
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的派生類因為里面的布局方法不相同,而導致子視圖的位置和尺寸的計算方法不同,從而得到了我們想要的效果。那么為什么要用6個布局位置對象和2個布局尺寸對象來設置子視圖的位置和尺寸而不直接用bounds
和center
呢? 原因在于bounds和center只提供了有限的設置方法而布局位置對象和布局尺寸對象則提供了功能更加強大的設置方法,而這些方法又可以簡化我們的編程,以及可以很方便的適配各種不同尺寸的屏幕。(還記得我們上面的例子里面,尺寸和位置可以設置為數值,.wrap, .fill,以及百分比的值嗎?)。
TangramKit為了存儲這些擴展的布局位置和布局尺寸對象,內部是使用了objc的runtime機制提供的動態屬性創建的方法:
public func objc_getAssociatedObject(_ object: Any!, _ key: UnsafeRawPointer!) -> Any!
系統通過這個方法來關聯視圖對象的那6個布局位置和2個布局尺寸對象。
上面的代碼中我們看到了布局容器視圖通過layoutSubviews
方法來實現對子視圖的重新布局。而且也提到了當容器視圖的尺寸發生變化時也會激發對layoutSubviews
的調用。除了自動激發外,我們可以通過手動調用布局視圖的setNeedLayout
方法來實現布局視圖的layoutSubviews
調用。當我們在設置子視圖的布局位置和布局尺寸時,系統內部會在設置完成后調用布局視圖的setNeedLayout
的方法,因此只要對子視圖的布局位置和布局尺寸進行設置都會重新激發布局視圖的布局視圖。那么對子視圖的frame,bounds,center真實位置和尺寸的改變呢?我們也要激發布局視圖的重新布局。為了解決這個問題,我們引入了KVO
的機制。布局視圖在添加子視圖時會監聽加入到其中的子視圖的frame,bounds,center的變化,并在其變化時調用布局視圖的setNeedLayout
來激發布局視圖的重新布局。我們知道每次當一個視圖調用addSubview添加子視圖時都會激發調用者的方法:didAddSubview
。為了實現對子視圖的變化的監控,布局視圖重載了這個方法并對子視圖的isHidden,frame,center
進行監控:
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")
}
當子視圖的frame或者center變更時,將會激發布局視圖的重新布局。上面曾經說過,在布局視圖重新布局子視圖時最終會調整子視圖的bounds和center.那么這樣就有可能會形成循環的重新布局,為了解決這種循環遞歸的情況,布局視圖在layoutSubviews調用進行布局前設置了一個布局中的標志,而在所有子視圖布局完成后將恢復這個布局中的標志。因此當我們布局視圖通過KVO監控到子視圖的位置和尺寸變化時,則會判斷那個布局中的標志,如果當前是在布局中則不會再次激發布局視圖的重新布局,從而防止了死循環的發生。
這就是TangramKit布局實現的原理,下面的圖表列出了TangramKit的整個布局框架的類體系結構:
布局位置類和布局尺寸類
在前面的介紹布局核心的章節以及布局實現原理的章節里面我們有說道布局位置類和布局尺寸類。之所以系統不直接操作視圖的bounds和center
屬性而是通過擴展視圖的2個布局尺寸屬性和6個布局位置屬性來進行子視圖的布局設置。原因是后者能夠提供豐富和多樣的設置。而且我們在編程時也不再需要通過設置視圖的frame來實現布局了,即使設置也可能會失效。
比重類TGWeight
TGWeight類的值表示尺寸或者位置的大小是父布局視圖的尺寸或者剩余空間的尺寸的比例值,也就是說值的大小依賴于父布局視圖的尺寸或者剩余空間的尺寸的大小而確定,這樣子視圖就不需要明確的指定位置和尺寸的大小了,非常適合那些需要適配屏幕的尺寸和位置的場景。 至于是父視圖的尺寸還是父視圖剩余空間的尺寸則要根據其所在的布局視圖的上下文而確定。比如:
//假如A,b是在一個垂直線性布局下的子視圖
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的頂部間距時父視圖的剩余高度的10%
為了簡化和更加直觀的表示比重類型的值,我們重載%運算符,這樣上面的代碼就可以簡寫為如下更加直觀的方式:
//假如A是在一個垂直線性布局下的子視圖
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的頂部間距時父視圖的剩余高度的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擴展出了2個布局尺寸對象 :
public var tg_width:TGLayoutSize
public var tg_height:TGLayoutSize
分別用來實現視圖的寬度和高度的布局尺寸設置。在TGLayoutSize類中,我們可以通過方法equal
來設置視圖尺寸的多種類型的值,類中是通過重載equal方法來實現多種類型的值的設置的。
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方法來設置:
- CGFloat類型的值表示視圖的尺寸是一個絕對值類型的尺寸值。比如:
A.tg_width.equal(100) //A的寬度為100
A.tg_height.equal(200) //A的高度為200
- TGWeight類型的值表示視圖的尺寸是一個依賴于父視圖尺寸的相對比例值。(具體見上面TGWeight類型值的定義和使用)
//假如A是在一個垂直線性布局下的子視圖
A.tg_width.equal(20%) //A的寬度是父布局視圖寬度的20%
A.tg_height.equal(30%) //A的高度是父布局視圖剩余高度的30%
- TGLayoutSize類型的值表示視圖的尺寸和另外一個尺寸對象的值相等,這也是一種相對值類型的尺寸值,通過設置這種尺寸的依賴我們就可以不必要明確的指定一個具體的值,而是會隨著所以依賴的尺寸變化而變化。設置為TGLayoutSize類型的值通常用于在相對布局中的子視圖,當然也可以在其他類型的布局中使用。下面是一個展示的例子:
A.tg_width.equal(B.tg_width) //A的寬度等于B的寬度
A.tg_height.equal(A.tg_width) //A的高度等于A的寬度
- UIView類型的值其實就是TGLayoutSize的簡化版本設置,表示某個維度的尺寸值等于指定視圖的相同維度的尺寸值。比如:
A.tg_width.equal(B) //表示A視圖的寬度等于B視圖的寬度
A.tg_height.equal(A.superview) //表示A視圖的高度等于父視圖的高度。
- [TGLayoutSize]數組類型的值,只用在相對布局里面的子視圖設置才有意義,其他的類型的布局中設置這種類型的值無效。他表示子視圖的尺寸和數組里面的所有子視圖來等分父布局視圖的尺寸。比如:
//A,B,C,D都是相對布局視圖里面的子視圖,我們希望A,B,C,D這四個子視圖來均分父視圖的寬度,這樣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的寬度都是布局視圖的寬度的一半
-
特殊類型的值。為了簡化尺寸的設置我們定義了三種特殊類型的尺寸值:
- wrap: 他表示尺寸的值由布局視圖的所有子視圖的尺寸或者由子視圖的內容包裹而成。也就是尺寸的大小是由子視圖或者視圖的內容共同決定的,這樣視圖的尺寸將依賴其內部的子視圖的尺寸或者子視圖內容的大小。
fill: 他表示視圖的尺寸的值將會填充滿父視圖的剩余空間,也就是說視圖的尺寸值是依賴于父視圖的尺寸的大小。
average:他表示視圖的尺寸將和其兄弟視圖一起來均分父視圖的尺寸,這樣所有兄弟視圖的尺寸都將相等。
下面是這三個特殊值使用的例子:
A.tg_width.equal(.wrap) //A視圖的寬度由里面的所有子視圖或者內容包裹而確定。
A.tg_height.equal(.fill) //A視圖的高度填充滿父視圖的剩余高度空間。
B.tg_width.equal(.average) //B視圖的寬度將會和其他兄弟視圖均分父視圖的寬度。
上面列出了布局尺寸類中的equal方法可以設置的值的類型,我們還看到了方法中存在著另外兩個默認的參數:increment 和multiple
這兩個參數的意義表示在尺寸等于上述類型的值的基礎上的增量值和倍數值。增量值默認是0,而倍數值則默認是1。比如某個子視圖的寬度等于另外一個子視圖的寬度值加20的時,可以通過equal方法設置如下:
A.tg_width.equal(B.tg_width, increment:20) //A的寬度等于B的寬度加20
除了可以在equal方法中指定增量值外,布局尺寸類還單獨提供一個add
方法來實現增量值的設置:
public func add(_ val:CGFloat) ->TGLayoutSize
這樣上述的代碼也可以用如下的方式設置:
A.tg_width.equal(B.tg_width).add(20)
在equal方法中的multiple值則是指定尺寸等于另外一個尺寸的倍數值。比如某個子視圖的高度等于另外一個子視圖的高度的一半時,可以通過equal方法設置如下:
A.tg_height.equal(B.tg_height, multiple:0.5); //A的高度等于B的高度的一半。
除了可以在equal方法中指定倍數值外,布局尺寸類還單獨提供一個multiply
方法來實現倍數值的設置:
public func multiply(_ val:CGFloat) ->TGLayoutSize
這樣上述的代碼也可以用如下的方式設置:
A.tg_height.equal(B.tg_height).multiply(0.5)
在布局尺寸類中我們除了可以用equal, add, multiply
方法來設置視圖的尺寸依賴值以及增量和倍數外,我們還可以對視圖尺寸的最大最小值進行控制處理。比如在實踐中我們希望某個視圖的寬度等于另外一個兄弟視圖的寬度,但是最小不能小于20,而最大則不能超過父視圖的寬度的一半。 這時候我們就需要用到布局尺寸類的另外兩個方法了:
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
上述的兩個方法min,max
分別用來設置視圖尺寸最小不能小于的值以及最大不能超過的值。方法中我們可以看出最大最小值除了可以設置具體的數值外還可以設置為另外一個布局尺寸對象,同樣我們還可以設置增量和倍數值。因此我們可以通過對min
和max
方法的使用來解決上述的問題:
//A的寬度等于B的寬度,最小為20,最大為父視圖寬度的一半。
A.tg_width.equal(B.tg_width).min(20).max(A.superview,multiple:0.5)
最后我們列出視圖的擴展屬性tg_width, tg_height在各布局視圖下equal方法能夠設置的值的類型,我們這里設置B為一個兄弟視圖,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擴展出了6個布局位置對象:
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 //視圖水平中心點布局位置
public var tg_centerY:TGLayoutPos //視圖垂直中心點布局位置
分別用來實現視圖的水平維度的左、中、右三個方位以及視圖垂直維度的上、中、下三個方位的布局位置設置。在TGLayoutPos類中,我們可以通過方法equal
來設置視圖位置的多種類型的值,類中是通過重載equal方法來實現多種類型的值的設置的。
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方法來設置:
- CGFloat類型的值表示視圖的位置是一個絕對值類型的位置值。 比如:
A.tg_left.equal(10) //A視圖的左邊位置是10
A.tg_right.equal(20) //A視圖的右邊位置是20
A.tg_centerX.equal(5) //A視圖的水平中心點的偏移位置是5
我們知道在視圖定位時位置的概念根據參考坐標系不同而不同:
- 定位的值如果是以父視圖作為參考系坐標那么視圖的位置就叫做邊距 ,邊距描述的是視圖距離父視圖的距離。
- 定位的值如果是以兄弟視圖作為參考系坐標那么視圖的位置就叫做間距,間距描述的是視圖距離兄弟視圖的距離(垂直線性布局中雖然第一個子視圖的頂部是距離父視圖但是我們仍然稱為間距)。**
對于絕對值類型的位置值,他所表示的意義是邊距還是間距這個要看他所加入的布局視圖的類型而不同。下面的列表中展示了位置在不同的布局中描述的是間距還是邊距:
位置/布局 | 邊距 | 間距 |
---|---|---|
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類型的值表示視圖的位置是一個依賴于父視圖尺寸的相對比例值。目前只有在線性布局、框架布局、和非布局父視圖中才支持這種類型的值的設置(具體見上面TGWeight類型值的定義和使用)
//假如A視圖是在一個垂直線性布局里面,垂直線性布局的寬度為50
A.tg_left.equal(20%) //A視圖的左邊距占用父視圖寬度的20%也就是10
A.tg_right.equal(30%) //A視圖的右邊距占用父視圖寬度的30%也就是15
- TGLayoutPos類型的值表示視圖的位置依賴另外一個視圖的位置。這種類型的值大部分用于在相對布局中使用的子視圖,但是有幾個特殊的位置就是父視圖的位置是幾乎在所有布局視圖中都支持。比如:
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視圖的水平中心點和B視圖的右邊對齊
- UIView類型的值其實就是TGLayoutPos的簡化版本設置,標識某個方位的位置等于指定視圖的相同方法的位置值。比如:
A.tg_left.equal(B) //A的左邊位置和B的左邊位置相等
- [TGLayoutPos]數組類型的值,只能用在相對布局里面的子視圖的tg_centerX,tg_centerY這兩個屬性的equal方法中才有意義,他表示子視圖和數組里面其他所有子視圖的位置在相對布局中整體水平居中或者垂直居中。比如:
//相對布局里面有A,B,C,D四個子視圖,想讓這四個子視圖在布局視圖里面整體水平居中。
A.tg_centerX.equal([B.tg_centerX,C.tg_centerX,D.tg_centerX])
A.tg_centerX.equal(B.tg_centerX) //這個意義和上面是不同的,他表示A視圖的水平中心點和B視圖的水平中心點是對齊的。
A.tg_centerX.equal([B.tg_centerX]) //這個表示A,B在布局視圖里面整體水平居中
上面列出了布局位置類中的equal方法可以設置的值的類型,我們還看到了方法中存在著另外一個默認的參數:offset
這個參數的意義表示在位置等于上述類型的值的基礎上的偏移值。偏移默認是0。比如某個子視圖的左邊位置等于另外一個子視圖的右邊的位置再往右偏移20時,可以通過equal方法設置如下:
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方法中指定偏移量值外,布局位置類還單獨提供了一個offset
方法來實現偏移量的設置:
public func offset(_ val:CGFloat) ->TGLayoutPos
這樣上述的代碼也可以用如下方法設置:
A.tg_left.equal(B.tg_right).offset(20)
A.tg_top.equal(A.superview.tg_top).offset(20)
通過偏移量的設置,我們可以發現那些表示的是邊距意義的位置值,其實就是等于位置依賴于父視圖對應位置的偏移值。比如某個子視圖的左邊距是20,其實就是等價于子視圖的左邊等于父視圖的左邊再偏移20。下面的代碼其實是等價的。
//A是一個相對布局里面的子視圖
A.tg_left.equal(20)
A.tg_left.equal(A.superview.tg_left).offset(20) //這句代碼和上句是等價的
A.tg_centerY.equal(0)
A.tg_centerY.equal(A.superview.tg_centerY).offset(0) //這句代碼和上句是等價的
A.tg_bottom.equal(20)
A.tg_bottom.equal(A.superview.tg_bottom).offset(20) //這句代碼和上句是等價的
在布局位置類中我們除了可以用equal,offset
方法設置視圖的位置依賴及偏移量外,我們還可以對視圖位置的最大最小值進行控制處理。比如在實踐中我們希望某個子視圖的左邊距等于父視圖的寬度的20%,但是最小不能小于20,最大不能超過30。 這時候我們就需要用到布局位置類的另外兩個方法了:
public func min(_ val:CGFloat, offset:CGFloat = 0) ->TGLayoutPos
public func max(_ val:CGFloat, offset:CGFloat = 0) ->TGLayoutPos
上述的兩個方法min,max
分別用來設置視圖位置最小不能小于的值以及最大不能超過的值。方法中我們可以設置一個具體的數值以及偏移量,因此我們可以通過對min
和max
方法的使用來解決上述的問題:
//A的左邊距等于父視圖的寬度的20%,最小為20,最大為30
A.tg_left.equal(20%).min(20).max(30)
最后我們列出子視圖的6個擴展屬性在各布局視圖下equal方法能夠設置的值的類型:
屬性/值 | 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之間的語法差異
后記
...敬請期待