大家好,我是微微笑的蝸牛,??。
上一篇文章我們講了樣式樹的生成,確定了每個(gè)節(jié)點(diǎn)的樣式。這一節(jié)主要介紹如何生成布局樹,也就是確定每個(gè)節(jié)點(diǎn)的位置。
盒子模型
首先介紹一下盒子模型,因?yàn)椴季侄际且运鼮榛A(chǔ)來展開。
簡單來說,一個(gè)節(jié)點(diǎn)的位置區(qū)域由內(nèi)容區(qū)、內(nèi)邊距、邊框、外邊距四個(gè)部分組成,如下圖所示。
盒子類型
元素設(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)行交流~