iOS - FlexBox 布局之 YogaKit


 

由於剛開始的項目主要用的H5、javaScript技術為主原生開發為輔的手段開發的項目,UI主要是還是H5,如今翻原生。為了方便同時維護兩端。才找到這個很不錯的庫。

FlexBox?聽起來像是一門H5布局技術。如何應用於原生移動端。

最近時不時的聽到關於 FlexBox 的聲音,除了在 Weex 以及 React Native 兩個著名的跨平台項目里有用到 FlexBox 外,AsyncDisplayKit 也同樣引入了 FlexBox 。

先說說 iOS 本身提供給我們 2 種布局方式:

  • Frame,直接設置橫縱坐標,並指定寬高。
  • Auto Layout,通過設置相對位置的約束進行布局。

Frame 沒什么太多可說的了,直接制定坐標和大小,設置絕對值。

Auto Layout 本身用意是好的,試圖讓我們從 Frame 中解放出來,擺脫關於坐標和大小的刻板思考方式。轉而利用 UI 之間的相對位置關系,設置對應約束進行布局。

但是 Auto Layout 好心並未做成好事,它的語法又臭又長! 至今學習 iOS 兩年,我使用到原生 Auto Layout 語法的時候屈指可數。只能靠 Masonry 這樣的第三方庫來使用它。

Auto Layout 的原理

說完了 Auto Layout 的使用,再來看看它工作原理。

實際上,我們設置 Auto Layout 的約束,就構成一系列的條件,成為一個方程。然后解出 Frame 的坐標和大小。

例如,我們設置一個名為 A 的 UI :

A.center = super.center
A.width  = 40
A.height = 40

 

則: A.frame = (super.center.x-40/2,super.center.y-40/2,40,40)

再設置一個 B:

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 )

如圖:

 

ZenonHuang_FlexBox_1

 

Cassowary

Auto Layout 內部有專門用來處理約束關系的算法,我一直以為是蘋果自家研發的,查閱資料才發現是來自一個叫 Cassowary 的算法。

Cassowary是個解析工具包,能夠有效解析線性等式系統和線性不等式系統,用戶的界面中總是會出現不等關系和相等關系,Cassowary開發了一種規則系統可以通過約束來描述視圖間關系。約束就是規則,能夠表示出一個視圖相對於另一個視圖的位置。

有興趣的可以進一步了解該算法的實現。

Frame / Auto Layout / FlexBox 的性能對比

在對 Auto Layout 進行一番了解之后,我們很容易得出 Auto Layout 因為多余的計算,性能差於 Frame 的結論。

但究竟差多少呢?FlexBox 的表現又如何呢?

這里根據 從 Auto Layout 的布局算法談性能 里的測試代碼進行修改,對 Frame / Auto Layout / FlexBox 進行布局,分段測算 10 ~ 350 個 UIView 的布局時間。取 100 次布局時間的平均值作為結果,耗時單位為秒。

結果如下圖:

 

ZenonHuang_FlexBox_2

 

雖然測試結果難免有偏差,但是根據折線圖可以明顯發現,FlexBox 的布局性能是比較接近 Frame 的。

60 FPS 作為一個 iOS 流暢度的黃金標准,要求布局在 0.0166667 s 內完成,Auto Layout 在超過 50 個視圖的時候,可能保持流暢就會開始有問題了。

本次測試使用的機器配置如下:

ZenonHuang_FlexBox_3

 

采用 Xcode9.2 ,iPad Pro (12.9-inch)(2nd generation) 模擬器。

測試布局的項目代碼上傳在 GitHub

FlexBox 是什么?

FlexBox 是一種 UI 布局方式,並得到了所有瀏覽器的支持。FlexBox 首先是基於 盒裝狀型 的,Flexible 意味着彈性,使其能適應不同屏幕,補充盒狀模型的靈活性。

FlexBox 把每個視圖,都看作一個矩形盒子,擁有內外邊距,沿着主軸方向排列,並且,同級的視圖之間沒有依賴。

和 Auto Layout 類似,FlexBox 采用了描述性的語言去進行布局,而不像 Frame 直接用絕對值坐標進行布局。

彈性布局的主要思想是讓 Flex Container 有能力來改變 Flex Item 的寬度和高度,以填滿可用空間(主要是為了容納所有類型的顯示設備和屏幕尺寸)的能力。

最重要的是, FlexBox 布局與方向無關,常規的布局設計缺乏靈活性,無法支持大型和復雜的應用程序(特別是涉及到方向轉變,縮放、拉伸和收縮等)。

FlexBox 組成

