大家好,我是微微笑的蜗牛,🐌。
上一篇文章我们讲了样式树的生成,确定了每个节点的样式。这一节主要介绍如何生成布局树,也就是确定每个节点的位置。
盒子模型
首先介绍一下盒子模型,因为布局都是以它为基础来展开。
简单来说,一个节点的位置区域由内容区、内边距、边框、外边距四个部分组成,如下图所示。
盒子类型
元素设定的 display 类型,决定了盒子类型。常用的有如下几种:
- block,块级元素,生成块级盒子
- inline,行内元素,生成行内级盒子
- none,表示该元素不显示
block box
块盒子,它是一个容器。默认竖向排列,内部元素独占一行。如下所示:
inline box
行内盒子,它也是一个容器。默认横向排列,在一行内展示。当一行充满后,会折行显示。如下图所示:
anonymous box
匿名盒子比较特殊,它不与实际的元素关联,所以称为匿名。关于匿名盒子的介绍可点击文末链接 1 查看。
因为盒子容器中只能包含一种类型的盒子,要么是 block,要么是 inline。当盒子中混合了不同布局类型的元素时,会产生匿名盒子。
匿名盒子又分为匿名块盒子和匿名行内盒子。
1. 匿名块盒子
匿名块盒子的产生分为两种情况。
当 block box 包含 block 和 inline 元素时,会产生一个匿名块盒子,将 inline 元素包裹在其中。
举个例子:
div, p {display: block}
<div>Some inline text <p>followed by a paragraph</p> followed by more inline text.</div>
div 和 p 都是块级元素,前后的文字是行内元素。此时会产生两个匿名块盒子,分别包裹前后的文字。如下图所示:
同样,当在 inline box 中包含了 block 元素时,也会产生匿名块盒子。
比如:
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
生成行内盒子,span
生成块级盒子。此时,行内盒子包含块级盒子。 -
"This is anonymous text before the SPAN."
会产生匿名块盒子。 -
"This is anonymous text after the SPAN."
也会产生匿名块盒子。
如下图所示:
2. 匿名行内盒子
一种比较常见的情况,当 block box 中包含文字时,会自动生成匿名行内盒子包裹文字。
比如:
p { display: block }
<p>Some <em>emphasized</em> text</p>
<p>
生成了块盒子,<em>
生成了行内盒子。这时会生成匿名行内盒子,分别包裹 Some 和 text。如下图所示:
数据结构
盒子模型
上边提到过,盒子模型包括内容、内边距、边框、外边距几部分。内容区是一个矩形,其余部分属于边距类型,可分别设置上下左右的值。
对于内容区来说,定义矩形结构:
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 {
// 内容区
var content: Rect = Rect()
// 内边距
var padding: EdgeSizes = EdgeSizes()
// 外边距
var margin: EdgeSizes = EdgeSizes()
// 边框
var border: EdgeSizes = EdgeSizes()
}
接着来定义布局类型,枚举即可,关联各自的样式节点。
// 布局类型
enum BoxType {
case AnonymousBlock
case BlockNode(StyleNode)
case InlineNode(StyleNode)
}
布局树
布局树节点包含盒子模型数据、布局类型、子节点。定义如下:
// 布局树
class LayoutBox {
// 布局描述
var dimensions: Dimensions
// 类型
var boxType: BoxType
// 子节点布局
var children: [LayoutBox]
}
布局
元素的布局类型由 display 属性决定,可从节点的样式表中得到。
这里我们只实现 block 节点的布局,inline 的暂且不处理。block 类型的节点会生成块级盒子,纵向排列。
那么布局究竟是要做些什么呢?其实主要是确定各个节点盒子模型中的数据,比如宽高、边距等等。
关于宽高的处理,有一些前置知识很重要。
节点的宽度是由父节点宽度来决定的,是由上至下的处理。而高度却不一样,父节点的高度由子节点的高度之和决定。它是一个由下至上的过程,必须等所有子节点高度计算出来后,父节点的高度才能确定。
整个布局过程,我们将分为 4 个步骤来处理:
- 宽度计算。计算节点宽度,确定水平方向间距。
- 位置计算。计算节点坐标,确定竖直方向间距。
- 子节点布局。确定子节点布局信息,同时更新父节点高度。
- 高度计算。获取 css 设置的高度。
宽度计算
宽度计算是最为复杂的一环。因为这跟 width、padding、border、margin 的设定有关,其中最重要的是 width 和 margin。
另外,width、margin 可以设置为 auto,表示让浏览器自行计算它们的值。
- 当 width 为 auto 时,表示假若在有边距的情况下,尽可能的将自身宽度设置为父容器的宽度。
- 当 margin 为 auto 时,表示浏览器选择一个合适的边距。
假设我们将 「width + margin + padding + border 之和」定义为元素所占的总宽度。如下图所示:
而总宽度和实际父容器的宽度很可能存在不匹配的情况,要么溢出,要么不足。这时候就需要根据 width、margin 各自的设置来进行调整,以满足需求。
下面,就来讲讲不同情况下的调整。
1. 首先做一些前置工作,进行数据准备。如下:
- 从样式表中提取宽度,若 width 没有设置,默认值为
auto
。 - 从样式表中提取水平边距,margin、padding、border 的 left 和 right 值。
- 计算总宽度,totalWidth = 所有边距 + 宽度。
- 计算剩余空间,leftSpace = 父容器宽度 - 总宽度。
2. 依据 css 中的计算方式(详情可点击文末链接 2),可分为如下几种情形:
-
当 width 不为 auto,且总宽度 > 父容器宽度,将 margin-left/margin-right 中为
auto
的值设置为 0。如果 margin-left 为 auto,那么 margin-left = 0;
如果 margin-right 为 auto,那么 margin-right = 0;
-
当 width/margin-left/margin-right 都有设置值时(也就是都不为 auto),根据 block 的 direction 调整 margin。
如果是 ltr,则调整 margin-right;如果是 rtl,则调整 margin-left。
这里我们默认都是 ltr,只调整 margin-right,修改其值满足宽度要求。
-
当 width 不为 auto,margin-left/margin-right 仅且只有一个为 auto 时,将其中为 auto 的属性设置为剩余空间。
-
当 width 为 auto,将 margin-left/margin-right 中为 auto 的属性设置为 0,width 设置为剩余空间。
-
当 width 不为 auto,margin-left 和 margin-right 都为 auto,两者平分剩余空间。
具体的代码处理,可查看 Layout.swift 中 calculateBlockWidth 函数。
3. 在处理完以上几种情况后,margin、width 的值已经确定,此时可以更新盒子模型数据,主要是水平方向的数据。
位置计算
这一步主要是计算坐标点,以及垂直方向的间距。
垂直方向的间距,分别取出 border、margin、padding 的 top 和 bottom 值即可。
节点的坐标,跟父容器的位置有关。如下图所示:
上图红色虚线框为子节点 2 的区域,现在我们要确定子节点 2 的坐标。
对于 x 坐标来说,比较好理解。
子节点的 x = 父容器 x + margin + border + padding
y 坐标的计算如下:
子节点的 y = 父容器 y + 父容器高度 + margin + border + padding
这里可能有些让人迷惑,为什么是加上父容器的高度?
block 是纵向排列,不应该是计算出该节点之前的全部子节点所占空间吗?
是的,没错。其实这里父容器的高度就是已经布局完成的子节点高度之和,下面的子节点布局中会提到。
子节点布局
这一步遍历子节点,递归计算子节点的布局。
// 计算子节点布局
func layoutBlockChildren() {
for child in self.children {
child.layout(containingBlock: self.dimensions)
// 计算整体高度
self.dimensions.content.height += child.dimensions.marginBox().height
}
}
注意这里父节点高度的计算,此时会累加子节点的高度。每当布局完成一个节点,就加上它的高度。因此,父节点的高度是布局好的子节点高度之和。
高度
若节点自身设置了高度,则取其值。
// 如果设置了 height,则取该值
func calculateBlockHeight() {
if let styleNode = getStyleNode() {
// 获取设置的 height
if let heightValue = styleNode.getValue(name: "height") {
if case Value.Length(let height, .Px) = heightValue {
self.dimensions.content.height = height
}
}
}
}
生成布局树
遍历样式树,根据元素的布局类型,生成布局树节点,进而生成布局树。
// 递归确定每个节点的 display 数据
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 元素的情况。这里需要注意,关于 inline 的处理,跟匿名盒子有关。
当 block box 中包含了 inline 元素时,会创建匿名块盒子包裹 inline 元素。排列在一起的 inline 元素,会放在同一个匿名盒子中。
如下图所示:
如果 block 的最后一个节点已经是匿名盒子,那么直接使用;否则创建一个新的盒子插入。
// 获取 inline 节点的容器。如果 block 包含一个 inline 节点,它会创建一个匿名 block 来包裹该 inline
// 所有在 block 中的排列在一起的 inline 节点,简单处理,都会放在一个匿名 block 中。
func getInlineContainer() -> LayoutBox {
switch self.boxType {
case .AnonymousBlock, .InlineNode(_):
return self
case .BlockNode(_):
// 取出最后一个子节点
let lastChild = self.children.last
// 如果已经是匿名盒子,不做处理,稍后返回
if case .AnonymousBlock = lastChild?.boxType {
} else {
// 生成新的匿名盒子
let anonymousBlock = LayoutBox(boxType: .AnonymousBlock)
// 添加匿名匿名盒子
self.children.append(anonymousBlock)
}
// 返回最后子节点
return self.children.last!
}
}
最后对布局树进行布局,确定节点位置信息。
完整代码可查看:https://github.com/silan-liu/tiny-web-render-engine-swift
总结
这一节主要介绍了关于如何确定节点的布局信息,并生成了一颗布局树。其中最为复杂的是宽度的计算,处理情况有点多。
下一节将讲述如何进行绘制,将布局信息转化为像素点,敬请期待~
最后
您要是觉得文章有帮助的话,可以点击下方名片关注公众号「微微笑的蜗牛」。
在公众号聊天框中回复「蜗牛」,可添加微信进行交流~