美文网首页UI布局iOS锦囊iOS开发之常用技术点
iOS 上的 FlexBox(箱式) 布局及yogaKit框架使

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

作者: nevermore_子高 | 来源:发表于2017-12-26 09:34 被阅读408次

    为什么要了解 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 的布局算法谈性能

    相关文章

      网友评论

      • 舒马赫:赞,推荐基于Flexbox的库FlexLib,使用xml文件来写布局:
        github.com/zhenglibao/FlexLib

      本文标题:iOS 上的 FlexBox(箱式) 布局及yogaKit框架使

      本文链接:https://www.haomeiwen.com/subject/lfrdgxtx.html