采用 FlexBox 布局的元素,稱為 Flex Container

Flex Container 的所有子元素,稱為 Flex Item

 

ZenonHuang_FlexBox_4

 

下面會講一下 FlexBox 里面的一些概念,方便之后進行 FlexBox 的使用。

Flex Container

前面提到了,FlexBox 的一個特點,就是視圖之間,是沒有依賴的。

Flex Item 的排布,就依賴於 Flex Container 的屬性設置,而不用相互之間進行設置。

所以先說一下 Flex Containner 的屬性設置。

Flex Direction

FlexBox 有一個 主軸(main axis) 和 側軸(cross axis)的概念。側軸垂直於主軸。

它們可以是水平,也可以是垂直。

主軸默認為 Row , 側軸默認為 Column

 

0DF515D1-1EEF-4C38-9782-F875C1433AE0

 

Flex Direction 決定了 Flex Containner 內的主軸排布方向。

主軸默認為 Row (從左到右):

 

87691D2C-34C3-4805-B960-4D8217717D98

 

同時,也可以設置 RowRevers(從右至左):

1F430BC5-A0BE-474B-9791-23F2B308AEE9

 

Column(從上到下):

AA2DF5F6-1164-4440-ACF2-9897D0D82730

 

ColumnRevers(從下到上):

C33D6321-66C3-4A78-B429-E82B3F83CB6E

 

Flex Wrap

Flex Wrap 決定在軸線上排列不下時,視圖的換行方式。

Flex Wrap 默認設置為 NoWrap,不會換行,一直沿着主軸排列到屏幕之外:

 

9C0FD351-E504-4A6B-A442-E3DE1E084FAC

 

設置為 Wrap ,則空間不足時,自動換行:

 

2C14AAC1-6DDB-4450-B393-5497E0743AFC

 

設置 WrapReverse,則換行方向與 Wrap 相反:

 

35B10621-C2BD-4699-B5B5-4383B35F510E

 

這是一個非常有用的屬性。比如典型的九宮格布局,iOS 如果不是用 UICollectionView 做,那么就需要保存 9個實例,然后做判斷,計算 frame ,可維護性實在不高。使用UICollectionView 可以很好的解決布局,但很多場景並不能復用,做起來也不是特別簡單。

FlexBox 布局的話,用 Flex Wrap 屬性設置 Wrap 就可以直接搞定。

移動平台上相似的方案,比如 Android 的 Linear Layout 和 iOS 的 UIStackView ,但卻遠沒有 FlexBox 強大。

Display

Display 選擇是否計算它,默認為 Flex. 如果設置為 None 自動忽略該視圖的計算。

在根據邏輯顯示 UI 時,比較有用。

比如我們現有的業務,需要顯示的騰訊身份標示。按照一般做法,多個 icon 互相連成一排,根據身份去設置不同的距離,同時隱藏其他 icon ,比較的麻煩。iOS 最好的辦法是使用 UIStackView ,這又有版本兼容等問題。而使用 FlexBox 布局,當不是某個身份時,只要設置 Display 為 None,就不會被納入 UI 計算當中。

Justify Content

Justify Content 用於定義 Flex Item 在主軸上的對齊方式:FlexStart(主軸起點對齊),FlexEnd(主軸終點對齊),Center(居中對齊)。

還有SpaceBetween(兩端對齊):

 

7F4F84F0-6B50-462A-BDA6-D11D087FFCE0

 

設置兩端對齊,讓 Flex Item 之間的間隔相等。

SpaceAround(外邊距相等排列):

 

3B7E08DD-6F78-4A8D-9D56-07565E5F9E24

 

讓每個 Flex Item 四周的外邊距相等

Align Items

Align Items 定義 Flex Item 在側軸上的對齊方式。

Align Items 可以和主軸對齊方式  Justify Content 一樣,設置FlexStart ,FlexEnd,Center,SpaceBetween,SpaceAround 。

Align Items 還可以設置 Baseline(基線對齊):

 

25B54897-E6D4-4742-837E-13E5E4D827DA

 

如圖所示,它是基於 Flex Item 的第一行文字的基線對齊。

如果 Baseline 和 Flex Item 的行內軸與側軸為同一條,則該值與 FlexStart 等效。 其它情況下,該值將參與基線對齊。

Align Items 還可以設置為 Stretch:

F40B6D31-225F-4A99-9209-15886475CC1F

 

Stretch 讓 Flex Item 拉伸填充整個Flex ContainerStretch會使Flex Item的外邊距在遵照對應屬性限制下,盡可能接近所在行或列的尺寸。

