背景
在《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: <Widget>[
#{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 技术有所推进。希望能让更多人跟我们一起找寻更有效率的写代码方法,如果有任何想法欢迎与我们交流,我们也持续不断地在进化工具中,谢谢各位的阅读!
本文作者:闲鱼技术 - 上叶,余晏阅读原文
本文为云栖社区原创内容,未经允许不得转载。