〇、前言
在上一篇文章《CSS行内布局(一):字体》中,我们深入到字体内部,学习了font-size
改变文字大小的原理,以及如何计算行内元素的内容区域的高度。这些知识点都与本文内容相关联,所以我强烈建议尚未阅读过的小伙伴,先看完上一篇文章后再继续下文。
这一篇是本系列最核心的部分,我们将通过学习一个看似简单,实则却很复杂的属性——line-height
,来一窥行内布局的究竟。主要将涉及以下内容:
- 行内布局模型及相关概念,如:根行内框(root inline box),行框(line box)
- 行高的意义,以及与行框高度的关系
- 行内框中幽灵般的支柱(strut)字符
line-height: normal
的真正含义
本文所有描述基于最新草案:CSS Inline Layout Module Level 3
一、测验
由于大家或多或少都使用过line-height
,网上相关的文章也很多,所以为了节省大家的时间,我们先来做一个小测验(例1):
代码很简单,问题是:
- 你知道为什么<p>的高度是164px,而不是150px吗?
- 你知道这个164px的计算过程吗?
如果你对这两个问题的答案都一清二楚,那么恭喜你,你可以关闭这个页面了。相反,如果你并不能回答这两个问题,那么本文能帮你找到答案。
二、定义
先来看下规范中对line-height的定义:
属性名 | line-height |
---|---|
描述 | 此属性将指定行内框(inline box)的行高。在行内布局中,行高将被用于确定行内框的布局边界(layout bounds),而布局边界会影响行框(line box)的高度 |
取值类型 | normal、数字(如:1.3)、带单位的长度(如:100px)、百分比(如:130%) |
初始值 | normal |
是否可被继承 | 是 |
line-height
直译为行高,可行高又是什么呢?规范中说与布局边界、行框等概念有关,而这些概念都存在于行内布局模型之中。
三、行内布局模型
一切的开始,都要从一个朴素的例子(例2)说起:
例2与例1的代码很相似,只是渲染了一段普通的文本而已,其背后的行内布局模型如下:
3.1 根行内框
CSS是一个框(box,或称为盒)的世界,几乎所有元素的模型都是一个框,比如:块元素对应块框,行内元素对应行内框。
在行内布局中,所有的文本和行内元素都被放在一个根行内框(root inline box)中。比如例2,你就可以想象成下面这样的一个层级结构:
<p>
<root-inline-box>
Lorem ipsum dolor sit amet, consectetur
</root-inline-box>
</p>
根行内框并不可见,也无法设置其样式,且始终只有一个。当遇到换行时,根行内框与普通行内框一样,会分裂成多个片段(fragment)。所以在例2中,虽然有两行文本,但是根行内框只有一个,每行只是根行内框的一个片段。
3.2 行框
在每一行中,根行内框的片段又被置于一个行框(line box)之中,有几行内容就有几个行框。行框的宽度一般与容器块框的宽度相同,而行框的高度由其内部行内框的布局边界决定,而布局边界又由line-height
计算得到。
另外,行框也不可见,所以无法直接获取行框的高度。不过,因为行框之间相互堆叠起来,中间没有任何空隙(本文暂不考虑存在float元素的情况),也不会相互重叠,所以在普通文档流中,如果容器块框未设置高度,那么块框的高度默认就是其中所有行框的高度之和(如例2的模型图所示)。而当块框中只有一行内容,也就是只有一个行框的时候,块框的高度就是行框的高度。假如我们截取并只渲染例2的第一行,通过element.clientHeight
等API,很容易就能得到<p>的高度为150px,也就是说这一行的行框高度为150px。见例3:
它的模型图如下:
接下来,我们就基于例3来看一下line-height
是如何决定行框高度的。
四、line-height: 150px
4.1 line-height的作用对象
在讲line-height
的作用之前,必须要先明确line-height
的作用对象。
在CSS规范的描述中可以看出,line-height
的作用对象是行内框。但是在以上所有的例子中,我们都是在<p>上设置的line-height
,难道用错了吗?其实并不是。由于line-height
可被继承,所以当我们在<p>上设置line-height
时,这个属性值会被<p>里所有的行内框所继承,包括根行内框。也就是说,line-height: 150px
实际作用的对象是根行内框,而不是<p>本身。与line-height
类似的属性还有font-family
、font-size
等。
4.2 行高
对一个行内框来说,line-height
的值就是行高的值。行高是个印刷排版方面的专有名词,但是在不同环境下可能意义略有不同。在CSS中,它表示的意义如下图:
行高与内容区域的高度相减得到一个值,称为行距(leading),行距的一半加在内容区域上方,另一半加在下方,即:
行距 = 行高 - 内容区域
半行距 = 行距/2
行高的上边界与下边界称为布局边界(layout bounds),而上下边界之间的距离就是行框的高度。用通俗的话讲,行框在高度上需要恰巧把上下布局边界给包住。
回到例3,现在已知根行内框的字体为Catamaran、字号为50px、行高为150px,结合Catamaran的UPM、ascent和descent等信息:
字体 | UPM | ascent | descent |
---|---|---|---|
Catamaran | 1000 | 1100 | 540 |
我们可以计算出以下根行内框的布局信息:
内容区域 = 字号 / UPM * (ascent + descent)
= 50 / 1000 * (1100 + 540)
= 82
行距 = 行高 - 内容区域
= 150 - 82
= 68
半行距 = 行距 / 2
= 68 / 2
= 34
所以在例3中,行高、行框高度和块框高度都是150px。
关于如何查看字体属性,以及如何计算行内元素的内容区域的高度,请阅读上一篇文章
有时,行高并不一定比内容区域高。当行高小于内容区域时,行距为负值。在这种情况下,会从内容区域上下两边分别减去半个行距。假设将例3中的line-height
改成50px,那么根行内框的内容区域高度不变,而行距变成了:
行距:50 - 82 = -32
半行距:-32 / 2 = -16
其渲染出来的结果如下:
此时,行高、行框高度和块框高度都是50px。
五、行框高度
到目前为止,你可能会有一个错觉,以为行框的高度和行高永远相等,然而事实并非如此。看下面这个例子(例4):
在这个例子中,ipsum被包裹在#big
元素中,并设置了更大的字号,其他部分都与例3相同。但是,只要审查一下元素,你就会发现<p>的高度变成了164px,而不是150px,这是为什么呢?答案还是在行内布局模型之中,如下:
对比之前的模型图,这次多了基线以及#big
这个元素对应的行内框。
当在行内布局中存在多个行内框时,所有行内框都要先按照基线进行对齐,然后再确定行框的高度。对于根行内框,它的字体、字号和行高等信息都没有发生变化。而对于#big
元素,它除了单独设置了字号之外,字体和行高也同样继承自<p>,所以可以计算出它的布局信息:
内容区域:100 / 1000 * (1100 + 540) = 164
行距:150 - 164 = -14
半行距:-14 / 2 = -7
然后,我们将#big
行内框的布局边界与根行内框的布局边界合并在一起(为了看得更加清楚,我使用了不同颜色进行标注)。
可以看出,由于字号不同,在按照基线对齐之后,#big
行内框和根行内框虽然有相同的行高,但是布局边界的垂直位置却不同。在这种情况下,行框需要能包下所有的布局边界,所以高度是从根行内框的下布局边界到#big
行内框的上布局边界。再进一步,我们还可以计算出此时行框的高度:
根行内框:
基线到下布局边界 = descent + 半行距
= 50 / 1000 * 540 + 34
= 61
---------------------------------------
#big行内框:
基线到上布局边界 = ascent + 半行距
= 100 / 1000 * 1100 - 7
= 103
---------------------------------------
行框的高度 = 根行内框基线到下布局边界 + #big行内框基线到上布局边界
= 61 + 103
= 164
因此,实际上行框的高度应该是大于或者等于行高。
六、strut
介绍完在复杂情况下的行内布局与行框高度的计算规则,现在,让我们回到本文开头测验的例1。
<p style="
font-family: 'Catamaran', sans-serif;
font-size: 50px;
line-height: 150px;
width: 600px;
outline: 1px solid;
">
<span id="big" style="font-size: 100px;">ipsum</span>
</p>
明明<p>中只有一个#big
元素,为什么<p>元素的高度还是164px,而不是150px了吗?
----- 思考5分钟 -----
因为此时根行内框依然存在,并且CSS规范中规定:
当行内框中不直接包含任何字符时,会向其中插入一个不可见的零宽字符,这个字符称为支柱(strut)。
在这个例子中,根行内框里只有唯一一个子行内框#big
,并没有直接包含任何字符,所以会插入支柱字符。支柱,顾名思义,其作用就是负责撑开行内框的内容区域。也就是说,即使一个行内框不包含任何字符,它的内容区域的高度也不为0。
所以实际上,可以将例1转换成下面这样,两者在行框高度上是一样的。
<p style="
font-family: 'Catamaran', sans-serif;
font-size: 50px;
line-height: 150px;
width: 600px;
outline: 1px solid;
">
strut
<span id="big" style="font-size: 100px;">ipsum</span>
</p>
由于根行内框永远存在以及支柱的缘故,我们可以推导出——当line-height
被设置在块元素上时,根行内框继承了这个值,因此也决定了块元素里的行框的最小高度。
其实,当行内元素中只有备选字体的字符时,也会插入支柱字符。这也是为什么在上一篇文章第四节中,会使用第一个可用字体的ascent和descent计算内容区域高度的原因。
七、line-height: normal
在上面所有的例子中,line-height
都是直接设置一个像素值,但是在日常开发中,为了使行高能随字号动态变化,一般都会把line-height
设置成一个纯数字倍数(如:1.2),此时:
行高 = 字号 * line-height倍数
当字号为50px时,如果line-height: 1.2
,那么行高就是50 * 1.2 = 60px
。
在更多时候,我们甚至不会显式设置line-height
,而是使用其默认值normal
。此时,行高由内容区域的高度和lineGap决定。lineGap是字体内部的一个属性,在上一篇文章的第三节的截图中,你可以找到它。和行距一样,lineGap的一半加在内容区域上方,另一半加在内容区域下方。如下:
也就是说:行高 = 内容区域 + lineGap
不过,大部分常见字体的lineGap都为0。所以一般来说,当line-height: normal
时,行高就等于内容区域的高度。又因为:
内容区域 = 字号 / UPM * (ascent + descent)
以Catamaran字体为例,代入其相关字体属性,我们可以计算出:
行高 = 内容区域 = 字号 / UPM * (ascent + descent)
= 字号 / 1000 * (1100 + 540)
= 字号 * 1.64
所以,对于使用Catamaran字体的行内元素来说,line-height: normal
与line-height: 1.64
的效果是一样的。
八、结尾
至此,行内布局中与line-height
相关的部分就介绍完了,可是行内布局的故事还没有结束。在本文中,行内元素都是根据基线对齐,但是如果通过vertical-align
改变了对齐方式,又会变成什么样呢?另外,行内布局并不仅仅支持一般行内元素,如果加入图片、视频和inline-block
元素,又会如何呢?
且听下回分解!