当前位置: 首页 > Web前端 > CSS

深入理解CSS:fontmetrics,line-heightandvertical-align

时间:2023-03-30 18:28:45 CSS

本文为饥饿谷讲师方方原创文章,首发于前端学习指南。这是一个翻译,对于inline和inline-block的元素分析非常强大。原文:DeepdiveCSS:fontmetrics,line-heightandvertical-align-VincentDeOliveiraline-height和vertical-align是如此简单的CSS属性,大多数人认为他们知道它们是如何工作的。但实际上这两个属性非常复杂,可能是CSS中最难的两个属性,因为这两个属性与CSS中一个鲜为人知的特性密切相关:InlineFormattingContext(IFC)(译者注:对应BFC)。例如,line-height的值可以是一个长度(length),也可以是一个数字,它的默认值是normal。那么,什么是正常的?我们经常把normal理解为1,或者1.2,甚至CSS规范文档也没有提到这个问题。我们知道当line-height的值为数字时,代表的是font-size的倍数,但问题是font-size:100px对应的文字高度在不同的字体中是不一样的!那么行高会随着文字大小的变化而变化吗?正常真的意味着1或1.2吗?行高如何影响垂直对齐?让我们深入研究一个不那么简单的CSS机制。font-size下面是一段简单的HTML代码,一个p标签包含3个span标签,每个span有一个font-family:

BaBaBa

p{font-size:100px}.a{font-family:Helvetica}.b{font-family:Gruppo}.c{font-family:Catamaran}(译者注:这些字体在你的电脑上可能没有)font-size是一样的,但是font-family不一样,得到的span元素高度不一样:whyfont-size:100px获取不到相同高度的元素?我测量了每个跨度的高度:Helvetica115px,Gruppo97px,Catamaran164px。乍一看似乎很奇怪,但仔细想想,这样做是有道理的。原因在于字体本身,这就是字体的工作方式:字体定义了一个em方块,它是放置字符的金属容器。这个em-square的宽高一般设置为1000个相对单位,但是也可以是1024或者2048个相对单位。字体规格是根据这个相对单位来设置的,包括ascender,descender,capitalheight,x-height等。注意这里的值是允许bleedoutsiderelativetoem-square(译者注:可以理解为超过em-square)在浏览器中,上面1000个相对单位会按照你需要缩放的font-size。我们将Catamaran字体放入FontForge并分析其字体指标:em-square为1000,ascender为1100,descender为540。通过测试发现macOS上的浏览器使用的是HHeadAscent和HHeadDescent值,Windows上的浏览器使用的是WinAscent和WinDescent(而且两个平台的值不同)。我们还看到CapitalHeight是680,X高度是485。这意味着Catamaran字体占用了1100+540个相对单位,虽然它的em-square只有1000个相对单位,所以当我们设置font-size:100px时,此字体的文字高度为164px。这个计算出的高度决定了HTML元素的content-area(内容区域),后面会讲到content-area。您可以将内容区域视为背景工作的区域。我们还可以看到大写字母的高度为68px,小写字母的高度(x-height)为49px。所以1ex=49px,1em=100px,而不是164px。(很好,ems是基于字体大小,而不是计算的高度)在我们继续之前,先了解一下。当p元素出现在屏幕上时,它可能包含多行内容,每行内容由多个内联元素(内联标签或包含文本的匿名内联元素)组成,每一行称为一个line-box。line-box的高度是根据它所有孩子的高度计算的。浏览器会计算这一行中每个子元素的高度,然后得到line-box的高度(具体是子元素最高点到最低点的高度),所以默认情况下,一行-box总是有足够的高度来容纳它的子元素。每个HTML元素实际上是一个包含多个行框的容器。如果你知道每个行框的高度,那么你就知道整个元素的高度。如果我们修改原来的HTML代码:

好的设计会更好。BaBaBa我们要做出一个结果。

