最近时不时的听到关于 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 的方式以后,也能够尝试本身封装一套布局代码,加快开发效率。