如何構建一個理想UI程式碼表達的自動化工具?

閒魚技術發表於2018-07-06

作者:閒魚技術-吉豐

基於設計師產出的 Sketch,甚至是一張 PNG,就能自動生成高可維護可擴充套件的 UI 程式碼,質量堪比一位資深前端工程師, 一定是一件讓整個大前端領域都為之尖叫的事情。

出於這樣一個讓人興奮的命題,閒魚團隊打造了 ui-automation 工具 。

背景

如何讓前端,客戶端的 UI 開發更有效率,一直是一個大前端領域熱門話題。
從純手寫 UI 程式碼,到編寫 XML 表達 UI,到所見即所得的 UI 編輯器。每一步都在極大提升著大前端領域的生產效率。
當下,隨著計算機視覺技術,深度學習技術在工程側的大量使用,閒魚團隊的同學們認為,基於當前的技術,是完全能夠完成接近甚至超越資深前端工程師編寫的理想的 UI 表達。

問題

類比基於傳統的掃描演算法和最新的 pix2code 的深度學習技術。它們確實在有些場景下,生成了渲染完全一致的 UI 程式碼,但是往往可維護性可擴充套件性差,除了在簡單的靜態頁面中能有所應用,在大部分需要動態能力的場景,無能為力。
核心的生成的 UI 程式碼質量問題,是之前的這些工具無法跨越的鴻溝。
而閒魚的 ui-automation 最核心的是要解決程式碼質量問題,使得生成的 UI 表達是最理想的,真正解放開發同學。

ui-automation 流程

  1. 資訊提取 => 2. DSL 推導 => 3. 目標平臺程式碼

相比於渲染流程

  1. ui 程式碼 => 2. gpu 渲染 => 3. 畫面

是一個逆向的流程。

資訊提取層

  1. 基於 Skecth 資訊的提取和預處理

    資訊全,精確,但是有冗餘,干擾資訊。
  2. 基於圖片資訊的提取

    資訊乾淨,沒有冗餘資訊。
    

DSL 層

將扁平化的上游資訊,樹形化,同時補充了完整的佈局約束的資訊。

模版層

根據上游的 DSL 資訊, 生成不同平臺的目的碼,如 flutter,weex 等。

undefined

本文重點闡述中間層 DSL 的定義和推導過程

基礎 UI 元素

我們定義了 3 中最基礎的 UI 元素
Shape,Text, Image。

結構上一個基礎 UI 元素有一下幾部分構成:

  1. 內容
  2. 渲染樣式
  3. 佈局樣式

    佈局樣式沿用了經典 flexbox 的模型。
    

DSL 層的輸入

輸入中包含了上述 3 類基本的 UI 元素, 包含它們的內容,它們的渲染樣式,以及相對螢幕的絕對座標和大小,其中層級結構和佈局屬性在演算法的推導中給出。

DSL 層的輸出

輸出是一個類似 dom 樹的結構,有完整的佈局屬性,除了上述三類基礎元素外,還有基礎容器元件,CI 元件,BI 元件。

DSL 分層處理

在 DSL 的推導過程中, 分兩大層

分組層

關注於巨集觀資訊的處理。目標是完成一棵最佳的 ViewTree,以及掃描出足夠的輔助資訊給下一層屬性推導使用。

(1)二元切分
對一組元素進行劃分的時候
任何元素之間可能存在如下兩種關係 1. 父子關係 2. 兄弟關係
根據兩種關係的特點,我們使用了兩個不同的模型來對陣列切割(一分為二)。 1. 父子關係使用重合模型來劃分。
重合模型會突出明顯的若干個 background|foreground 圖層 在 x,y 兩個方向上都重合了剩下的所有元素。 2. 兄弟關係使用投影模型來劃分。
投影模型,通過往一個方向上投影,兄弟關係的元素間 會存在明顯的獨立且連續分佈的規律。兩個方向都可投影的情況下, 優先水平方向。
在對重合模型和投影模型做適度優化後,第一次分組的容錯性,穩定性,準確性得到了極大的提升。

一個簡單的遞迴虛擬碼

Group(...children:View[]){}
type slice = ( views:View[] ) => View[][]  //sliceByOverlaps(views) || sliceByProjection(views)
const regroup = ( views:View[] ) => views.length === 1 ? views[0] : new Group(...slice(views).map(regroup))

經過上步驟切分後, 得到的是一棵標準意義上的二叉樹。

undefined
例一:
注:7 號元素是簡化的處理,實際包含了 3 個基礎元素。
輸入:

