聽說你想寫個(gè)渲染引擎 - 布局樹

大家好,我是微微笑的蝸牛,??。

上一篇文章我們講了樣式樹的生成,確定了每個(gè)節(jié)點(diǎn)的樣式。這一節(jié)主要介紹如何生成布局樹,也就是確定每個(gè)節(jié)點(diǎn)的位置。

盒子模型

首先介紹一下盒子模型,因?yàn)椴季侄际且运鼮榛A(chǔ)來展開。

簡單來說,一個(gè)節(jié)點(diǎn)的位置區(qū)域由內(nèi)容區(qū)、內(nèi)邊距、邊框、外邊距四個(gè)部分組成,如下圖所示。

image

盒子類型

元素設(shè)定的 display 類型,決定了盒子類型。常用的有如下幾種:

  • block,塊級元素,生成塊級盒子
  • inline,行內(nèi)元素,生成行內(nèi)級盒子
  • none,表示該元素不顯示

block box

塊盒子,它是一個(gè)容器。默認(rèn)豎向排列,內(nèi)部元素獨(dú)占一行。如下所示:

inline box

行內(nèi)盒子,它也是一個(gè)容器。默認(rèn)橫向排列,在一行內(nèi)展示。當(dāng)一行充滿后,會(huì)折行顯示。如下圖所示:

anonymous box

匿名盒子比較特殊,它不與實(shí)際的元素關(guān)聯(lián),所以稱為匿名。關(guān)于匿名盒子的介紹可點(diǎn)擊文末鏈接 1 查看

因?yàn)楹凶尤萜髦兄荒馨环N類型的盒子,要么是 block,要么是 inline。當(dāng)盒子中混合了不同布局類型的元素時(shí),會(huì)產(chǎn)生匿名盒子。

匿名盒子又分為匿名塊盒子和匿名行內(nèi)盒子。

1. 匿名塊盒子

匿名塊盒子的產(chǎn)生分為兩種情況。

當(dāng) block box 包含 block 和 inline 元素時(shí),會(huì)產(chǎn)生一個(gè)匿名塊盒子,將 inline 元素包裹在其中。

舉個(gè)例子:

div, p {display: block}

<div>Some inline text <p>followed by a paragraph</p> followed by more inline text.</div>

div 和 p 都是塊級元素,前后的文字是行內(nèi)元素。此時(shí)會(huì)產(chǎn)生兩個(gè)匿名塊盒子,分別包裹前后的文字。如下圖所示:

同樣,當(dāng)在 inline box 中包含了 block 元素時(shí),也會(huì)產(chǎn)生匿名塊盒子。

比如:

p    { display: inline }
span { display: block }

<p>
This is anonymous text before the SPAN.
<span>This is the content of SPAN.</span>
This is anonymous text after the SPAN.
</p>
  • p 生成行內(nèi)盒子,span 生成塊級盒子。此時(shí),行內(nèi)盒子包含塊級盒子。
  • "This is anonymous text before the SPAN." 會(huì)產(chǎn)生匿名塊盒子。
  • "This is anonymous text after the SPAN." 也會(huì)產(chǎn)生匿名塊盒子。

如下圖所示:

2. 匿名行內(nèi)盒子

一種比較常見的情況,當(dāng) block box 中包含文字時(shí),會(huì)自動(dòng)生成匿名行內(nèi)盒子包裹文字。

比如:

p    { display: block }

<p>Some <em>emphasized</em> text</p>

<p> 生成了塊盒子,<em> 生成了行內(nèi)盒子。這時(shí)會(huì)生成匿名行內(nèi)盒子,分別包裹 Some 和 text。如下圖所示:

數(shù)據(jù)結(jié)構(gòu)

盒子模型

上邊提到過,盒子模型包括內(nèi)容、內(nèi)邊距、邊框、外邊距幾部分。內(nèi)容區(qū)是一個(gè)矩形,其余部分屬于邊距類型,可分別設(shè)置上下左右的值。

對于內(nèi)容區(qū)來說,定義矩形結(jié)構(gòu):

