UI2Code智能生成Flutter代码——机器生成代码
背景
在《UI2CODE--整體設(shè)計(jì)》篇中,我們提到UI2Code工程的整體流程。前步圖片分析之后,我們可以得到對(duì)應(yīng)的DSL布局描述。利用DSL的資訊,結(jié)合IntelliJ Plugin介面工具,面向使用者提供生成對(duì)應(yīng)Flutter代碼。
本篇主要介紹我們?nèi)绾翁幚鞤SL的資訊,想法上即是Flutter的翻譯機(jī)。總體概念如下:
輸入的DSL是什么?
DSL做為一種描述語言,抽象表示為了解決某一類任務(wù)而專門設(shè)計(jì)的計(jì)算機(jī)語言。在此我們的DSL代表圖像識(shí)別和布局識(shí)別側(cè)的輸出,為一JSON格式。
這些資訊主要描述了這個(gè)圖層(Layer)的范圍(Frame)、是什么樣子的類型(Type)、是什么樣子的樣式(Styles)、含有哪些數(shù)據(jù)(Value)等等。圖層集(Layers)欄位則代表了這張視覺稿的所有圖層。
核心思路
本節(jié)的目標(biāo)是將DSL翻譯成目標(biāo)的Flutter代碼。我們首先需要理解的是分散的圖層間的關(guān)系,可能會(huì)有交疊、可能是并列排版。知道了關(guān)系之后,需想辦法轉(zhuǎn)化成Flutter widget的視圖,根據(jù)此視圖來生產(chǎn)對(duì)應(yīng)代碼。
架構(gòu)上我們把DSL tree和Flutter tree的建立,分拆為兩個(gè)獨(dú)立的分界。這樣比較容易定義問題,并且保持彈性。如果今天的目標(biāo)語言換成Weex或是iOS UI,我們就只需要更動(dòng)代碼翻譯的模組。
第一把刀:DSL tree建立
上圖的左側(cè)代表了來源DSL的layers資料,代表者一個(gè)一個(gè)的圖層。右側(cè)是目標(biāo)的DSL Tree,這棵樹的結(jié)構(gòu)上明確敘述了圖層之間的包裹、交疊等關(guān)系。并且包含了某些特殊關(guān)系的節(jié)點(diǎn)聚合。
作法上利用每個(gè)Layer的Frame,以及所屬的類別(文字、圖像、容器),利用下面的規(guī)則組合樹的關(guān)系:
根據(jù)以上我們采用了分層,由大至小的次序?qū)ayer分群合并。另外,在合并時(shí)layer之間彼此可能有關(guān)聯(lián);它們可能同屬于Block,也可能同屬于某個(gè)Repeat。所以對(duì)于上面定義的Repeat、BI、Block、CI、Shape都可能有交錯(cuò)的嵌套關(guān)系,這是必須要處理的部份。
第二把刀:Flutter tree建立
在Flutter Tree的建構(gòu)中,核心概念先處理布局。布局的概念如剝洋蔥一般,我們先去除四周的padding,然后以人類視覺layout的直覺先嘗試橫切分,再進(jìn)行豎切分。
1.先剝洋蔥去除padding
2.接著我們的算法會(huì)先嘗試是否可以橫切,如下圖我們可以切割成為Row1/ Row2
3.針對(duì)Row1在嘗試再進(jìn)行豎切,如下圖可以得到Column1/ Column2/ Column3
根據(jù)以上切分的規(guī)則,我們就可以定義出如Row、Column、Padding的幾個(gè)節(jié)點(diǎn),以及它們的Parent/ Child關(guān)系。將DSL tree同一層的節(jié)點(diǎn)做切分,一邊切分一邊建立Flutter node,遍歷完整顆DSL,即可得到粗略的Flutter tree關(guān)系。
= 無法切分時(shí)的處理
當(dāng)圖層切分不開時(shí),這時(shí)候就要使用絕對(duì)布局疊層的概念,這個(gè)概念在Flutter內(nèi)稱之為Stack。
多個(gè)圖層在DSL tree的關(guān)系為兄弟節(jié)點(diǎn),根據(jù)此些圖層的Frame,我們判斷出來它們是彼此相交的,我們會(huì)以Z-order概念,來決定上下交疊的關(guān)系。最后,這些圖層將組成一個(gè)新Stack節(jié)點(diǎn),并且產(chǎn)生此節(jié)點(diǎn)的Frames為此些圖層覆蓋的范圍。
= 針對(duì)文字的進(jìn)階處理
基本上交疊的圖層以Stack的處理就可以正確顯示,但在文字圖層上可能含有誤區(qū)。
如上圖因?yàn)槲淖直旧淼纳舷伦笥沂呛衟adding的,在我們圖層的識(shí)別時(shí),可能會(huì)計(jì)算出彼此的frame是交疊的,但實(shí)際上UI希望它們并不是Stack關(guān)系。
?
為了解決這個(gè)問題,我們引入了一個(gè)oriFrame的概念,用文字最原始的像素當(dāng)做是oriFrame。所以遇到為文字的圖層時(shí),我們會(huì)先判斷本身的oriFrame是否交疊,如果是的話才采用Stack切割,否則就以此oriFrame對(duì)原始的frame做修正。
文字還有什么特性?
另外,因?yàn)槲淖值膬?nèi)容通常是動(dòng)態(tài)的,所以擁有了”所見不一定為所得”的特性。這些特性主要包含了是否該換行、內(nèi)容區(qū)域是否可以拉伸、文字Padding等,這些特性都會(huì)影響到我們的布局。
以下圖為例,我們?cè)谔幚鞮ayout時(shí)肉眼很明顯可以知道這些特徵。文字的行數(shù)我們可以以視覺稿當(dāng)做最大顯示范本,文字區(qū)域的寬度部分,則需要特別判斷哪些區(qū)域是可以被拉伸的。
確立文字范圍
在決定拉伸對(duì)象之前,我們需要定義哪些widget是將內(nèi)容完整顯示,不能被拉伸的:如圖片、Container容器、Stack區(qū)域、Component組件
接著處理的流程如下:
分群拉伸算法:這個(gè)算法的目的是找到最佳拉伸的對(duì)象。我們的思考上將Widget做分組,分組后判斷整體的Alignment(如左右對(duì)齊)或是拉伸關(guān)系。若在拉伸狀況下,判斷適合讓哪個(gè)組別拉伸,在進(jìn)一步判斷適合讓組別的內(nèi)部元件拉伸。
舉例如下為一個(gè)Row排列的控件,其中排列為Image、CI、Text1、Text2、Text3:
依據(jù)Widget之間的距離,在上圖分為了Group1及Group2兩個(gè)群體。先以Group1判斷是否存在可拉伸的對(duì)象, 接著才判斷Group2。所以這5個(gè)Widget分別得到了3, 2, 1, 4, 5的優(yōu)先級(jí)。以本例而言,Text1為最高優(yōu)先,而且其為可拉伸的,故決定將Flex屬性加于此。
在Expanded的處理上,是我們目前遇到最大的困難點(diǎn),甚至人工判斷都可能有歧義。上面的規(guī)則是我們歸納出眾多視覺稿的通解,但不能100%完全解決問題。所以這部份判斷錯(cuò)誤的部分,我們期待在Plugin的交互中使用人工解決。
= 判斷Alignment優(yōu)化
以上的處理已經(jīng)可以正確生成Flutter tree,但是我們想進(jìn)一步地將Flutter代碼更加優(yōu)雅。在此我們針對(duì)了三種元件的Alignment做了處理,分別是Container、Row、Column,其概念都是分析內(nèi)部元件的padding關(guān)系,決定為居左、居中、或是居右對(duì)齊。
舉例如Column內(nèi)部的children我們?nèi)ヅ袛嘧笥业膒adding是否相等。若是則移除其padding,并且加上crossAxisAlignment為center。
針對(duì)Row/ Container我們則會(huì)判斷crossAxisAlignment(垂直方向)以及mainAxisAlignment(水平方向)。水平部份,這邊我們采用更精細(xì)的方法,我們利用歐式距離建立一個(gè)非監(jiān)督算法,計(jì)算views是更為接近哪一個(gè)(居左、居中、居右)。算法這邊先不詳述,之后再以篇幅介紹。
最后:生成Flutter代碼
經(jīng)過前面的步驟后,最終我們產(chǎn)生了一個(gè)Flutter Tree。生成時(shí)在節(jié)點(diǎn)的定義上,我們分為了兩種,分別是View與Layout,以是否可以擁有Child為區(qū)別。以下是我們針對(duì)Flutter Tree所定義的部份類別:
在節(jié)點(diǎn)的定義中,皆存儲(chǔ)了各節(jié)點(diǎn)的Parent、Child屬性。根據(jù)這些關(guān)系,我們定義每個(gè)節(jié)點(diǎn)的代碼樣板,例如FColumn對(duì)應(yīng)的樣板為:
?
new Column( #{alignment}, children: <Widget>[#{children}, ]),?
最后我們以Root widget開始遍歷整顆樹,將每個(gè)節(jié)點(diǎn)所生成的Flutter代碼結(jié)合,這樣我們就可以得到整個(gè)Widget tree的代碼了。
數(shù)據(jù)分離
為了更好的重復(fù)利用生成代碼,我們把生成的代碼和數(shù)據(jù)再進(jìn)一步做分離。分離后輸出分為代碼區(qū)以及Data model數(shù)據(jù)區(qū):
我們切割這些區(qū)域的目的為簡(jiǎn)化Widget tree直觀上的代碼復(fù)雜度,以及將數(shù)據(jù)抽離,讓資料可由外部呼叫傳入,以達(dá)成動(dòng)態(tài)性。
整體架構(gòu)回顧
總合以上的概念,工程的細(xì)部架構(gòu)如下:
前面所說的針對(duì)文字以及Alignment的處理,在這邊我們?cè)O(shè)計(jì)了一個(gè)工廠模式,如上圖中經(jīng)過Flutter Tree Builder后,我們可以去遍歷整顆Widget tree,在工廠中判斷判斷符合條件的規(guī)則,經(jīng)過處理去震蕩優(yōu)化原本的Widget tree。在這邊未來我們可以不斷地加上合適的規(guī)則,讓W(xué)idget tree更加優(yōu)化。
整體架構(gòu)使用靜態(tài)分析的方法,讀到此各位可能會(huì)有疑問:一些如動(dòng)態(tài)的事件、View的Visibility、Input輸入文字框等怎么處理?由于這些動(dòng)態(tài)性在靜態(tài)分析下無法解決,所以我們?cè)鰪?qiáng)了Plugin上的編輯性,使用者只要勾選某些屬性,即會(huì)在生成代碼時(shí)自動(dòng)判斷,在Flutter自動(dòng)增加對(duì)應(yīng)的邏輯。以彌補(bǔ)靜態(tài)圖無法處理的問題。
由于UI的靈活性高,十個(gè)人寫的代碼可能有十種不同風(fēng)格。并且在分析上游的UI2DSL,以及Flutter代碼的翻譯,某些部份的精確性取決于我們的樣本的認(rèn)知,是否能夠在有限的樣本內(nèi)觀查出泛化的定律,分析上還是存有很多挑戰(zhàn)性。
結(jié)合落地業(yè)務(wù)
在整個(gè)UI2CODE的效果中,大約七成以上的頁面都可以正確分析出來,剩下的是一些小細(xì)節(jié)如文字的處理等,基本上我們工具都能夠?qū)⒋罂蚣艿奶幚砗?#xff0c;使用者可能只需微小的調(diào)整。
UI2CODE案子在內(nèi)部團(tuán)隊(duì)上線后,已經(jīng)在閑魚APP內(nèi)的"玩家頁面"采用了自動(dòng)化生成的代碼。在采用自動(dòng)化工具后,大約減少了三分之二的UI開發(fā)時(shí)間(因初期還在熟悉工作流程,未來相信可以更快速)。同時(shí),若在客戶端大量采用我們工具,還可以讓團(tuán)隊(duì)的代碼結(jié)構(gòu)有一些的規(guī)范,讓生成工具來規(guī)范Widget UI以及Data Binding的框架,一致性以及后續(xù)的維護(hù),相信是一個(gè)很大的誘因。
并且閑魚團(tuán)隊(duì)近期計(jì)畫開發(fā)一款新的APP,在初期時(shí)能夠快速開發(fā)UI,也將采用我們的工具。期望有更多的業(yè)務(wù)和經(jīng)驗(yàn)積累。
后續(xù)計(jì)畫
近期我們推出了第一版UI2CODE,先計(jì)畫于內(nèi)部團(tuán)隊(duì)使用,利用使用的經(jīng)驗(yàn),讓我們?cè)诏B代之下不斷提高準(zhǔn)確性。并且,我們正在調(diào)研結(jié)合NLP以及AST(語法樹)的可能性,希望能夠產(chǎn)出更有質(zhì)量的代碼。
我們也期望未來能將此工具開放于Flutter community,對(duì)于推動(dòng)整個(gè)Flutter技術(shù)有所推進(jìn)。希望能讓更多人跟我們一起找尋更有效率的寫代碼方法,如果有任何想法歡迎與我們交流,我們也持續(xù)不斷地在進(jìn)化工具中,謝謝各位的閱讀!
原文鏈接
本文為云棲社區(qū)原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。
總結(jié)
以上是生活随笔為你收集整理的UI2Code智能生成Flutter代码——机器生成代码的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开发跨平台app推荐React Nati
- 下一篇: 阿里云云效如何保障双11大型项目管理