[3, 2, 1, 4, 5, 6, 7]

輸出:

[
     3,
    [
        [2, 1],
        [
            4,
            [
                [5, 6],
                7
            ]
        ]
    ]
]

這樣的一棵二叉樹。

(2) 歸併
標準意義上的二叉樹, 並不符合我們的需求,所以需要做一次扁平化的歸併,將同方向的父子節點歸併為一個陣列, 降低樹的深度。
一個簡單的遞迴虛擬碼

const flattenOnDirection = (view:View, parentDir: FlexDirection) => {
    return view.isContainer ?
        ? view.dir != null && view.dir == parentDir
            ? flatten(view.children.map(child => flattenOnDirection(child, view.dir)))
            : [flattenGroup(view)]
        : [view]
}

const flattenGroup = (view: View) => {
    if(view.isContainer) {
        view.children = flatten(node.children.map(l => flattenOnDirection(l, dir)), true)
    }
    return view
}

經過這層處理上例一的樹修正為

[
     3,
    [
        [2, 1],
        4,
        [5, 6],
        7
    ]
]

(3) 排序層
根據容器方向,做一輪左到右,上至下的排序。

(4) 感知輔助線
對 ViewTree 做一次深度遍歷, 掃描出輔助線的資訊,將影響後續的對齊方式的推導,但並不影響 ViewTree 結構。
undefined
如上圖, 灰色框表示基礎元素,紅色框比較容器,黃色虛線表示掃描出輔助線的資訊。因為有輔助線資訊的存在,我們才能讓第 3 行的文字,左對齊,而非右對齊。人的視覺資訊處理亦是如此。

(5) 疏密切分
根據疏密分佈, 在對同一容器下的孩子節點,根據疏密分佈切分。
undefined
如上圖,同在水平方向的兄弟節點,根據疏密關係,分解為左族群和右族群,一個向左對齊,一個向右對齊,中間的剩餘空間是共享的。

(6) 掃描網格分佈資訊
這裡用到圖形相似度的演算法,若干水平行, 每行的元素子樹之間相似。
在垂直方向掃描得到最大不重複的組合, 打破原有層級約束重新組合。
如:
undefined
掃描後, 打破原有層級約束後, 重新組合
undefined

(7) 感知中間線
對 ViewTree 做一次深度遍歷, 掃描出有效的中間線資訊,會影響 ViewTree 結構。

(8) 合併層 1. 合併背景圖層到容器的背景屬性 2. 合併背景圖層到 Text 的背景屬性 3. 合併僅包含一個孩子節點的容器

大致經過上述 8 個小層的處理後, 我們得到了一個理想的 ViewTree。下一步開始我們的屬性推導。

屬性推導層

關注於區域性資訊的深度推演。

(1) 推導每一個容器的方向
推導方向是最獨立的,僅僅依賴於孩子節點的分佈情況。

(2) 推導每一個節點是 在流裡面的,還是脫離流絕對的
這裡依賴一個重合衝突演算法。大體是重合衝突率高的,就是絕對的元素,重合衝突率低的是流式元素。同時存在一定的冗餘能力,允許小部分的重疊(負 margin),這樣極大的提高了線性佈局的動態性。

(3) 推導每一個節點的大小。
以一個盡力撐滿的貪心模型,推匯出每一個元素的大小。同時盡力用屬性約束取代直接給定寬或定高的形式,來達元素大小是到跟隨內容或跟隨孩子節點或跟隨父容器的動態性。
對於一個容器的副軸的大小的處理,會略微複雜些,

(4) 推匯出一些特殊佈局

  1. 網格
  2. 左右對齊佈局

(5) 推導主軸方向對齊方式
優先居中, 其次居左, 最後居右。

(6) 推導副軸方向對齊方式

(7) 推導位置

  1. 流式元素 通過 margin 表示座標。 居中通過(5)(6)推導的 JustifyContent,AlignItems,AlignSelf 等要素描述。
  2. 絕對元素 通過 left, top, bottom, right 等描述座標。居中通過 transform 描述。

(8) 推導 padding

ui-automation 工具目前已經運用在閒魚內部的各個業務場景之中,伴隨著大量的應用,工具本身同樣日益進化。

最後,閒魚技術團隊廣招各類方向的達人,無論你是精通移動端,前端,後臺,還是機器學習,音視訊,自動化測試等,都歡迎投遞簡歷加入我們,一同用技術改善生活!


相關文章