struct Rect {
    var x: Float = 0.0
    var y: Float = 0.0
    var width: Float = 0.0
    var height: Float = 0.0
}

對于邊距類型,定義如下:

// 邊距定義
struct EdgeSizes {
    var left: Float = 0.0
    var right: Float = 0.0
    var top: Float = 0.0
    var bottom: Float = 0.0
}

那么盒子模型的定義如下:

struct Dimensions {
    // 內(nèi)容區(qū)
    var content: Rect = Rect()
    
    // 內(nèi)邊距
    var padding: EdgeSizes = EdgeSizes()
    
    // 外邊距
    var margin: EdgeSizes = EdgeSizes()
    
    // 邊框
    var border: EdgeSizes = EdgeSizes()
}

接著來定義布局類型,枚舉即可,關(guān)聯(lián)各自的樣式節(jié)點(diǎn)。

// 布局類型
enum BoxType {
    case AnonymousBlock
    case BlockNode(StyleNode)
    case InlineNode(StyleNode)
}

布局樹

布局樹節(jié)點(diǎn)包含盒子模型數(shù)據(jù)、布局類型、子節(jié)點(diǎn)。定義如下:

// 布局樹
class LayoutBox {
    
    // 布局描述
    var dimensions: Dimensions
    
    // 類型
    var boxType: BoxType
    
    // 子節(jié)點(diǎn)布局
    var children: [LayoutBox]
}

布局

元素的布局類型由 display 屬性決定,可從節(jié)點(diǎn)的樣式表中得到。

這里我們只實(shí)現(xiàn) block 節(jié)點(diǎn)的布局,inline 的暫且不處理。block 類型的節(jié)點(diǎn)會(huì)生成塊級盒子,縱向排列。

那么布局究竟是要做些什么呢?其實(shí)主要是確定各個(gè)節(jié)點(diǎn)盒子模型中的數(shù)據(jù),比如寬高、邊距等等。

關(guān)于寬高的處理,有一些前置知識很重要。

節(jié)點(diǎn)的寬度是由父節(jié)點(diǎn)寬度來決定的,是由上至下的處理。而高度卻不一樣,父節(jié)點(diǎn)的高度由子節(jié)點(diǎn)的高度之和決定。它是一個(gè)由下至上的過程,必須等所有子節(jié)點(diǎn)高度計(jì)算出來后,父節(jié)點(diǎn)的高度才能確定。

整個(gè)布局過程,我們將分為 4 個(gè)步驟來處理:

  • 寬度計(jì)算。計(jì)算節(jié)點(diǎn)寬度,確定水平方向間距。
  • 位置計(jì)算。計(jì)算節(jié)點(diǎn)坐標(biāo),確定豎直方向間距。
  • 子節(jié)點(diǎn)布局。確定子節(jié)點(diǎn)布局信息,同時(shí)更新父節(jié)點(diǎn)高度。
  • 高度計(jì)算。獲取 css 設(shè)置的高度。

寬度計(jì)算

寬度計(jì)算是最為復(fù)雜的一環(huán)。因?yàn)檫@跟 width、padding、border、margin 的設(shè)定有關(guān),其中最重要的是 width 和 margin。

另外,width、margin 可以設(shè)置為 auto,表示讓瀏覽器自行計(jì)算它們的值。

  • 當(dāng) width 為 auto 時(shí),表示假若在有邊距的情況下,盡可能的將自身寬度設(shè)置為父容器的寬度。
  • 當(dāng) margin 為 auto 時(shí),表示瀏覽器選擇一個(gè)合適的邊距。

假設(shè)我們將 「width + margin + padding + border 之和」定義為元素所占的總寬度。如下圖所示:

而總寬度和實(shí)際父容器的寬度很可能存在不匹配的情況,要么溢出,要么不足。這時(shí)候就需要根據(jù) width、margin 各自的設(shè)置來進(jìn)行調(diào)整,以滿足需求。

下面,就來講講不同情況下的調(diào)整。