如果 Flex Item 未設置數值,或設為 auto,將占滿整個Flex Container的高度

Align Content

Align Content 也是側軸在 Flex Item 里的對齊方式,只不過是以一整個行,作為最小單位。

注意,如果Flex Item只有一根軸線(只有一行的Flex Itme),該屬性不起作用。

調整為 FlexWrap 為 Wrap,效果才顯示出來:

 

6C8FB222-16DC-4B6E-A2D9-251C4CA69F8E

 

Flex Item

在上面說完了 Flex Container 的屬性,終於說到了 Flex Item.  Flex Container 里的屬性,都是作用於自己包含的 Flex ItemFlex Item 的屬性,都是作用於自己本身,.

AlignSelf

AlignSelf 可以讓單個 Flex Item 與其它 Flex Item 有不一樣的對齊方式,覆蓋 Align Items屬性。

默認值為auto,表示繼承Flex ContainerAlign Items屬性。如果它本身沒有Flex Container,則等同於Stretch

FlexGrow

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 倍。

FlexShrink

與 FlexGrow 處理空間剩余相反,FlexShrink 用來處理空間不足的情況。即怎么縮小。

FlexShrink 默認為1,即如果空間不足,該項目將縮小

如果所有 Flex Item 的 FlexShrink 屬性都為 1,當空間不足時,都將等比例縮小。

如果一個 Flex Item 的 FlexShrink 屬性為 0 ,其余 Flex Item 都為1,則空間不足時,FlexShrink 為 0 的前者不縮小。

FlexBasis

FlexBasis 定義了在分配多余的空間之前, Flex Item 占據的 main size(主軸空間)。瀏覽器根據這個屬性,計算主軸是否有多余空間。

FlexBasis 的默認值為 auto,即 Flex Item 的本來大小。

想了解更多 FlexBox 屬性,可以參考 A Complete Guide to Flexbox

FlexBox 的實現 -- Yoga

最開頭已經介紹過,FlexBox 布局已經應用於幾個知名的開源項目,它們用到的就是來自於 Facebook 的 Yoga.

Yoga 是由 C 實現的 Flexbox 布局引擎,性能和穩定性已經在各大項目中得到了很好的驗證,但不足的是 Yoga 只實現了 W3C 標准的一個子集。

下面將針對 Yoga iOS 上的實現 YogaKit 做一些講解。

基於上面對 FlexBox 布局的基本了解,作一些簡單的布局。

YGLayout

整個 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 的實例對象,來直接設置它對應的布局了。

isEnabled

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 樣式.

applyLayoutPreservingOrigin:

對於這個方法,頭文件里是這么解釋的:

/**
 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];

 

效果如下:

 

75F9B6E0-1C63-4A91-97EE-3F3A6087BE3B

 

我們真正的布局代碼,只用設置 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];

 

效果如下:

 

E2BE382B-85DA-47E9-93D3-FD94DD7A1ABA

 

布局代碼只用設置, 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];
            }

 

效果如下:

 

8E963A9D-F9AF-47A6-966C-5AEAA836E598

 

只要設置 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];

 

效果如下 :

 

7221D26D-E78E-4AF3-B129-6D983C842

 

我們只要設置 Flex Container 的 paddingHorizontal ,以及 Flex Item的marginHorizontal,flexGrow 就可以了。並且可以復用  Flex Item 的 layout 布局樣式。

UIScrollView 排列自動計算 contentSize

在 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;

 

效果如下:

 

679CD74E-2C82-4B4F-871A-46D42832C8CB

 

布置 UIScrollView 主要是使用了一個中間 contentView,起到了計算 scrollview 的 contentSize 的作用。這里要注意的是,要在scrollview調用完 applyLayoutPreservingOrigin: 后進行設置,否則得不到結果。

UIScrollView 的用法,目前在網上也沒找到比較官方的示例,完全是筆者自己摸索的,歡迎知道的大佬指教。

上面所用的示例代碼,已經上傳至 GitHub

總結

FlexBox 的確是一個非常適用於移動端的布局方式,語意清晰,性能穩定,現在移動端 UI 視圖越來越復雜,尤其是在所有瀏覽器都已經支持了 FlexBox 之后,作為移動開發者有必要了解新的解決方式。

大家在熟練使用 YogaKit 的方式之后,也可以嘗試自己封裝一套布局代碼,加快開發效率。

參考:

Flex 布局教程:語法篇

FlexBox 布局模型

YogaKit

Yoga Tutorial: Using a Cross-Platform Layout Engine

從 Auto Layout 的布局算法談性能




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM