背景
在《UI2CODE--整体设计》篇中,我们提到UI2Code工程的整体流程。前步图片分析之后,我们可以得到对应的DSL布局描述。利用DSL的资讯,结合IntelliJ Plugin介面工具,面向使用者提供生成对应Flutter代码。
本篇主要介绍我们如何处理DSL的资讯,想法上即是Flutter的翻译机。总体概念如下:
输入的DSL是什么?
DSL做为一种描述语言,抽象表示为了解决某一类任务而专门设计的计算机语言。在此我们的DSL代表图像识别和布局识别侧的输出,为一JSON格式。
这些资讯主要描述了这个图层(Layer)的范围(Frame)、是什么样子的类型(Type)、是什么样子的样式(Styles)、含有哪些数据(Value)等等。图层集(Layers)栏位则代表了这张视觉稿的所有图层。
核心思路
本节的目标是将DSL翻译成目标的Flutter代码。我们首先需要理解的是分散的图层间的关系,可能会有交叠、可能是并列排版。知道了关系之后,需想办法转化成Flutter widget的视图,根据此视图来生产对应代码。
架构上我们把DSL tree和Flutter tree的建立,分拆为两个独立的分界。这样比较容易定义问题,并且保持弹性。如果今天的目标语言换成Weex或是iOS UI,我们就只需要更动代码翻译的模组。
第一把刀:DSL tree建立
上图的左侧代表了来源DSL的layers资料,代表者一个一个的图层。右侧是目标的DSL Tree,这棵树的结构上明确叙述了图层之间的包裹、交叠等关系。并且包含了某些特殊关系的节点聚合。
作法上利用每个Layer的Frame,以及所属的类别(文字、图像、容器),利用下面的规则组合树的关系:
- 图层之间的包裹关系,例如某些图层为容器,代表下面是可以挂其他节点的(这边带有背景属性的容器,我们定义称之为Shape)
- 区块式组件(Block, 如ListView/GridView)。可以将图层组成View item的关系
- 闲鱼定义的组件资讯(如CI以及BI),这部份非闲鱼工程可以忽略
- 重复布局(Repeat)的资讯,将相同的图层归类合并,目的为简化树
根据以上我们采用了分层,由大至小的次序将Layer分群合并。另外,在合并时layer之间彼此可能有关联;它们可能同属于Block,也可能同属于某个Repeat。所以对于上面定义的Repeat、BI、Block、CI、Shape都可能有交错的嵌套关系,这是必须要处理的部份。
第二把刀:Flutter tree建立
在Flutter Tree的建构中,核心概念先处理布局。布局的概念如剥洋葱一般,我们先去除四周的padding,然后以人类视觉layout的直觉先尝试横切分,再进行竖切分。
1.先剥洋葱去除padding
2.接著我们的算法会先尝试是否可以横切,如下图我们可以切割成为Row1/ Row2
3.针对Row1在尝试再进行竖切,如下图可以得到Column1/ Column2/ Column3
根据以上切分的规则,我们就可以定义出如Row、Column、Padding的几个节点,以及它们的Parent/ Child关系。将DSL tree同一层的节点做切分,一边切分一边建立Flutter node,遍历完整颗DSL,即可得到粗略的Flutter tree关系。
= 无法切分时的处理
当图层切分不开时,这时候就要使用绝对布局叠层的概念,这个概念在Flutter内称之为Stack。
多个图层在DSL tree的关系为兄弟节点,根据此些图层的Frame,我们判断出来它们是彼此相交的,我们会以Z-order概念,来决定上下交叠的关系。最后,这些图层将组成一个新Stack节点,并且产生此节点的Frames为此些图层覆盖的范围。
= 针对文字的进阶处理
基本上交叠的图层以Stack的处理就可以正确显示,但在文字图层上可能含有误区。
如上图因为文字本身的上下左右是含有padding的,在我们图层的识别时,可能会计算出彼此的frame是交叠的,但实际上UI希望它们并不是Stack关系。
为了解决这个问题,我们引入了一个oriFrame的概念,用文字最原始的像素当做是oriFrame。所以遇到为文字的图层时,我们会先判断本身的oriFrame是否交叠,如果是的话才采用Stack切割,否则就以此oriFrame对原始的frame做修正。
文字还有什么特性?
另外,因为文字的内容通常是动态的,所以拥有了”所见不一定为所得”的特性。这些特性主要包含了是否该换行、内容区域是否可以拉伸、文字Padding等,这些特性都会影响到我们的布局。
以下图为例,我们在处理Layout时肉眼很明显可以知道这些特徵。文字的行数我们可以以视觉稿当做最大显示范本,文字区域的宽度部分,则需要特别判断哪些区域是可以被拉伸的。
确立文字范围
在决定拉伸对象之前,我们需要定义哪些widget是将内容完整显示,不能被拉伸的:如图片、Container容器、Stack区域、Component组件
接著处理的流程如下:
- 首先判断所有Child内是否有多行文字或宽度固定的文字,如果是的话针对其处理。需要加上Flex。
-
若无以上的状况,则判断Child间的Padding关系
-
如果可拉伸的widget的Padding大于平均值的个数有多个,则这些都加上Flex
- 如果只有单个时,则找寻最大Padding的widget(使用分群拉伸算法)
-
- 最后,但当Row里面存有拉伸的状况时,需要把Row的最后一个child加上Right padding,否则拉伸元素会填满父容器。
分群拉伸算法:这个算法的目的是找到最佳拉伸的对象。我们的思考上将Widget做分组,分组后判断整体的Alignment(如左右对齐)或是拉伸关系。若在拉伸状况下,判断适合让哪个组别拉伸,在进一步判断适合让组别的内部元件拉伸。
举例如下为一个Row排列的控件,其中排列为Image、CI、Text1、Text2、Text3:
依据Widget之间的距离,在上图分为了Group1及Group2两个群体。先以Group1判断是否存在可拉伸的对象, 接著才判断Group2。所以这5个Widget分别得到了3, 2, 1, 4, 5的优先级。以本例而言,Text1为最高优先,而且其为可拉伸的,故决定将Flex属性加于此。
在Expanded的处理上,是我们目前遇到最大的困难点,甚至人工判断都可能有歧义。上面的规则是我们归纳出众多视觉稿的通解,但不能100%完全解决问题。所以这部份判断错误的部分,我们期待在Plugin的交互中使用人工解决。
= 判断Alignment优化
以上的处理已经可以正确生成Flutter tree,但是我们想进一步地将Flutter代码更加优雅。在此我们针对了三种元件的Alignment做了处理,分别是Container、Row、Column,其概念都是分析内部元件的padding关系,决定为居左、居中、或是居右对齐。
举例如Column内部的children我们去判断左右的padding是否相等。若是则移除其padding,并且加上crossAxisAlignment为center。
针对Row/ Container我们则会判断crossAxisAlignment(垂直方向)以及mainAxisAlignment(水平方向)。水平部份,这边我们采用更精细的方法,我们利用欧式距离建立一个非监督算法,计算views是更为接近哪一个(居左、居中、居右)。算法这边先不详述,之后再以篇幅介绍。
最后:生成Flutter代码
经过前面的步骤后,最终我们产生了一个Flutter Tree。生成时在节点的定义上,我们分为了两种,分别是View与Layout,以是否可以拥有Child为区别。以下是我们针对Flutter Tree所定义的部份类别:
在节点的定义中,皆存储了各节点的Parent、Child属性。根据这些关系,我们定义每个节点的代码样板,例如FColumn对应的样板为:
new Column(
#{alignment},
children: [
#{children},
]
),
最后我们以Root widget开始遍历整颗树,将每个节点所生成的Flutter代码结合,这样我们就可以得到整个Widget tree的代码了。
数据分离
为了更好的重复利用生成代码,我们把生成的代码和数据再进一步做分离。分离后输出分为代码区以及Data model数据区:
我们切割这些区域的目的为简化Widget tree直观上的代码复杂度,以及将数据抽离,让资料可由外部呼叫传入,以达成动态性。
整体架构回顾
总合以上的概念,工程的细部架构如下:
前面所说的针对文字以及Alignment的处理,在这边我们设计了一个工厂模式,如上图中经过Flutter Tree Builder后,我们可以去遍历整颗Widget tree,在工厂中判断判断符合条件的规则,经过处理去震荡优化原本的Widget tree。在这边未来我们可以不断地加上合适的规则,让Widget tree更加优化。
整体架构使用静态分析的方法,读到此各位可能会有疑问:一些如动态的事件、View的Visibility、Input输入文字框等怎么处理?由于这些动态性在静态分析下无法解决,所以我们增强了Plugin上的编辑性,使用者只要勾选某些属性,即会在生成代码时自动判断,在Flutter自动增加对应的逻辑。以弥补静态图无法处理的问题。
由于UI的灵活性高,十个人写的代码可能有十种不同风格。并且在分析上游的UI2DSL,以及Flutter代码的翻译,某些部份的精确性取决于我们的样本的认知,是否能够在有限的样本内观查出泛化的定律,分析上还是存有很多挑战性。
结合落地业务
在整个UI2CODE的效果中,大约七成以上的页面都可以正确分析出来,剩下的是一些小细节如文字的处理等,基本上我们工具都能够将大框架的处理好,使用者可能只需微小的调整。
UI2CODE案子在内部团队上线后,已经在闲鱼APP内的"玩家页面"采用了自动化生成的代码。在采用自动化工具后,大约减少了三分之二的UI开发时间(因初期还在熟悉工作流程,未来相信可以更快速)。同时,若在客户端大量采用我们工具,还可以让团队的代码结构有一些的规范,让生成工具来规范Widget UI以及Data Binding的框架,一致性以及后续的维护,相信是一个很大的诱因。
并且闲鱼团队近期计画开发一款新的APP,在初期时能够快速开发UI,也将采用我们的工具。期望有更多的业务和经验积累。
后续计画
近期我们推出了第一版UI2CODE,先计画于内部团队使用,利用使用的经验,让我们在叠代之下不断提高准确性。并且,我们正在调研结合NLP以及AST(语法树)的可能性,希望能够产出更有质量的代码。
我们也期望未来能将此工具开放于Flutter community,对于推动整个Flutter技术有所推进。希望能让更多人跟我们一起找寻更有效率的写代码方法,如果有任何想法欢迎与我们交流,我们也持续不断地在进化工具中,谢谢各位的阅读!
本文作者: 闲鱼技术-上叶,余晏
阅读原文
本文为云栖社区原创内容,未经允许不得转载。