UI布局iOS锦囊iOS开发之常用技术点

iOS 上的 FlexBox(箱式) 布局及yogaKit框架使

2017-12-26  本文已影响408人  nevermore_子高

为什么要了解 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 的方式之后,也可以尝试自己封装一套布局代码,加快开发效率。

参考:

Flex 布局教程:语法篇

FlexBox 布局模型

YogaKit

Yoga Tutorial: Using a Cross-Platform Layout Engine

从 Auto Layout 的布局算法谈性能

上一篇下一篇

猜你喜欢

热点阅读