UI2Code(三)imgcook
imgcook是阿里实现的基于sketch或Ps设计稿,自动生成布局代码的工具,支持生成支持flexbox布局的代码,包括JARVIS、Vue、微信小程序、React、H5、Rax等等。由两部分组成,一个是sketch(Ps)插件,另外一部分是 imgcook平台 。
经过前面的研究,我们知道,UI2Code作为可以从UI截图生成布局代码的工作,其构建流程大致如下:
- 版面分析,包含背景分析和前景分析
- 提取GUI元素
- 组件识别
- 属性提取
- 布局推导
- DSL 推导
- 编译,得到目标平台代码
其中难点是版面分析和布局推导。
版面分析,目的是得到相对准确的背景和前景,通过传统的计算机图像计算和机器学习,将UI图片拆分并分层,得到相对独立的控件,为下一步控件识别和属性提取做准备。可以说,这部分做的好坏直接影响到后面所有的流程。
而对于线上业务逻辑来说,UI图千变万化,背景和前景可能存在很复杂的耦合关系,没有统一的规则约束,这就导致版面分析的难度较大。
- 通过纯图像计算,版面分析不准确,组件间的提取不够独立或被隔断,泛化能力不够
- 通过机器学习识别,控件属性信息不全,没有坐标、宽度等信息,且准确度不够
所以将图像计算与机器学习相结合,通过上述1、2、3、4步骤得到一定泛化能力且相对准确的版面结构和控件信息。
而对于我们来说,1、2、3、4步想要得到的内容,在Sketch设计稿里面都是有提供的,我们只需要想办法将其提取处理就可以,然后进行下一步操作。
所以经过优化后,整个流程如下:
- 提取 Sketch 设计稿信息
- 布局推导
- DSL 推导
- 编译
imgcook 做的就是这样的事情。
实际效果
页面级别 :
设计稿
运行效果
卡片级别 :
设计稿
运行效果
实现拆解
主要分为几个大的过程:
- Sketch -> Json
- Json -> DSL
- DSL -> Code
分别来看下。
sketchToJson
在sketch中选中图层,或者symbol,选中imgcook插件导出,当从sketch导出时,会产生一个json文件,以上面第二个卡片为例,信息如下
其中包含的信息包括:
-
artboardImg,选中symbol的渲染图,如 https://ai-sample.oss-cn-hangzhou.aliyuncs.com/test/b4dd75404f6e11e9b26815c07ac4b122.png
-
控件唯一id
-
控件类型,比如Text、Image、Shape、Repeat
-
控件属性props,包括xy坐标、宽高、背景色、背景圆角、溢出处理(overflow)、图片内容、文字内容、字体和大小、字体颜色、行高、行数等等
-
children属性,这个一般都是铺平的,当时Type是Repeat时会有该值
而在插件处理过程中,可能会有以下过程:
-
没有图层信息,图层信息都被过滤掉
-
平铺的数组,没有层级关系和兄弟关系
-
Repeat包含多个children,内容是Text
-
完全被遮挡或者不可见的控件被过滤
-
复杂的mask计算,对于遮罩的处理
-
控件类型和属性都是依次解析
相当于把sketch文件中的所有信息都处理过后,得到一份期望的json文件,其中基本没有布局层次属性,仅有控件属性,包括大小、位置、控件特有属性等。
布局层次关系及位置关系由下一步来具体确定。
JsonToDSL
把上面的json复制到imgcook平台,会发起gen-layout-process请求,该请求会把json文件当做请求参数上传,并返回一个dsl的Response,请求抓包文件结果经过缩减后大概如下:
{ "type": "Block", "id": "Block-661077", "__VERSION__": "2.0", "mask": true, "props": { "style": { "display": "flex", "alignItems": "flex-start", "flexDirection": "column", "width": 375, "height": 164 }, "attrs": { "x": 0, "y": 0, "className": "block-661077" } }, "children": [ { "__VERSION__": "2.0", "props": { "style": { "display": "flex", "alignItems": "flex-start", "flexDirection": "row", "backgroundColor": "#ffffff", "width": 375, "height": 68 }, "attrs": { "x": 0, "y": 0, "className": "shape-0" } }, "children": [ { "__VERSION__": "2.0", "props": { "style": { "marginTop": 6, "marginLeft": 6, "width": 23, "height": 23 }, "attrs": { "x": 22, "y": 22, "source": "https://ai-sample.oss-cn-hangzhou.aliyuncs.com/test/b46d27404f6e11e98785ebe830f6201d.png", "src": "https://ai-sample.oss-cn-hangzhou.aliyuncs.com/test/b46d27404f6e11e98785ebe830f6201d.png", "className": "image-7" } }, "children": [], "type": "Image", "id": "Image-7", "componentType": "picture", "_jsonId": "Image-7", "_jsonElementId": "Image-7", "title": "Image", "semantic": { "dvc_default": [ { "name": "dvc_default", "level": 100, "result": "img", "prefix": "", "id": 262862 } ], "dvc_picture": [ { "name": "dvc_picture", "level": 88, "result": "zhaoshangbank", "prefix": "", "id": 953603, "choose": true } ] } } ], "type": "Shape", "id": "Shape-0", "__ADAPT__": true, "componentType": "view", "_jsonId": "Shape-0", "_jsonElementId": "Shape-0", "title": "Shape", "semantic": { "dvc_layout": [] } } ], "artboardImg": "https://ai-sample.oss-cn-hangzhou.aliyuncs.com/test/b4dd75404f6e11e9b26815c07ac4b122.png", "name": "可提1 copy", "componentType": "view", "_jsonId": "Block-661077", "_jsonElementId": "Block-661077", "title": "Block", "semantic": { "dvc_default": [ { "name": "dvc_default", "level": 100, "result": "block", "prefix": "", "id": 613351 } ], "dvc_layout": [ { "name": "dvc_layout", "level": 100, "result": "box", "prefix": "", "id": 803050, "choose": true } ] } }
仔细观察该json文件,得出信息:
-
完整的DomTree,嵌套关系明确
-
控件属性props中多了一些信息,包括布局方式(“display”: “flex”)、flex布局方向(flexDirection)、主轴对齐方式(justifyContent)、副轴对齐方式(alignItems)、文本过长处理(text-overflow)、className、margin定位信息(marginTop、marginLeft)
-
语义semantic,是一个数组,最终产生一个className
-
组件类型componentType,包括view、text、picture
这一步完成后,基本就得到了完整可用的布局DSL。
下一步就是compile的过程。
DSL2Code
DSL compile目标代码的过程,imgcook支持比较多的代码模板,JARVIS、Vue、微信小程序、React、H5、Rax。
以Vue为例,发起请求,Response 结果这里不再列出,感兴趣的读者可以自行查看。
其中renderCode包含3部分内容:
-
template,DOMTree,完整的布局信息,支持动态数据绑定
-
script,需要绑定的方法逻辑
-
style,需要绑定的css样式
分别对应vue中的代码块
实际的vue代码:
招商银行 6889 </span>
当前额度 </span>
110,000</span>
</div>
</div>
...
export default {
name: "DvcComponent",
methods: {
onClick_2(){
console.log("test")
},
}
}
.box {
display: flex;
align-items: flex-start;
flex-direction: column;
width: 375px;
height: 164px;
...
从生成的代码中可以看出,div一般使用flex布局。 这里的图片也已经被上传的图床,虽然不可线上使用,但是可以即时的预览。后面可以通过插件导出到本地。
到这里生成的代码基本是可用的,并且在imgcook平台上可以调整样式,绑定方法等操作。
这里的分析是很早以前进行的,数据结构可能已经变更,但大体上应该是相同的。
实现难点
经过上面的分析,基本知道了imgcook的大致流程,个人其中的难点在如下几个方面:
一是布局推导的过程,这块也是整个UI2Code的核心内容。
从准确性和还原过程中的可选项来看,应该是通过计算得到的,其中的布局细节较多,这块也是最重要的点,其结果直接导致页面布局的好坏。
imgcook平台对于这块的实现猜测是基于切割规则加算法来实现flex布局的,整体上来说基本能够实现设计稿上所体现的布局变化,但是有时候也会显得不够灵活,下面会详细说到。
具体如何实现布局推导,可以期待imgcook官方后续有没有说明。
这里只是引用一下闲鱼之前关于切割方面的论述,基本就是先横切再竖切,递归这个过程,在切割之前可以处理坐标嵌套,得到父子信息,然后在父布局中递归切割过程,代码如下:
''' 切割方法 ---------------------------| | | | --------- 30 | | | | | | --------- 60 | | | | —————————— 120 | | | | | | ---------- 130 | | | |__________________________| 如上图所示,第一个切线是60,第二个切线是130 ''' def cut_by_col(cut_num, _im_mask): zero_start = None zero_end = None end_range = len(_im_mask) for x in range(0, end_range): im = _im_mask[x] if len(np.where(im == 255)[0]) == len(im): # 判断是否贯穿整个区域 if zero_start == None: zero_start = x elif zero_start != None and zero_end == None: zero_end = x if zero_start != None and zero_end != None: start = zero_start if start > 0: # 首次非联通区域过滤掉 cut_num.append(start) zero_start = None zero_end = None if x == end_range - 1 and zero_start != None and zero_end == None and zero_start > 0: zero_end = x start = zero_start if start > 0: cut_num.append(start) zero_start = None zero_end = None
切割过后,根据各种优化算法得到实际的布局信息。
当然,实际生产过程肯定要比我猜测的要复杂的多得多。比如什么时候适合flex布局,什么时候适合相对布局,以及谁是优先级更高的,还有是否可以阈值控制来使相对布局变成flex布局等等。
第二个难点是ClassName的语义分析。
猜测是通过机器学习进行OCR图片识别和NLP自然语言处理翻译,这块属于易用性方面的优化。
第三个难点是配套设施,上面分析的都是如何得到一个相对准确的布局DSL,imgcook平台除了这个核心之外,平台给开发者提供了更好的拓展,根据得到的DSL信息,开发者可以根据自己的喜好编译成各种平台、各种语言。另外还有工程化配套的工具,比如导出插件,可以把生成的文件全部导出到本地,不依赖线上图床服务等等。
存在的问题
- 数据绑定,需要手动绑定数据,并且data名字要先想好
-
图床问题,默认是上传至阿里的图床,不可以直接使用,可以使用导出的zip包中包含所有的icon
-
界面布局,简版 flex,基本只有方向,默认margin定位
-
flex 的对齐方式,比如justify-content: space-between属性阈值较低,大概2个像素,不容易被触发
不过imgcook团队一直在做优化工作,相信可以做的越来越好用,造福开发者。
u51-weex
基于以上的分析,和imgcook平台的开放能力,我这边第一时间体验了自定义DSL的功能,由于我们团队内使用weex较多,所以做了一个自定义weex DSL的功能,地址在: https://github.com/imgcook-dsl/u51-weex
具体的使用方式可以参考官方文档: https://imgcook.taobao.org/docs?slug=dsl-dev
理论上来说可以对任意平台的布局进行拓展。
这块也在团队内部做了一些推广,整体上来说可以提高UI方面的开发效率,当然也存在着一些问题,这里就不具体展开了。
总结
整体来讲imgcook是一个基本可用的布局代码生成平台,对于提升工作效率方面有一些帮助。
同时,对于拓展平台支持的兼容性也比较不错。
虽然对于页面级别也可以生成,但是对于代码层级关系、后期代码维护上都不太建议直接上页面级别;比较建议的方式是生成卡片item的布局,导出作为一个component使用。
最后希望更多开发者可以使用imgcook,提出一些建议,使得他们团队可以进行更多的优化。
这篇文章写的时间比较久,当文章发布的时候发现imgcook平台又新添加了 Lab ,期待更多有意思的产品。