CSS行内布局(二):line-height

707 阅读11分钟

〇、前言

在上一篇文章《CSS行内布局(一):字体》中,我们深入到字体内部,学习了font-size改变文字大小的原理,以及如何计算行内元素的内容区域的高度。这些知识点都与本文内容相关联,所以我强烈建议尚未阅读过的小伙伴,先看完上一篇文章后再继续下文。

这一篇是本系列最核心的部分,我们将通过学习一个看似简单,实则却很复杂的属性——line-height,来一窥行内布局的究竟。主要将涉及以下内容:

  • 行内布局模型及相关概念,如:根行内框(root inline box),行框(line box)
  • 行高的意义,以及与行框高度的关系
  • 行内框中幽灵般的支柱(strut)字符
  • line-height: normal的真正含义

本文所有描述基于最新草案:CSS Inline Layout Module Level 3

一、测验

由于大家或多或少都使用过line-height,网上相关的文章也很多,所以为了节省大家的时间,我们先来做一个小测验(例1):

代码很简单,问题是:

  1. 你知道为什么<p>的高度是164px,而不是150px吗?
  2. 你知道这个164px的计算过程吗?

如果你对这两个问题的答案都一清二楚,那么恭喜你,你可以关闭这个页面了。相反,如果你并不能回答这两个问题,那么本文能帮你找到答案。

二、定义

先来看下规范中对line-height的定义:

属性名line-height
描述此属性将指定行内框(inline box)的行高。在行内布局中,行高将被用于确定行内框的布局边界(layout bounds),而布局边界会影响行框(line box)的高度
取值类型normal、数字(如:1.3)、带单位的长度(如:100px)、百分比(如:130%)
初始值normal
是否可被继承

line-height直译为行高,可行高又是什么呢?规范中说与布局边界、行框等概念有关,而这些概念都存在于行内布局模型之中。

三、行内布局模型

一切的开始,都要从一个朴素的例子(例2)说起:

例2与例1的代码很相似,只是渲染了一段普通的文本而已,其背后的行内布局模型如下:

inline-layout-model.jpg

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

它的模型图如下:

one-line-model.jpg

接下来,我们就基于例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-familyfont-size等。

4.2 行高

对一个行内框来说,line-height的值就是行高的值。行高是个印刷排版方面的专有名词,但是在不同环境下可能意义略有不同。在CSS中,它表示的意义如下图:

行高与内容区域的高度相减得到一个值,称为行距(leading),行距的一半加在内容区域上方,另一半加在下方,即:

行距 = 行高 - 内容区域
半行距 = 行距/2

行高的上边界与下边界称为布局边界(layout bounds),而上下边界之间的距离就是行框的高度。用通俗的话讲,行框在高度上需要恰巧把上下布局边界给包住。


回到例3,现在已知根行内框的字体为Catamaran、字号为50px、行高为150px,结合Catamaran的UPM、ascent和descent等信息:

字体UPMascentdescent
Catamaran10001100540

我们可以计算出以下根行内框的布局信息:

内容区域 = 字号 / UPM * (ascent + descent)
       = 50 / 1000 * (1100 + 540)
       = 82
       
行距 = 行高 - 内容区域
    = 150 - 82 
    = 68
    
半行距 = 行距 / 2
      = 68 / 2 
      = 34

150.jpg

所以在例3中,行高、行框高度和块框高度都是150px。

关于如何查看字体属性,以及如何计算行内元素的内容区域的高度,请阅读上一篇文章


有时,行高并不一定比内容区域高。当行高小于内容区域时,行距为负值。在这种情况下,会从内容区域上下两边分别减去半个行距。假设将例3中的line-height改成50px,那么根行内框的内容区域高度不变,而行距变成了:

行距:50 - 82 = -32
半行距:-32 / 2 = -16

其渲染出来的结果如下:

50.jpg

此时,行高、行框高度和块框高度都是50px。

五、行框高度

到目前为止,你可能会有一个错觉,以为行框的高度和行高永远相等,然而事实并非如此。看下面这个例子(例4):

在这个例子中,ipsum被包裹在#big元素中,并设置了更大的字号,其他部分都与例3相同。但是,只要审查一下元素,你就会发现<p>的高度变成了164px,而不是150px,这是为什么呢?答案还是在行内布局模型之中,如下:

image.png

对比之前的模型图,这次多了基线以及#big这个元素对应的行内框。

当在行内布局中存在多个行内框时,所有行内框都要先按照基线进行对齐,然后再确定行框的高度。对于根行内框,它的字体、字号和行高等信息都没有发生变化。而对于#big元素,它除了单独设置了字号之外,字体和行高也同样继承自<p>,所以可以计算出它的布局信息:

内容区域:100 / 1000 * (1100 + 540) = 164
行距:150 - 164 = -14
半行距:-14 / 2 = -7

然后,我们将#big行内框的布局边界与根行内框的布局边界合并在一起(为了看得更加清楚,我使用了不同颜色进行标注)。

complicated4.jpg

可以看出,由于字号不同,在按照基线对齐之后,#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: normalline-height: 1.64的效果是一样的。

八、结尾

至此,行内布局中与line-height相关的部分就介绍完了,可是行内布局的故事还没有结束。在本文中,行内元素都是根据基线对齐,但是如果通过vertical-align改变了对齐方式,又会变成什么样呢?另外,行内布局并不仅仅支持一般行内元素,如果加入图片、视频和inline-block元素,又会如何呢?

且听下回分解!