1. 首先做一些前置工作,進(jìn)行數(shù)據(jù)準(zhǔn)備。如下:

  • 從樣式表中提取寬度,若 width 沒有設(shè)置,默認(rèn)值為 auto
  • 從樣式表中提取水平邊距,margin、padding、border 的 left 和 right 值。
  • 計(jì)算總寬度,totalWidth = 所有邊距 + 寬度。
  • 計(jì)算剩余空間,leftSpace = 父容器寬度 - 總寬度。

2. 依據(jù) css 中的計(jì)算方式(詳情可點(diǎn)擊文末鏈接 2),可分為如下幾種情形:

  • 當(dāng) width 不為 auto,且總寬度 > 父容器寬度,將 margin-left/margin-right 中為 auto 的值設(shè)置為 0。

    如果 margin-left 為 auto,那么 margin-left = 0;

    如果 margin-right 為 auto,那么 margin-right = 0;

  • 當(dāng) width/margin-left/margin-right 都有設(shè)置值時(shí)(也就是都不為 auto),根據(jù) block 的 direction 調(diào)整 margin。

    如果是 ltr,則調(diào)整 margin-right;如果是 rtl,則調(diào)整 margin-left。

    這里我們默認(rèn)都是 ltr,只調(diào)整 margin-right,修改其值滿足寬度要求。

  • 當(dāng) width 不為 auto,margin-left/margin-right 僅且只有一個(gè)為 auto 時(shí),將其中為 auto 的屬性設(shè)置為剩余空間。

  • 當(dāng) width 為 auto,將 margin-left/margin-right 中為 auto 的屬性設(shè)置為 0,width 設(shè)置為剩余空間。

  • 當(dāng) width 不為 auto,margin-left 和 margin-right 都為 auto,兩者平分剩余空間。

具體的代碼處理,可查看 Layout.swift 中 calculateBlockWidth 函數(shù)。

3. 在處理完以上幾種情況后,margin、width 的值已經(jīng)確定,此時(shí)可以更新盒子模型數(shù)據(jù),主要是水平方向的數(shù)據(jù)。

位置計(jì)算

這一步主要是計(jì)算坐標(biāo)點(diǎn),以及垂直方向的間距。

垂直方向的間距,分別取出 border、margin、padding 的 top 和 bottom 值即可。

節(jié)點(diǎn)的坐標(biāo),跟父容器的位置有關(guān)。如下圖所示:

上圖紅色虛線框?yàn)樽庸?jié)點(diǎn) 2 的區(qū)域,現(xiàn)在我們要確定子節(jié)點(diǎn) 2 的坐標(biāo)。

對于 x 坐標(biāo)來說,比較好理解。

子節(jié)點(diǎn)的 x = 父容器 x + margin + border + padding

y 坐標(biāo)的計(jì)算如下:

子節(jié)點(diǎn)的 y = 父容器 y + 父容器高度 + margin + border + padding

這里可能有些讓人迷惑,為什么是加上父容器的高度?

block 是縱向排列,不應(yīng)該是計(jì)算出該節(jié)點(diǎn)之前的全部子節(jié)點(diǎn)所占空間嗎?

是的,沒錯(cuò)。其實(shí)這里父容器的高度就是已經(jīng)布局完成的子節(jié)點(diǎn)高度之和,下面的子節(jié)點(diǎn)布局中會(huì)提到。

子節(jié)點(diǎn)布局

這一步遍歷子節(jié)點(diǎn),遞歸計(jì)算子節(jié)點(diǎn)的布局。

// 計(jì)算子節(jié)點(diǎn)布局
func layoutBlockChildren() {
        for child in self.children {
            child.layout(containingBlock: self.dimensions)
                        
            // 計(jì)算整體高度
            self.dimensions.content.height += child.dimensions.marginBox().height
        }
  }

注意這里父節(jié)點(diǎn)高度的計(jì)算,此時(shí)會(huì)累加子節(jié)點(diǎn)的高度。每當(dāng)布局完成一個(gè)節(jié)點(diǎn),就加上它的高度。因此,父節(jié)點(diǎn)的高度是布局好的子節(jié)點(diǎn)高度之和。