然后你会得到3个line-boxes(固定宽度):第一行和最后一行在中间各有一个匿名内联元素(文本内容)一行包含两个匿名内联元素和三个span。我们可以清楚地看到第二个line-box比其他两个高。因为第二行的子元素有一个使用Catamaran字体的span。line-box的难点在于我们看不到它,我们也无法用CSS来控制它。即使我们使用::first-line为第一行添加背景色,我们也看不到第一个line-box的高度。line-height到目前为止我已经提到了两个概念:content-area和line-box。如果你仔细阅读,你会发现我说的line-box的高度是根据子元素的高度来计算的,而不是子元素的content-area的高度。这个区别很大。接下来,一些听起来很奇怪的事情:内联元素有两个高度:内容区域高度和虚拟区域(实际区域?)高度(虚拟区域是我自己发明的一个词,意思是人类有效的高度,你不会'在其他任何地方都看不到这个词)。内容区域的高度由字体规格定义(见上文)。vitual-area的高度就是line-height,用来计算line-box的高度。这样一来,这打破了一个长期存在的谣言:line-height表示两条基线之间的距离。在CSS中,情况并非如此。虚拟区域和内容区域高度之间的差异称为行距。行距的一半将添加到内容区域的顶部,另一半将添加到底部。所以内容区域总是在虚拟区域的中间。计算出的line-height(即virtual-area的高度)可以等于、大于或小于content-area。如果virtual-area小于content-area,则leading为负,因此line-box看起来比内容短。还有其他类型的内联元素:可替换的内联元素,如img/input/svg等inline-block元素,以及显示值以inline-开头的所有元素,如某个Inline中的inline-table/inline-flex特殊格式化上下文的元素。例如,flexbox元素中的子元素都在flex格式化上下文中。这些子元素的显示值都是“blockified”之类的内联元素,它们的高度是基于height、margin和border属性的(译者注:padding好像少了)。如果你设置它的height为auto,那么它的height的值就是line-height,它的content-area的值也是line-height。我们还没有解释line-height:normal是什么意思。要回答这个问题,我们还得回到content-areaheight的计算,而这个问题的答案就在于字体测量。我们回到FontForge,Catamaran的em-squareheight是1000,我们也看到很多其他的ascender/descender值:常规的Ascent/Descent:ascender是770,descender是230,用于渲染字符。SpecificationsAscent/Descent:ascender为1100,descender为540。用于计算content-area的高度规格LineGap:用于计算line-height:normal。在Catamaran字体中,LineGap的值为0,则line-height:normal的结果与content-area的高度相同,为1640个相对单位。为了对比,我们再来看看Arial字体。em-square为2048,ascender为1854,descender为434,linegap为67。那么当font-size:100px时,content-area的高度为100/2048*(1854+434)=111.72,约112px;它的line-height:normal的结果是100/2048*(67+1854+434)大约是115px。所有这些值都是由字体设计师设定的。从这个角度来看,line-height:1是一个非常糟糕的做法。请记住,当line-height的值为数字时,它实际上是相对font-size的倍数,而不是相对于content-area的倍数。所以line-height:1很可能会使virtual-area比content-area短,从而导致许多其他问题。不仅仅是line-height:1有问题。在我电脑上的1117个字体中,行高大于1的大约有1059个,最低的是0.618,最高的是3.378。你没看错,3.378!line-box计算的一些细节:对于行内元素,padding和border会增加背景面积,但不会增加content-area(不是line-box的高度)。通常您无法在屏幕上看到内容区域。margin-top和margin-bottom对两者都没有影响。对于replacedinlineelements(替换行内元素)、inline-block元素和blockifiedinline元素,padding、margin和border会增加高度(译者注:注意margin),从而影响content-area和line-box的高度vertical-alignvertical-align属性我没有提到,它也是计算line-box高度的重要因素之一。我们甚至可以说垂直对齐是内联格式化上下文(IFC)最重要的属性。它的默认值为基线。还记得fontmetrics中的ascender和descender吗?这两个值决定了基线的位置。很少有字体的升序和降序是一比一的,所以我们经常会看到一些意想不到的现象,下面是一个例子。代码如下:

BaBa

p{font-family:Catamaran;字体大小:100px;line-height:200px;}一个p标签里面有两个span标签,span继承了font-family、font-size和line-heightof200px。此时两个span的baselines高度相等,line-box的高度就是span的line-height。如果第二个跨度的字体大小变小怎么办?span:last-child{font-size:50px;}我们会发现一个很奇怪的现象,line-box的高度变高了!如下所示。提醒一下,line-box的高度是从子元素的最高点到最低点。这个例子可以作为“line-height的值应该写成数字”的论据,但有时为了排版好看,我们必须把line-height写成一个固定值。但让我告诉你真相,无论你将line-height写成什么,你都会遇到对齐行内元素的问题。让我们看另一个例子。p标签的line-height:200px包含一个span,它继承了p的line-height。

Ba

p{line-height:200px;}span{font-family:Catamaran;font-size:100px;}此时line-box的高度是多少?看起来是200px,其实不是。这里你没有考虑到的问题是p有自己的font-family,默认是serif。p的基线位置和span的不一样,所以最终的line-box比我们预想的要高。出现此问题是因为浏览器认为每个行框都以宽度为0的字符开头(CSS文档将其称为strut)并将其纳入行框高度的计算中。看不见的人物,看得见的影响。为了说明这个问题,我们画一张图来说明这个问题。使用基线对齐很费解。如果我们使用vertical-align:middle不是更好吗?阅读CSS文档,你会发现middle的意思是“将当前元素的垂直中点与父元素基线的高度加上父元素x高度的一半高度对齐”。baseline的高度和字体有关,x-height的高度也和字体有关,居中对齐不靠谱。更糟糕的是,一般来说,中间根本没有居中对齐!内联元素的对齐受太多因素影响,CSS无法实现。对了,vertical-align的其他4个值可能有用:vertical-align:top/bottom,意思是和line-box的top或者bottom对齐vertical-align:text-top/text-bottom,意思是对齐content-area的顶部或底部对齐,但还是要注意,大多数情况下,对齐的是virtual-area,也就是看不见的高度。看看下面这个使用vertical-align:top的例子:最后,vertical-align的值也可以是一个数字,表示根据基线升高或降低。除非绝对必要,否则不要使用数字。CSS很棒我们讨论了line-height和vertical-align如何相互影响,现在问题来了:CSS可以控制字体规格吗?最简洁的答案是不。我也很想用CSS来控制字体。不管怎样,我还是想试试。字体指标只是一些固定值,我们应该能够围绕它们做一些事情。比如我们想让一段文字使用Catamaran字体,并且大写字母的高度恰好是100px,这似乎是可以的,我们只需要一些数学知识。首先我们将所有字体指标设置为CSS自定义属性,然后计算一个字体大小,使大写字母的高度恰好为100px。p{/*字体规格*/--font:Catamaran;--fm-资本高度:0.68;--fm-descender:0.54;--fm-ascender:1.1;--fm-linegap:0;-大写高度的大小*/--capital-height:100;/*应用字体系列*/font-family:var(--font);/*计算字体大小以获得大写高度等于所需的字体大小*/--computedFontSize:(var(--capital-height)/var(--fm-capitalHeight));字体大小:calc(var(--computedFontSize)*1px);看起来并不太复杂,是吗??如果我们希望文本垂直居中怎么办?也就是说,让B上方的空间与下方的空间高度相同。为此,我们必须根据上升沿与下降沿的比率来计算垂直对齐。首先计算line-height:normal的值和content-area的高度:p{...--lineheightNormal:(var(--fm-ascender)+var(--fm-descender)+var(--fm-linegap));--contentArea:(var(--lineheightNormal)*var(--computedFontSize));}然后我们需要计算:B下面空间的高度B以上空间的高度像这样:p{...--distanceBottom:(var(--fm-descender));--distanceTop:(var(--fm-ascender)-var(--fm-capitalHeight));}然后我们可以计算vertical-align的值。p{...--valign:((var(--distanceBottom)-var(--distanceTop))*var(--computedFontSize));}span{vertical-align:calc(var(--valign)*-1px);}最后,设置行高:p{.../*需要的行高*/--line-height:3;line-height:calc(((var(--line-height)*var(--capital-height))-var(--valign))*1px);}添加一个与B高度相同的图标很容易:span::before{内容:'';显示:内联块;宽度:calc(1px*var(--capital-height));高度:计算(1px*var(--资本高度));右边距:10px;背景:url('https://cdn.pbrd.co/images/yBAKn5bbv.png');background-size:cover;}JSBin效果演示注意这里只是为了演示,请不要在生产环境中使用该方案。结论我们知道:IFC真的很难理解。所有内联元素都有两个基于字体测量的基于内容区域虚拟区域(即行高)的高度。这两个高度是看不到line-height的:normal是根据fontmetrics计算出来的line-height:n(n=1,2,3…)可以得到一个content-areavertical-align比virtual-area短,line-box的高度不靠谱受其子元素line-height和vertical-align的影响我们不能轻易用css来控制字号,但我还是likeCSS:)添加饿了么微信进入前端技术交流群:astak10,密码:每次写代码每天一题,每周资源推荐,精彩博客推荐,工作,笔试,面试经验交流与解答,免费直播课,轻松分享群友……,福利无限