前言碎碎語:標題問題在昨天困擾了筆者很久很久,早上把問題提到了各網路也暫時沒有回覆。因為明天要早起飛異地參加一場校招面試,筆者還是很緊張的,但奈何問題不解決寢食難安……所以還是卯起勁重新思考這個問題,算是暫時有了一個自己比較認可以及清晰的答案,與各位讀者分享。如您有不同觀點想法意見建議,懇請斧正!
正式探討之前,我們觀察一個現象(在 Chrome 下的表現,其他瀏覽器下的表現和計算可能有細微差別):
上圖對應的 HTML 是(之後的探討均基於此):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Line Height</title> <style> body { margin: 0; font: 32px/1 'Microsoft YaHei'; } div { width: 400px; margin: 30px auto; outline: 1px solid black; background: #008E59; } img { height: 80px; margin-top: 10px; } </style> </head> <body> <div> <span>Some Text</span> <img src="picture.jpg" alt=""/> </div> </body> </html> |
我們來計算下 DIV 和 SPAN 的高度
1 2 3 4 |
document.getElementsByTagName('div')[0].offsetHeight //93 document.getElementsByTagName('span')[0].offsetHeight //42 |
對於此圖,筆者產生如下疑問:
- line-height 為 32px,為何 SPAN 的高度為 42px?
- DIV 的高度 93px,比 IMG 高度加外邊距 90px 以及 SPAN 高度 42px 都要大,如何計算的?
- 圖片和文字下的空白(即便沒有文字一樣存在)是如何產生的?
假設我們把 IMG 刪除,HTML 部分改為:
1 2 3 4 5 |
<body> <div> <span>Some Text</span> </div> </body> |
此時來計算:
1 2 3 4 |
document.getElementsByTagName('div')[0].offsetHeight //32 document.getElementsByTagName('span')[0].offsetHeight //42 |
新問題又來了:
- DIV 的子元素高度為 42px,為何沒有“撐起” DIV 的高度?
以上問題就是本文要探討的了。覆蓋了五個知識點:
- 行內盒(或行內不可替換元素)的高度
- 行內可替換元素的高度
- 行盒的高度
- 行距與行高
- 建立行內格式化上下文的塊盒的
auto
高度
所以在探討之前,筆者已假設您知道這些概念:行內盒、行內不可替換元素、行內可替換元素、行盒、行內格式化上下文。如果您還有點不清楚,我們可以快速補習下:
可替換元素、不可替換元素
簡單地講,可替換元素是指須根據其標籤和屬性來決定具體顯示內容的元素,如本文中會探討的 IMG 元素,其具體顯示內容由 src 等屬性決定; 不可替換元素則是內容直接呈現的元素。如本文會探討的 DIV 和 SPAN 等。
塊盒
此概念是塊格式化上下文的內容,要解釋起來就更復雜啦,筆者粗陋地給您一個描述:塊盒通常是 display: block 的不可替換元素。
行內級元素、行內級盒、行內盒、行內格式化上下文
display: inline|inline-table|inline-block 產生行內級元素。行內級元素生成行內級盒,而這些盒會參與行內格式化上下文。
display 值是 inline 的不可替換元素會生成一個行內盒。
不是行內盒的行內級盒被稱為原子行內級盒。
行盒
在行內格式化上下文中,盒從包含塊的頂部一個接一個地水平擺放。包含了一行裡所有盒的矩形區域被稱為行盒。一個段落就是多個行盒的垂直堆疊。
因此,我們可以得到下圖(大致描摹):
現在開始計算!
1 行內可替換元素和文件流內行內塊可替換元素高度計算
W3C 有明確規範,如下:
如果
height 和
width 計算值均為 auto
且該元素有固有高度,那麼該固有高度為 height
使用值。
否則,如果 height 計算值為 auto ,且該元素有一個固有比例,則 height 的使用值為:
width 使用值 / 固有比例
否則,如果 height 計算值為 auto ,且該元素有固有高度,那麼該固有高度為 height 使用值。
否則,如果 height 計算值為 auto ,但以上情況均不符合,那麼 height 的使用值必須設定為一個最大矩形的高度,該矩形比例為2:1,高度不超過150px,且寬度不大於裝置寬度。
因此,在我們的例項中,IMG 盒的高度為 80+10 = 90px。
2 行內盒的高度計算
“高度”一詞在這裡頗有歧義,筆者認為,總共可以有三種概念需要辨析:
- 行內盒的內容區域高度
- 行內盒的盒高度
- 計算行盒高度時的行內盒的盒高度
您可能對第二和第三解釋抱有疑問,但我們先擱置懷疑,把清楚明白的東西先解決。
當我們用 JavaScript 去獲取一個行內盒的 offsetHeight 值時,如我們上面所做的:
1 |
document.getElementsByTagName('span')[0].offsetHeight |
筆者將此高度稱作“行內盒的盒高度”,類比於我們所熟知的塊盒盒高度。其計算值是:
內容區域高度 + 上下邊框 + 上下內邊距 = 行內盒的盒高度
邊框和內邊距的寬度預設為 0,否則為我們自己指定,但“內容區域高度”是怎麼計算的呢?
W3C 這麼說:
height
不適用。內容區域的高度應基於字型,但本規範沒有指定如何。使用者代理可能,比如說,使用行高盒 EM-Box
或字型的最大上端部 Ascender
和下端部 Descender
。(後一種會確保有部分在行高盒之上或之下的字元仍然落在內容區域內,但會導致不同字型有大小不一的盒子;前者則確保作者可以控制相對於 line-height
的背景設計,但也導致字元繪製在其內容區域之外。)
言下之意:
- height 屬性無效
- 行內盒內容區域高度在規範裡面沒有定義,瀏覽器可以自己折騰
既然規範沒有明確規定計算,我們讓瀏覽器實測一下。筆者瀏覽器測試如下:
- Chrome 42
- IE11 42
- Firefox 43
如果我們更改字型,假設應用如下 CSS
1 |
body { font-family: Simsun; } |
- Chrome 33
- IE11 37
- Firefox 35
而如果我們修改 line-height ,以上結果均不受影響。
筆者也曾疑惑,這個 offsetHeight 就是內容區域高度嗎?答案:是。筆者的驗證方法是基於 W3C 如下規定:
儘管不可替換元素的外邊距、邊框以及內邊距不納入行盒的計算,它們仍然渲染在行內盒的周圍。這意味著如果 line-height 指定的高度小於被包含盒的內容高度,內邊距和邊框的背景和顏色可能“流進”毗鄰的行盒。使用者代理應當按文件順序渲染這些盒。這會造成後面的盒的邊框繪製在前面盒的邊框和文字上。
您可以用以下程式碼實測,會發現紅色行內盒的背景溢位到了黑色行內盒所在的行盒。
1 2 3 4 5 |
<div> <span style="background:red">Some Text</span> <br/> <span style="background:rgba(0,0,0,.5)">Some Text</span> </div> |
可知內容區域高度,即行內盒沒有內邊距和邊框時的 offsetHeight 。
因此總結論是:
行內盒的內容區域高度計算沒有統一的標準,不同的字型或者不同的瀏覽器都可能導致不同的結果,且其高度與 line-height 無關。
由此我們無法確切地獲得一個跨瀏覽器的行內盒的內容區域高度。同樣我們也無法確切獲得一個跨瀏覽器的行內盒高度(因為其計算式裡面就包括了不定變數內容區域高度)。
但問題來了,不同瀏覽器都採用不同的行內盒內容區域高度,又如何能統一計算行盒以及塊容器的高度呢?這個問題便導致了筆者在上面所提到的“計算行盒高度時的行內盒的盒高度”概念。
我們進入下一個話題,行盒高度計算。
3 行盒高度計算
根據規範,行盒的高度決定如下:
- 計算行盒內每個行內級盒的高度。對於可替換元素、行內塊元素以及行內表格元素,高度是其外邊距盒的高度;對於行內盒,高度是其 line-height 。
- 行內級盒根據其 vertical-align 屬性垂直對齊。如果它們對齊 top 或 bottom ,它們必須以能最小化行盒高度的方式對齊。如果這些盒足夠高,則有多種解決方案並且 CSS2.1 沒有規定此行盒的基線的位置。
- 行盒高度是最上盒頂部到最下盒底部的距離。
懂了:W3C 儘管允許瀏覽器有自己的行內盒內容區域計算方式,但統一了一個行盒高度的計算方式:
計算行盒的高度時,針對行內盒,高度直接取 line-height 。行內盒可以有邊框、內邊距、外邊距,然而跟行盒的高度完全沒關係!
根據此規定,我們很快可以得出,計算行盒高度時,SPAN 盒的高度取 32px。
接著,由於我們的 vertical-align 是預設的 baseline ,因此,應當把盒的基線同父盒的基線對齊。如果盒沒有基線,對齊盒的下外邊距邊緣與父盒的基線。也就是說,把 SPAN 盒的基線同 DIV 盒的基線對齊,把 IMG 盒的下外邊距邊緣同 DIV 盒的基線對齊。
下圖是字型的基線、上下端線等位置資訊
筆者作圖如下:
假設我們設 DIV 盒的基線是 0,則 IMG 盒的下邊緣同 DIV 盒基線對齊;上邊緣(上外邊距邊緣頂部)在高於基線 90px 處。而 SPAN 盒由於其基線對齊 DIV 盒基線,故其行盒下邊緣略低於基線。
整個行盒的高度即 IMG 盒上邊緣到 SPAN 盒下邊緣。假設沒有 IMG 元素,則高度為 SPAN 盒的 line-height 。
但讀者您可能注意到了,29 和 -3 是怎麼得來的呢?下面,筆者帶您算!
4 行距和行高計算
29 和 -3 兩值是在計算行距和行高後得來的。我們先來看規範:
CSS 假設每種字型都由字型特性來指定一個基線之上的特性高度和之下的特性深度。本節中我們用A表示(給定字型給定字號的)高度,用D表示深度。同時定義 AD = A + D,即從頂部到底部的距離。(參見下面如何找到TrueType和OpenType字型的A和D)注意該字型的這些特性是就整個而言的,無須對應任何個別字元的上端部和下端部。
使用者代理必須在一個不可替換行內盒中依照字元的相應基線對齊各個字元。接著,就每個字元來決定A和D。注意單個元素的字元可能來自於不同字型因此不見得所有的A和D一樣。如果行內盒完全不包含字元,則被視為包含了一個具有元素首個可用字型的A和D的支柱(一個零寬度的不可見字元)。
接著對每個字元新增行距L,其中 L = line-height - AD 。行距的一般新增到A之上,另一半新增到D之下,從而賦予字元以及其行距一個基線之上的完整高度 A’ = A + L/2,以及完整深度 D’ = D+ L/2。
注意。L可能為負。
包含了所有字元以及字元兩側半行距的行內盒的高度正是 line-height 。
我們在上述規定中接觸到了這些概念:特性高度 A,特性深度 D,頂部到底部距離 AD,完整高度 A’,完整深度 D’,行距 L。
關於特性值,筆者 Google 到一個網站,推薦讀者使用:
不得不吐槽下,國內真的很難找到這樣專業精緻的字型網站(也可能是我的開啟方式不對 >_<)。
好,我們可以獲得我們例項中 Microsoft YaHei 的字型特性了:Dcsender -536;Height 2703。
- AD 即內容區域高度,在本例中是 42
- D 即字型下端(基線之下)高度,為 42*(536/2703) = 8
- L = 32 – 42 = -10
- 故,D’ = 8 + -10/2 = 3
即知行內盒的下邊緣在基線之下 3px。同時行內盒的高度被視為 32px,故亦知其上邊緣在基線之上 29px 處。
我們說啦,整個行盒的高度即 IMG 盒上邊緣到 SPAN 盒下邊緣。所以得行內盒高度為 90 + 3 = 93px。
5 建立行內格式化上下文的塊盒的 auto 高度
根據 W3C CSS2.1:10.6.3,該高度是從其上內容邊緣到其最後一個行盒的下邊緣。只考慮文件流內子盒,絕對定位和浮動子盒應被忽略,相對定位子盒不考慮位移,子盒可以是匿名盒。
在本例中,DIV 盒的行內格式化上下文僅有一個行盒,故其高度取該行盒高度,93px。