高度

若節(jié)點(diǎn)自身設(shè)置了高度,則取其值。

// 如果設(shè)置了 height,則取該值
func calculateBlockHeight() {
    
    if let styleNode = getStyleNode() {
                
        // 獲取設(shè)置的 height
        if let heightValue = styleNode.getValue(name: "height") {
            
            if case Value.Length(let height, .Px) = heightValue {
                self.dimensions.content.height = height
            }
        }
    }
}

生成布局樹

遍歷樣式樹,根據(jù)元素的布局類型,生成布局樹節(jié)點(diǎn),進(jìn)而生成布局樹。

// 遞歸確定每個(gè)節(jié)點(diǎn)的 display 數(shù)據(jù)
mutating func buildLayoutBox(styleNode: StyleNode) -> LayoutBox {
    let root = LayoutBox(styleNode: styleNode)

    for child in styleNode.children {
        switch child.getDisplay() {
        
        case .Block:
            let childLayoutBox = buildLayoutBox(styleNode: child)
            root.children.append(childLayoutBox)
            break
            
        case .Inline:
            let childLayoutBox = buildLayoutBox(styleNode: child)
            
            // inline 元素,找到 container
            let container = root.getInlineContainer()
            container.children.append(childLayoutBox)
            break
            
        default:
            break
        }
    }

    return root
}

上邊的代碼分別處理了 block 和 inline 元素的情況。這里需要注意,關(guān)于 inline 的處理,跟匿名盒子有關(guān)。

當(dāng) block box 中包含了 inline 元素時(shí),會(huì)創(chuàng)建匿名塊盒子包裹 inline 元素。排列在一起的 inline 元素,會(huì)放在同一個(gè)匿名盒子中。

如下圖所示:

如果 block 的最后一個(gè)節(jié)點(diǎn)已經(jīng)是匿名盒子,那么直接使用;否則創(chuàng)建一個(gè)新的盒子插入。

// 獲取 inline 節(jié)點(diǎn)的容器。如果 block 包含一個(gè) inline 節(jié)點(diǎn),它會(huì)創(chuàng)建一個(gè)匿名 block 來包裹該 inline
// 所有在 block 中的排列在一起的 inline 節(jié)點(diǎn),簡單處理,都會(huì)放在一個(gè)匿名 block 中。
func getInlineContainer() -> LayoutBox {

    switch self.boxType {

    case .AnonymousBlock, .InlineNode(_):
        return self
        
    case .BlockNode(_):
        // 取出最后一個(gè)子節(jié)點(diǎn)
        let lastChild = self.children.last
        
        // 如果已經(jīng)是匿名盒子,不做處理,稍后返回
        if case .AnonymousBlock = lastChild?.boxType  {
            
        } else {
            // 生成新的匿名盒子
            let anonymousBlock = LayoutBox(boxType: .AnonymousBlock)
            
            // 添加匿名匿名盒子
            self.children.append(anonymousBlock)
        }
        
        // 返回最后子節(jié)點(diǎn)
        return self.children.last!
    }
}

最后對布局樹進(jìn)行布局,確定節(jié)點(diǎn)位置信息。

完整代碼可查看:https://github.com/silan-liu/tiny-web-render-engine-swift

總結(jié)

這一節(jié)主要介紹了關(guān)于如何確定節(jié)點(diǎn)的布局信息,并生成了一顆布局樹。其中最為復(fù)雜的是寬度的計(jì)算,處理情況有點(diǎn)多。

下一節(jié)將講述如何進(jìn)行繪制,將布局信息轉(zhuǎn)化為像素點(diǎn),敬請期待~

最后

您要是覺得文章有幫助的話,可以點(diǎn)擊下方名片關(guān)注公眾號「微微笑的蝸牛」。

在公眾號聊天框中回復(fù)「蝸牛」,可添加微信進(jìn)行交流~

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容