为什么要了解 FlexBox?
最近时不时的听到关于 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,super.center.y,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 )
如图:
need-to-insert-img
Cassowary
Auto Layout内部有专门用来处理约束关系的算法,我一直以为是苹果自家研发的,查阅资料才发现是来自一个叫Cassowary的算法。
Cassowary是个解析工具包,能够有效解析线性等式系统和线性不等式系统,用户的界面中总是会出现不等关系和相等关系,Cassowary开发了一种规则系统可以通过约束来描述视图间关系。约束就是规则,能够表示出一个视图相对于另一个视图的位置。
戴铭<深入剖析Auto Layout,分析iOS各版本新增特性>
有兴趣的可以进一步了解该算法的实现。
Frame / Auto Layout / FlexBox 的性能对比
在对Auto Layout进行一番了解之后,我们很容易得出Auto Layout因为多余的计算,性能差于 Frame 的结论。
但究竟差多少呢?FlexBox 的表现又如何呢?
这里根据从 Auto Layout 的布局算法谈性能里的测试代码进行修改,对 Frame / Auto Layout / FlexBox 进行布局,分段测算 10 ~ 350 个 UIView 的布局时间。取 100 次布局时间的平均值作为结果,耗时单位为秒。
结果如下图:
need-to-insert-img
虽然测试结果难免有偏差,但是根据折线图可以明显发现,FlexBox 的布局性能是比较接近 Frame 的。
60 FPS作为一个 iOS 流畅度的黄金标准,要求布局在 0.0166667 s 内完成,Auto Layout在超过 50 个视图的时候,可能保持流畅就会开始有问题了。
本次测试使用的机器配置如下:
need-to-insert-img
采用 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。
need-to-insert-img
下面会讲一下 FlexBox 里面的一些概念,方便之后进行 FlexBox 的使用。
Flex Container
前面提到了,FlexBox的一个特点,就是视图之间,是没有依赖的。
Flex Item的排布,就依赖于Flex Container的属性设置,而不用相互之间进行设置。
所以先说一下Flex Containner的属性设置。
Flex Direction
FlexBox 有一个主轴(main axis)和侧轴(cross axis)的概念。侧轴垂直于主轴。
它们可以是水平,也可以是垂直。
主轴默认为Row, 侧轴默认为Column:
need-to-insert-img
Flex Direction决定了Flex Containner内的主轴排布方向。
主轴默认为 Row (从左到右):
同时,也可以设置 RowRevers(从右至左):
Column(从上到下):
ColumnRevers(从下到上):
Flex Wrap
Flex Wrap 决定在轴线上排列不下时,视图的换行方式。
Flex Wrap 默认设置为 NoWrap,不会换行,一直沿着主轴排列到屏幕之外:
设置为 Wrap ,则空间不足时,自动换行:
need-to-insert-img
设置 WrapReverse,则换行方向与 Wrap 相反:
need-to-insert-img
这是一个非常有用的属性。比如典型的九宫格布局,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(两端对齐):
need-to-insert-img
设置两端对齐,让Flex Item之间的间隔相等。
SpaceAround(外边距相等排列):
need-to-insert-img
让每个Flex Item四周的外边距相等
Align Items
Align Items定义Flex Item在侧轴上的对齐方式。
Align Items可以和主轴对齐方式Justify Content一样,设置FlexStart ,FlexEnd,Center,SpaceBetween,SpaceAround 。
Align Items还可以设置 Baseline(基线对齐):
need-to-insert-img
如图所示,它是基于Flex Item的第一行文字的基线对齐。
如果Baseline和Flex Item的行内轴与侧轴为同一条,则该值与FlexStart等效。 其它情况下,该值将参与基线对齐。
Align Items还可以设置为 Stretch:
need-to-insert-img
Stretch让Flex Item拉伸填充整个Flex Container。Stretch会使Flex Item的外边距在遵照对应属性限制下,尽可能接近所在行或列的尺寸。
如果Flex Item未设置数值,或设为auto,将占满整个Flex Container的高度
Align Content
Align Content也是侧轴在Flex Item里的对齐方式,只不过是以一整个行,作为最小单位。
注意,如果Flex Item只有一根轴线(只有一行的Flex Itme),该属性不起作用。
调整为FlexWrap为Wrap,效果才显示出来:
Flex Item
在上面说完了Flex Container的属性,终于说到了Flex Item.Flex Container里的属性,都是作用于自己包含的Flex Item,Flex Item的属性,都是作用于自己本身,.
AlignSelf
AlignSelf可以让单个Flex Item与其它Flex Item有不一样的对齐方式,覆盖Align Items属性。
默认值为auto,表示继承Flex Container的Align 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 viewsinthe 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];
效果如下:
need-to-insert-img
我们真正的布局代码,只用设置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]; }
效果如下:
need-to-insert-img
只要设置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 排列自动计算 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;
效果如下:
need-to-insert-img
布置UIScrollView主要是使用了一个中间contentView,起到了计算scrollview的contentSize的作用。这里要注意的是,要在scrollview调用完applyLayoutPreservingOrigin:后进行设置,否则得不到结果。
UIScrollView 的用法,目前在网上也没找到比较官方的示例,完全是笔者自己摸索的,欢迎知道的大佬指教。
上面所用的示例代码,已经上传至GitHub
总结
FlexBox 的确是一个非常适用于移动端的布局方式,语意清晰,性能稳定,现在移动端 UI 视图越来越复杂,尤其是在所有浏览器都已经支持了 FlexBox 之后,作为移动开发者有必要了解新的解决方式。
大家在熟练使用 YogaKit 的方式之后,也可以尝试自己封装一套布局代码,加快开发效率。
参考: