我们开源了FlexLayout
iOS开发中的UI位置计算是一直是一件令人头痛的事情,尤其是对于比较复杂的页面,一个UI元素的位置的计算需要依赖其它UI元素,而且布局规则复杂,边界条件很多,这种情况下写出来的代码不仅难以阅读,而且更难维护,相信很多iOS程序员都曾遇到过这种情况。
FlexLayout是我们团队研发的一套基于CSS FlexBox模型的相对布局系统,它可以有效的降低View位置计算的复杂度。在iOS上实现真正意义上的相对布局。
FlexLayout提供一组声明式(declarative)的API来描述一个UI组件(Node或Component)的结构,属性,相对位置等信息;通过一个pure function将数据映射为一组对数据的描述;开发者只需要像设计师一样描绘页面”长什么样”而不用思考具体的实现方式。
俗话说:”Talk is cheap, show me the code”,我们以一个实际的应用场景来说明如何使用FlexLayout
如上图cell,要求高度根据内容动态变化,名字长度自适应,名字和时间两端对齐,但是评价星星始终跟在名字后面,同时又不能压缩它后面的时间label。
使用FlexLayout
像React一样思考
接下来我们讨论如何使用FlexLayout来描述这个cell,就像React中的Component,第一步是思考如何将cell切分成若干个Component,以上面的布局为例,我们首将其先切成几个大的node,如下图所示
用代码描述
接下来我们开始用代码描述每个node,习惯上,我们先从粒度最小的node开始,自底向上构建,以上面的cell为例,我们先从第一个红框开始
using namespace o2o::flex;
- (FlexLayout )titleLayout:(NSString* )name Time:(NSString* )time Score:(float)score{
return FlexLayout
//children元素之间的间隔
.direction = FlexDirection::Horizontal,
.spacing = 5,
.alignItems = FlexAlign::Center,//children元素垂直居中
.children = {
{
//姓名
.content = TextNode{
.text = name,
.font = [UIFont systemFontOfSize:14.0f],
.color = [UIColor blackColor],
}
},
{
//星星
.viewBuilder = ^{
O2OStarView* starView = [[O2OStarView alloc] initWithOrigin:CGPointMake(0, 0) viewType:O2OStarViewTypeForDisplay starWidth:14 starMargin:0 starNumber:5];
starView.score = score;
return starView;
},
.width = 70,
.height = 12,
.flexShrink = 0,
},
{
//时间
.content = TextNode{
.text = time,
.font = [UIFont systemFontOfSize:12.0f],
.color = [UIColor grayColor],
},
.flexShrink = 0,
.marginLeft = Auto
}
}
};
}
如果熟悉flex,那么上面的代码应该一目了然,无需过多解释。对于不熟悉flex模型的,上面代码做了这么几件事:
- 首先定义了一个容器Node,它的布局方向是水平方向(Horizontal),有3个子Node(children),他们之间的间隔(spacing)是5,指定子元素垂直方向上的布局方式(alignItems)是居中(FlexAlign::Center)
- 第一个子元素是姓名,用在FlexLayout中用TextNode描述
- 第二个子元素是评价的星星,由于这个View不属于UIKit原生组件,是一个自定义view,这种情况FlexLayout提供一个ViewBuiler用来创建自定义View
- 第三个子元素是时间,同样使用TextNode
使用flexShrink
根据之前的约定,我们需要星星一直跟在名字的后面,当名字过长时,自身不被压缩,同时后面的时间node也不被压缩。实现这个规则,如果使用UIKit,计算会变得很复杂,要考虑名字的长度,星星的长度,时间的文字长度,以及他们之间的间隔,还要考虑最右边的padding,实现起来要定义一些局部变量来保存各种计算结果,还要通过复杂的过程性的语句来实现上面的规则,代码可读性和维护性都很糟糕。有了FlexLayout,只需要指定星星和时间的flexShrink为0,表示当总体宽度不够时,不压缩自身宽度,那么只有名字的长度会被压缩,如下图所示:
定义Pure Function
了解了FlexLayout的布局语法,我们需要一个Pure Function将数据映射为一个完整的FlexLayout:
- (FlexLayout)layoutForModel:(CModel* )model{
return FlexLayout {
.backColor = "white",
.padding = 10,
.spacing = 10,
.children = {
{
.content = ImageNode{.image = model.image},
.width = 40,
.height = 40,
.cornerRadius = 20,
.flexShrink = 0,
},
{
.direction = FlexDirection::Vertical,
.flexGrow = 1,
.spacing = 6,
.children = {
[self titleLayout:model.name Time:model.time Score:model.score],
{.content = TextNode{
.lines = 0,
.text = model.sign,
.font = [UIFont systemFontOfSize:14.0f],
.color = [UIColor lightGrayColor]
}}
}
}
}
};
}
至此,我们的工作就做完了,剩下的工作就完全交给FlexLayout了,FlexLayout会帮你创建view,绑定属性,计算位置。
关于State与Side Effect:如果熟悉React,应该知道props和state的概念,对于FlexLayout来说,Props可以类比上面方法中的CModel*,而state在FlexLayout中没有对应的实现,原因是Pure Function没有办法真正的规避Side Effect,只能通过约定,这显然不是一个很好的方式,我们的另一个项目会在设计上完善这种情况
声明式,没有计算
上面代码中,如果使用传统方式,那么我们需要实现sizeThatFits:先算出view的高度,然后再通过layoutSubViews逐个计算出元素的位置,实际项目中这样的代码很常见也非常难以维护,而使用FlexLayout我们无需关心每个view的计算过程。Flexlayout的语法是declarative的,使整个页面的结构变的清晰易读,代码也容易维护。
结论
我们在支付宝口碑的业务中,很多地方都使用了FlexLayout,以商家中心券为例,对于如此复杂的页面,代码量比传统方式减少了20%,而且可读性和维护性都提升了,最为重要的是,作为程序员,看到这样的代码能让我们心情愉悦,高效率的coding
附录
关于Flexlayout的实现
FlexLayout的核心是实现Flex布局模型的算法,关于Flex布局模型的算法,Github上有很多种实现,对于客户端来说,C语言的版本是可以直接移植的,但是经过我们的多次试验,发现css_layout自身有一些局限性,因此我们参照FlexBox官网提供的说明,自行实现了一套标准的算法,和css_layout的区别可以参考这里。
FlexLayout使用C++编写,里面除了使用了常用的C++集合类,模板类之外,还大量使用了C++ 11的一些新特性,例如unorderd_map,统一初始化函数{},lambda表达式,右值引用等等。其中统一初始化函数( Aggregate initialization)给FlexLayout的API设计提供了极大的灵活性,C++的各类容器提供了严格的类型检查,不会发生运行时的类型错误,Template为struct提供了默认值的实现能力,等等
关于FlexLayout的未来
仅仅创建一套声明式的相对布局系统并不是我们的终极目标,我们的另一个项目”FNode”在FlexLayout的基础上构建了一套Functional UI,像React一样通过构建一个个component完成页面的展现,同时引入单向数据流保持业务逻辑简单清晰,敬请期待
FlexLayout和css_layout关于Flex模型实现的比较
特性 | css-layout | VZFlexLayout | 备注 |
---|---|---|---|
flex-direction | 支持 | 支持 | |
flex-wrap | 支持 | 支持 | 均不支持 wrap-reverse |
align-items, align-self, justify-content | 支持 | 支持 | |
align-content | 部分支持(不支持 space-between 和 space-around) | 支持 | |
flex | 部分支持(合并 flex-grow 与 flex-shrink ,不支持修改 flex-basis) | 完整支持 flex-basis, flex-grow, flex-shrink | |
固定布局元素 | 支持(使用 left, right, top, bottom 指定元素位置和大小) | 支持(使用 width, height, margin 控制位置和大小,可以自动计算宽高、自动居中,更为灵活) | |
RTL布局方向 | 支持 | 不支持 | |
width/height | 支持(有些情况下对 auto 的计算不正确) | 支持 | |
min-width/max-width, min-height/max-height | 支持 | 支持 | |
margin | 支持 | 支持(支持设置为 auto) | |
padding, border-width | 支持 | 支持 | |
spacing, lineSpacing | 不支持 | 支持 | 非css特性 |