最近時不時的聽到關於 FlexBox 的聲音,除了在 Weex 以及 React Native 兩個著名的跨平臺項目裏有用到 FlexBox 外,AsyncDisplayKit 也一樣引入了 FlexBox 。css
先說說 iOS 自己提供給咱們 2 種佈局方式:html
Frame 沒什麼太多可說的了,直接制定座標和大小,設置絕對值。git
Auto Layout
自己用意是好的,試圖讓咱們從 Frame 中解放出來,擺脫關於座標和大小的刻板思考方式。轉而利用 UI 之間的相對位置關係,設置對應約束進行佈局。github
可是 Auto Layout
好心並未作成好事,它的語法又臭又長! 至今學習 iOS 兩年,我使用到原生 Auto Layout
語法的時候屈指可數。只能靠 Masonry 這樣的第三方庫來使用它。算法
說完了 Auto Layout
的使用,再來看看它工做原理。api
實際上,咱們設置 Auto Layout
的約束,就構成一系列的條件,成爲一個方程。而後解出 Frame 的座標和大小。瀏覽器
例如,咱們設置一個名爲 A 的 UI :bash
A.center = super.center
A.width = 40
A.height = 40
複製代碼
則: A.frame = (super.center.x-40/2,super.center.y-40/2,40,40)app
再設置一個 B:dom
B.width = A.width
B.height = A.height
B.top = A.bottom + 50
B.left = A.left
複製代碼
則: B.frame = ( A.x , A.y + A.height + 50 , A.width , A.height )
如圖:
Auto Layout
內部有專門用來處理約束關係的算法,我一直覺得是蘋果自家研發的,查閱資料才發現是來自一個叫 Cassowary
的算法。
Cassowary是個解析工具包,可以有效解析線性等式系統和線性不等式系統,用戶的界面中老是會出現不等關係和相等關係,Cassowary開發了一種規則系統能夠經過約束來描述視圖間關係。約束就是規則,可以表示出一個視圖相對於另外一個視圖的位置。
有興趣的能夠進一步瞭解該算法的實現。
在對 Auto Layout
進行一番瞭解以後,咱們很容易得出 Auto Layout
由於多餘的計算,性能差於 Frame 的結論。
但究竟差多少呢?FlexBox 的表現又如何呢?
這裏根據 從 Auto Layout 的佈局算法談性能 裏的測試代碼進行修改,對 Frame / Auto Layout / FlexBox 進行佈局,分段測算 10 ~ 350 個 UIView 的佈局時間。取 100 次佈局時間的平均值做爲結果,耗時單位爲秒。
結果以下圖:
雖然測試結果不免有誤差,可是根據折線圖能夠明顯發現,FlexBox 的佈局性能是比較接近 Frame 的。
60 FPS
做爲一個 iOS 流暢度的黃金標準,要求佈局在 0.0166667 s 內完成,Auto Layout
在超過 50 個視圖的時候,可能保持流暢就會開始有問題了。
本次測試使用的機器配置以下:
採用 Xcode9.2 ,iPad Pro (12.9-inch)(2nd generation) 模擬器。
測試佈局的項目代碼上傳在 GitHub
FlexBox
是一種 UI 佈局方式,並獲得了全部瀏覽器的支持。FlexBox
首先是基於 盒裝狀型 的,Flexible 意味着彈性,使其能適應不一樣屏幕,補充盒狀模型的靈活性。
FlexBox
把每一個視圖,都看做一個矩形盒子,擁有內外邊距,沿着主軸方向排列,而且,同級的視圖之間沒有依賴。
和 Auto Layout
相似,FlexBox
採用了描述性的語言去進行佈局,而不像 Frame 直接用絕對值座標進行佈局。
彈性佈局的主要思想是讓 Flex Container 有能力來改變 Flex Item 的寬度和高度,以填滿可用空間(主要是爲了容納全部類型的顯示設備和屏幕尺寸)的能力。
最重要的是, FlexBox
佈局與方向無關,常規的佈局設計缺少靈活性,沒法支持大型和複雜的應用程序(特別是涉及到方向轉變,縮放、拉伸和收縮等)。
採用 FlexBox
佈局的元素,稱爲 Flex Container
。
Flex Container
的全部子元素,稱爲 Flex Item
。
下面會講一下 FlexBox 裏面的一些概念,方便以後進行 FlexBox 的使用。
前面提到了,FlexBox
的一個特色,就是視圖之間,是沒有依賴的。
Flex Item
的排布,就依賴於 Flex Container
的屬性設置,而不用相互之間進行設置。
因此先說一下 Flex Containner
的屬性設置。
FlexBox 有一個 主軸(main axis)
和 側軸(cross axis)
的概念。側軸垂直於主軸。
它們能夠是水平,也能夠是垂直。
主軸默認爲 Row
, 側軸默認爲 Column
:
Flex Direction
決定了 Flex Containner
內的主軸排布方向。
主軸默認爲 Row (從左到右):
同時,也能夠設置 RowRevers(從右至左):
Column(從上到下):
ColumnRevers(從下到上):
Flex Wrap 決定在軸線上排列不下時,視圖的換行方式。
Flex Wrap 默認設置爲 NoWrap,不會換行,一直沿着主軸排列到屏幕以外:
設置爲 Wrap ,則空間不足時,自動換行:
設置 WrapReverse,則換行方向與 Wrap 相反:
這是一個很是有用的屬性。好比典型的九宮格佈局
,iOS 若是不是用 UICollectionView
作,那麼就須要保存 9
個實例,而後作判斷,計算 frame ,可維護性實在不高。使用UICollectionView
能夠很好的解決佈局,但不少場景並不能複用,作起來也不是特別簡單。
FlexBox 佈局的話,用 Flex Wrap
屬性設置 Wrap
就能夠直接搞定。
移動平臺上類似的方案,好比 Android 的 Linear Layout 和 iOS 的 UIStackView ,但卻遠沒有 FlexBox 強大。
Display 選擇是否計算它,默認爲 Flex. 若是設置爲 None 自動忽略該視圖的計算。
在根據邏輯顯示 UI 時,比較有用。
好比咱們現有的業務,須要顯示的騰訊身份標示。按照通常作法,多個 icon 互相連成一排,根據身份去設置不一樣的距離,同時隱藏其餘 icon ,比較的麻煩。iOS 最好的辦法是使用 UIStackView ,這又有版本兼容等問題。而使用 FlexBox 佈局,當不是某個身份時,只要設置 Display 爲 None,就不會被歸入 UI 計算當中。
Justify Content
用於定義 Flex Item
在主軸上的對齊方式:FlexStart(主軸起點對齊),FlexEnd(主軸終點對齊),Center(居中對齊)。
還有SpaceBetween(兩端對齊):
設置兩端對齊,讓 Flex Item
之間的間隔相等。
SpaceAround(外邊距相等排列):
讓每一個 Flex Item
四周的外邊距相等
Align Items
定義 Flex Item
在側軸上的對齊方式。
Align Items
能夠和主軸對齊方式 Justify Content
同樣,設置FlexStart ,FlexEnd,Center,SpaceBetween,SpaceAround 。
Align Items
還能夠設置 Baseline(基線對齊):
如圖所示,它是基於 Flex Item
的第一行文字的基線對齊。
若是 Baseline
和 Flex Item
的行內軸與側軸爲同一條,則該值與 FlexStart
等效。 其它狀況下,該值將參與基線對齊。
Align Items
還能夠設置爲 Stretch:
Stretch
讓 Flex Item
拉伸填充整個Flex Container
。Stretch
會使Flex Item
的外邊距在遵守對應屬性限制下,儘量接近所在行或列的尺寸。
若是 Flex Item
未設置數值,或設爲 auto
,將佔滿整個Flex Container
的高度
Align Content
也是側軸在 Flex Item
裏的對齊方式,只不過是以一整個行,做爲最小單位。
注意,若是Flex Item
只有一根軸線(只有一行的Flex Itme
),該屬性不起做用。
調整爲 FlexWrap
爲 Wrap
,效果才顯示出來:
在上面說完了 Flex Container
的屬性,終於說到了 Flex Item
. Flex Container
裏的屬性,都是做用於本身包含的 Flex Item
,Flex Item
的屬性,都是做用於本身自己,.
AlignSelf
可讓單個 Flex Item
與其它 Flex Item
有不同的對齊方式,覆蓋 Align Items
屬性。
默認值爲auto
,表示繼承Flex Container
的Align Items
屬性。若是它自己沒有Flex Container
,則等同於Stretch
。
FlexGrow
能夠設置分配剩餘空間
的比例
。即如何擴大。
FlexGrow
默認值爲 0
,若是沒有去定義 FlexGrow
,該佈局是不會擁有分配剩餘空間權利的。
例如:
總體寬度 100 , sub1 寬爲 10 ,sub2 寬爲 20 ,則剩餘空間
爲 70。
設置 FlexGrow
就是分配這 70 寬度的比例。
再說比例值的問題:
若是全部 Flex Item
的 FlexGrow
屬性都爲 1
,若是有剩餘空間的話,則等分剩餘空間。
若是一個 Flex Item
的 FlexGrow
屬性爲 2
,其他 Flex Item
都爲 1
,則前者佔據的剩餘空間將比其餘 Flex Item
多 1
倍。
與 FlexGrow
處理空間剩餘相反,FlexShrink
用來處理空間不足的狀況。即怎麼縮小。
FlexShrink
默認爲1,即若是空間不足,該項目將縮小
若是全部 Flex Item
的 FlexShrink
屬性都爲 1
,當空間不足時,都將等比例縮小。
若是一個 Flex Item
的 FlexShrink
屬性爲 0
,其他 Flex Item
都爲1
,則空間不足時,FlexShrink
爲 0
的前者不縮小。
FlexBasis
定義了在分配多餘的空間以前, Flex Item
佔據的 main size(主軸空間)
。瀏覽器根據這個屬性,計算主軸是否有多餘空間。
FlexBasis
的默認值爲 auto
,即 Flex Item
的原本大小。
想了解更多 FlexBox 屬性,能夠參考 A Complete Guide to Flexbox
最開頭已經介紹過,FlexBox 佈局已經應用於幾個知名的開源項目,它們用到的就是來自於 Facebook 的 Yoga.
Yoga 是由 C 實現的 Flexbox 佈局引擎,性能和穩定性已經在各大項目中獲得了很好的驗證,但不足的是 Yoga 只實現了 W3C 標準的一個子集。
下面將針對 Yoga iOS 上的實現 YogaKit
作一些講解。
基於上面對 FlexBox
佈局的基本瞭解,做一些簡單的佈局。
整個 YogaKit 的關鍵,就在於 YGLayout
對象當中。經過 YGLayout
來設置佈局屬性。
在 UIView+Yoga.h
的文件裏:
/**
The YGLayout that is attached to this view. It is lazily created.
*/
@property (nonatomic, readonly, strong) YGLayout *yoga;
/**
In ObjC land, every time you access `view.yoga.*` you are adding another `objc_msgSend`
to your code. If you plan on making multiple changes to YGLayout, it's more performant to use this method, which uses a single objc_msgSend call. */ - (void)configureLayoutWithBlock:(YGLayoutConfigurationBlock)block NS_SWIFT_NAME(configureLayout(block:)); 複製代碼
能夠看到一個名爲 yoga
的 YGLayout
只讀對象,和 configureLayoutWithBlock:(YGLayoutConfigurationBlock)block
方法,而且還使用了 NS_SWIFT_NAME()
來定義在 Swift 裏的方法名。
這樣咱們就能夠直接使用 UIView 的實例對象,來直接設置它對應的佈局了。
YGLayout.h
裏是這麼定義 isEnabled
的。
/**
The property that decides during layout/sizing whether or not styling properties should be applied.
Defaults to NO.
*/
@property (nonatomic, readwrite, assign, setter=setEnabled:) BOOL isEnabled;
複製代碼
isEnabled
默認爲 NO
,須要咱們在佈局期間設置爲 YES
,來開啓 Yoga 樣式.
對於這個方法,頭文件裏是這麼解釋的:
/**
Perform a layout calculation and update the frames of the views in the hierarchy with the results.
If the origin is not preserved, the root view's layout results will applied from {0,0}. */ - (void)applyLayoutPreservingOrigin:(BOOL)preserveOrigin NS_SWIFT_NAME(applyLayout(preservingOrigin:)); 複製代碼
簡單來講,就是用於執行 layout 計算的。因此,一旦在佈局代碼完成以後,就要在根視圖
的屬性 yoga 對象上調用這個方法,應用佈局到根視圖
和子視圖
。
下面經過實例來介紹如何使用 Yoga
進行 FlexBox
佈局。
[self configureLayoutWithBlock:^(YGLayout * layout) {
layout.isEnabled = YES;
layout.justifyContent = YGJustifyCenter;
layout.alignItems = YGAlignCenter;
}];
[self.redView configureLayoutWithBlock:^(YGLayout * layout) {
layout.isEnabled = YES;
layout.width=layout.height= 100;
}];
[self addSubview:self.redView];
[self.yoga applyLayoutPreservingOrigin:YES];
複製代碼
效果以下:
咱們真正的佈局代碼,只用設置 Flex Container
的 justifyContent
和 alignItems
就能夠了.
讓一個 view
略小於其 superView
,邊距爲10:
[self.yellowView configureLayoutWithBlock:^(YGLayout *layout) {
layout.isEnabled = YES;
layout.margin = 10;
layout.flexGrow = 1;
}];
[self.redView addSubview:self.yellowView];
複製代碼
效果以下:
佈局代碼只用設置, View 的 margin
和 flexGrow
.
縱向等間距的排列一組 view:
[self configureLayoutWithBlock:^(YGLayout *layout) {
layout.isEnabled = YES;
layout.justifyContent = YGJustifySpaceBetween;
layout.alignItems = YGAlignCenter;
}];
for ( int i = 1 ; i <= 10 ; ++i )
{
UIView *item = [UIView new];
item.backgroundColor = [UIColor colorWithHue:( arc4random() % 256 / 256.0 )
saturation:( arc4random() % 128 / 256.0 ) + 0.5
brightness:( arc4random() % 128 / 256.0 ) + 0.5
alpha:1];
[item configureLayoutWithBlock:^(YGLayout *layout) {
layout.isEnabled = YES;
layout.height = 10*i;
layout.width = 10*i;
}];
[self addSubview:item];
}
複製代碼
效果以下:
只要設置 Flex Container
的 layout.justifyContent = YGJustifySpaceBetween
,就能夠很輕鬆的作到。
讓兩個高度爲 100
的 view
垂直居中,等寬,等間隔排列,間隔爲10.自動計算其寬度:
[self configureLayoutWithBlock:^(YGLayout *layout) {
layout.isEnabled = YES;
layout.flexDirection = YGFlexDirectionRow;
layout.alignItems = YGAlignCenter;
layout.paddingHorizontal = 5;
}];
YGLayoutConfigurationBlock layoutBlock =^(YGLayout *layout) {
layout.isEnabled = YES;
layout.height= 100;
layout.marginHorizontal = 5;
layout.flexGrow = 1;
};
[self.redView configureLayoutWithBlock:layoutBlock];
[self.yellowView configureLayoutWithBlock:layoutBlock];
[self addSubview:self.redView];
[self addSubview:self.yellowView];
複製代碼
效果以下 :
咱們只要設置 Flex Container
的 paddingHorizontal ,以及 Flex Item
的marginHorizontal,flexGrow 就能夠了。而且能夠複用 Flex Item
的 layout 佈局樣式。
在 UIScrollView
順序排列一些 view
,並自動計算 contentSize
:
[self configureLayoutWithBlock:^(YGLayout *layout) {
layout.isEnabled = YES;
layout.justifyContent = YGJustifyCenter;
layout.alignItems = YGAlignStretch;
}];
UIScrollView *scrollView = [[UIScrollView alloc] init] ;
scrollView.backgroundColor = [UIColor grayColor];
[scrollView configureLayoutWithBlock:^(YGLayout *layout) {
layout.isEnabled = YES;
layout.flexDirection = YGFlexDirectionColumn;
layout.height =500;
}];
[self addSubview:scrollView];
UIView *contentView = [UIView new];
[contentView configureLayoutWithBlock:^(YGLayout * _Nonnull layout) {
layout.isEnabled = YES;
}];
for ( int i = 1 ; i <= 20 ; ++i )
{
UIView *item = [UIView new];
item.backgroundColor = [UIColor colorWithHue:( arc4random() % 256 / 256.0 )
saturation:( arc4random() % 128 / 256.0 ) + 0.5
brightness:( arc4random() % 128 / 256.0 ) + 0.5
alpha:1];
[item configureLayoutWithBlock:^(YGLayout *layout) {
layout.isEnabled = YES;
layout.height = 20*i;
layout.width = 100;
layout.marginLeft = 10;
}];
[contentView addSubview:item];
}
[scrollView addSubview:contentView];
[scrollView.yoga applyLayoutPreservingOrigin:YES];
scrollView.contentSize = contentView.bounds.size;
複製代碼
效果以下:
佈置 UIScrollView
主要是使用了一箇中間 contentView
,起到了計算 scrollview
的 contentSize
的做用。這裏要注意的是,要在scrollview
調用完 applyLayoutPreservingOrigin:
後進行設置,不然得不到結果。
UIScrollView 的用法,目前在網上也沒找到比較官方的示例,徹底是筆者本身摸索的,歡迎知道的大佬指教。
上面所用的示例代碼,已經上傳至 GitHub
FlexBox 的確是一個很是適用於移動端的佈局方式,語意清晰,性能穩定,如今移動端 UI 視圖愈來愈複雜,尤爲是在全部瀏覽器都已經支持了 FlexBox 以後,做爲移動開發者有必要了解新的解決方式。
你們在熟練使用 YogaKit 的方式以後,也能夠嘗試本身封裝一套佈局代碼,加快開發效率。