2020-9-6 seo達人
垂直居中基本上是入門 CSS 必須要掌握的問題了,我們肯定在各種教程中都看到過“CSS 垂直居中的 N 種方法”,通常來說,這些方法已經(jīng)可以滿足各種使用場景了,然而當(dāng)我們碰到了需要使用某些特殊字體進行混排、或者使文字對齊圖標(biāo)的情況時,也許會發(fā)現(xiàn),無論使用哪種垂直居中的方法,總是感覺文字向上或向下偏移了幾像素,不得不專門對它們進行位移,為什么會出現(xiàn)這種情況呢?
下圖是一個使用各種常見的垂直居中的方法來居中文字的示例,其中涉及到不同字體的混排,可以看出,雖然這里面用了幾種常用的垂直居中的方法,但是在實際的觀感上這些文字都沒有恰好垂直居中,有些文字看起來比較居中,而有些文字則偏移得很厲害。
在線查看:CodePen(字體文件直接引用了谷歌字體,如果沒有效果需要注意網(wǎng)絡(luò)情況)
通過設(shè)置vertical-align:middle
對文字進行垂直居中時,父元素需要設(shè)置font-size: 0
,因為vertical-align:middle
是將子元素的中點與父元素的baseline + x-height / 2
的位置進行對齊的,設(shè)置字號為 0 可以保證讓這些線的位置都重合在中點。
我們用鼠標(biāo)選中這些文字,就能發(fā)現(xiàn)選中的區(qū)域確實是在父層容器里垂直居中的,那么為什么文字卻各有高低呢?這里就涉及到了字體本身的構(gòu)造和相關(guān)的度量值。
這里先提出一個問題,我們在 CSS 中給文字設(shè)置了 font-size
,這個值實際設(shè)置的是字體的什么屬性呢?
下面的圖給出了一個示例,文字所在的標(biāo)簽均為 span
,對每種字體的文字都設(shè)置了紅色的 outline
以便觀察,且設(shè)有 line-height: normal
。從圖中可以看出,雖然這些文字的字號都是 40px,但是他們的寬高都各不相同,所以字號并非設(shè)置了文字實際顯示的大小。
為了解答這個問題,我們需要對字體進行深入了解,以下這些內(nèi)容是西文字體的相關(guān)概念。首先一個字體會有一個 EM Square(也被稱為 UPM、em、em size)[4],這個值最初在排版中表示一個字體中大寫 M 的寬度,以這個值構(gòu)成一個正方形,那么所有字母都可以被容納進去,此時這個值實際反映的就成了字體容器的高度。在金屬活字中,這個容器就是每個字符的金屬塊,在一種字體里,它們的高度都是統(tǒng)一的,這樣每個字模都可以放入印刷工具中并進行排印。在數(shù)碼排印中,em 是一個被設(shè)置了大小的方格,計量單位是一種相對單位,會根據(jù)實際字體大小縮放,例如 1000 單位的字體設(shè)置了 16pt 的字號,那么這里 1000 單位的大小就是 16pt。Em 在 OpenType 字體中通常為 1000 ,在 TrueType 字體中通常為 1024 或 2048(2 的 n 次冪)。
金屬活字,圖片來自 http://designwithfontforge.com/en-US/The_EM_Square.html
字體本身還有很多概念和度量值(metrics),這里介紹幾個常見的概念,以維基百科的這張圖為例(下面的度量值的計量單位均為基于 em 的相對單位):
接下來我們在 FontForge 軟件里看看這些值的取值,這里以 Arial
字體給出一個例子:
從圖中可以看出,在 General 菜單中,Arial 的 em size 是 2048,字體的 ascent 是1638,descent 是410,在 OS/2 菜單的 Metrics 信息中,可以得到 capital height 是 1467,x height 為 1062,line gap 為 67。
然而這里需要注意,盡管我們在 General 菜單中得到了 ascent 和 descent 的取值,但是這個值應(yīng)該僅用于字體的設(shè)計,它們的和永遠為 em size;而計算機在實際進行渲染的時候是按照 OS/2 菜單中對應(yīng)的值來計算,一般操作系統(tǒng)會使用 hhea(Horizontal Header Table)表的 HHead Ascent 和 HHead Descent,而 Windows 是個特例,會使用 Win Ascent 和 Win Descent。通常來說,實際用于渲染的 ascent 和 descent 取值要比用于字體設(shè)計的大,這是因為多出來的區(qū)域通常會留給注音符號或用來控制行間距,如下圖所示,字母頂部的水平線即為第一張圖中 ascent 高度 1638,而注音符號均超過了這個區(qū)域。根據(jù)資料的說法[5],在一些軟件中,如果文字內(nèi)容超過用于渲染的 ascent 和 descent,就會被截斷,不過我在瀏覽器里實驗后發(fā)現(xiàn)瀏覽器并沒有做這個截斷(Edge 86.0.608.0 Canary (64 bit), MacOS 10.15.6)。
在本文中,我們將后面提到的 ascent 和 descent 均認為是 OS/2 選項中讀取到的用于渲染的 ascent 和 descent 值,同時我們將 ascent + descent 的值叫做 content-area。
理論上一個字體在 Windows 和 MacOS 上的渲染應(yīng)該保持一致,即各自系統(tǒng)上的 ascent 和 descent 應(yīng)該相同,然而有些字體在設(shè)計時不知道出于什么原因,導(dǎo)致其確實在兩個系統(tǒng)中有不同的表現(xiàn)。以下是 Roboto 的例子:
Differences between Win and HHead metrics cause the font to be rendered differently on Windows vs. iOS (or Mac I assume) · Issue #267 · googlefonts/roboto
那么回到本節(jié)一開始的問題,CSS 中的font-size
設(shè)置的值表示什么,想必我們已經(jīng)有了答案,那就是一個字體 em size 對應(yīng)的大小;而文字在設(shè)置了line-height: normal
時,行高的取值則為 content-area + line-gap,即文本實際撐起來的高度。
知道了這些,我們就不難算出一個字體的顯示效果,上面 Arial 字體在line-height: normal
和font-size: 100px
時撐起的高度為(1854 + 434 + 67) / 2048 * 100px = 115px
。
在實驗中發(fā)現(xiàn),對于一個行內(nèi)元素,鼠標(biāo)拉取的 selection 高度為當(dāng)前行line-height
最高的元素值。如果是塊狀元素,當(dāng)line-height
的值為大于 content-area 時,selection 高度為line-height
,當(dāng)其小于等于 content-area 時,其高度為 content-area 的高度。
在中間插一個問題,我們應(yīng)該都使用過 line-height
來給文字進行垂直居中,那么 line-height
實際是以字體的哪個部分的中點進行計算呢?為了驗證這個問題,我新建了一個很有“設(shè)計感”的字體,em size 設(shè)為 1000,ascent 為 800,descent 為 200,并對其分別設(shè)置了正常的和比較夸張的 metrics:
上面圖中左邊是 FontForge 里設(shè)置的 metrics,右邊是實際顯示效果,文字字號設(shè)為 100px,四個字母均在父層的 flex 布局下垂直居中,四個字母的 line-height
分別為 0、1em、normal、3em,紅色邊框是元素的 outline
,黃色背景是鼠標(biāo)選取的背景。由上面兩張圖可以看出,字體的 metrics 對文字渲染位置的影響還是很大的。同時可以看出,在設(shè)置 line-height
時,雖然 line gap 參與了撐起取值為 normal
的空間,但是不參與文字垂直居中的計算,即垂直居中的中點始終是 content-area 的中點。
我們又對字體進行了微調(diào),使其 ascent 有一定偏移,這時可以看出 1em 行高的文字 outline 恰好在正中間,因此可以得出結(jié)論:在瀏覽器進行渲染時,em square 總是相對于 content-area 垂直居中。
說完了字體構(gòu)造,又回到上一節(jié)的問題,為什么不同字體文字混排的時候進行垂直居中,文字各有高低呢?
在這個問題上,本文給出這樣一個結(jié)論,那就是因為不同字體的各項度量值均不相同,在進行垂直居中布局時,content-area 的中點與視覺的中點不統(tǒng)一,因此導(dǎo)致實際看起來存在位置偏移,下面這張圖是 Arial 字體的幾個中線位置:
從圖上可以看出來,大寫字母和小寫字母的視覺中線與整個字符的中線還是存在一定的偏移的。這里我沒有找到排版相關(guān)學(xué)科的定論,究竟以哪條線進行居中更符合人眼觀感的居中,以我個人的觀感來看,大寫字母的中線可能看起來更加舒服一點(尤其是與沒有小寫字母的內(nèi)容進行混排的時候)。
需要注意一點,這里選擇的 Arial 這個字體本身的偏移比較少,所以使用時整體感覺還是比較居中的,這并不代表其他字體也都是這樣。
對于中文字體,本身的設(shè)計上沒有基線、升部、降部等說法,每個字都在一個方形盒子中。但是在計算機上顯示時,也在一定程度上沿用了西文字體的概念,通常來說,中文字體的方形盒子中文字體底端在 baseline 和 descender 之間,頂端超出一點 ascender,而標(biāo)點符號正好在 baseline 上。
我們已經(jīng)了解了字體的相關(guān)概念,那么如何解決在使用字體時出現(xiàn)的偏移問題呢?
通過上面的內(nèi)容可以知道,文字顯示的偏移主要是視覺上的中點和渲染時的中點不一致導(dǎo)致的,那么我們只要把這個不一致修正過來,就可以實現(xiàn)視覺上的居中了。
為了實現(xiàn)這個目標(biāo),我們可以借助 vertical-align
這個屬性來完成。當(dāng) vertical-align
取值為數(shù)值的時候,該值就表示將子元素的基線與父元素基線的距離,其中正數(shù)朝上,負數(shù)朝下。
這里介紹的方案,是把某個字體下的文字通過計算設(shè)置 vertical-align
的數(shù)值偏移,使其大寫字母的視覺中點與用于計算垂直居中的點重合,這樣字體本身的屬性就不再影響居中的計算。
具體我們將通過以下的計算方法來獲取:首先我們需要已知當(dāng)前字體的 em-size,ascent,descent,capital height 這幾個值(如果不知道 em-size,也可以提供其他值與 em-size 的比值),以下依然以 Arial 為例:
const emSize = 2048; const ascent = 1854; const descent = 434; const capitalHeight = 1467;
// 計算前需要已知給定的字體大小 const fontSize = FONT_SIZE; // 根據(jù)文字大小,求得文字的偏移 const verticalAlign = ((ascent - descent - capitalHeight) / emSize) * fontSize; return ( <span style={{ fontFamily: FONT_FAMILY, fontSize }}> <span style={{ verticalAlign }}>TEXT</span> </span> )
由此設(shè)置以后,外層 span 將表現(xiàn)得像一個普通的可替換元素參與行內(nèi)的布局,在一定程度上無視字體 metrics 的差異,可以使用各種方法對其進行垂直居中。
由于這種方案具有固定的計算步驟,因此可以根據(jù)具體的開發(fā)需求,將其封裝為組件、使用 CSS 自定義屬性或使用 CSS 預(yù)處理器對文本進行處理,通過傳入字體信息,就能修正文字垂直偏移。
雖然上述的方案可以在一定程度上解決文字垂直居中的問題,但是在實際使用中還存在著不方便的地方,我們需要在使用字體之前就知道字體的各項 metrics,在自定義字體較少的情況下,開發(fā)者可以手動使用 FontForge 等工具查看,然而當(dāng)字體較多時,挨個查看還是比較麻煩的。
目前的一種思路是我們可以使用 Canvas 獲取字體的相關(guān)信息,如現(xiàn)在已經(jīng)有開源的獲取字體 metrics 的庫 FontMetrics.js。它的核心思想是使用 Canvas 渲染對應(yīng)字體的文字,然后使用 getImageData
對渲染出來的內(nèi)容進行分析。如果在實際項目中,這種方案可能導(dǎo)致潛在的性能問題;而且這種方式獲取到的是渲染后的結(jié)果,部分字體作者在構(gòu)建字體時并沒有嚴格將設(shè)計的 metrics 和字符對應(yīng),這也會導(dǎo)致獲取到的 metrics 不夠準確。
另一種思路是直接解析字體文件,拿到字體的 metrics 信息,如 opentype.js 這個項目。不過這種做法也不夠輕量,不適合在實際運行中使用,不過可以考慮在打包過程中自動執(zhí)行這個過程。
此外,目前的解決方案更多是偏向理論的方法,當(dāng)文字本身字號較小的情況下,瀏覽器可能并不能按照預(yù)期的效果渲染,文字會根據(jù)所處的 DOM 環(huán)境不同而具有 1px 的偏移[9]。
CSS Houdini 提出了一個 Font Metrics 草案[6],可以針對文字渲染調(diào)整字體相關(guān)的 metrics。從目前的設(shè)計來看,可以調(diào)整 baseline 位置、字體的 em size,以及字體的邊界大小(即 content-area)等配置,通過這些可以解決因字體的屬性導(dǎo)致的排版問題。
[Exposed=Window] interface FontMetrics {
readonly attribute double width;
readonly attribute FrozenArray<double> advances;
readonly attribute double boundingBoxLeft;
readonly attribute double boundingBoxRight;
readonly attribute double height;
readonly attribute double emHeightAscent;
readonly attribute double emHeightDescent;
readonly attribute double boundingBoxAscent;
readonly attribute double boundingBoxDescent;
readonly attribute double fontBoundingBoxAscent;
readonly attribute double fontBoundingBoxDescent;
readonly attribute Baseline dominantBaseline;
readonly attribute FrozenArray<Baseline> baselines;
readonly attribute FrozenArray<Font> fonts;
};
從 https://ishoudinireadyyet.com/ 這個網(wǎng)站上可以看到,目前 Font Metrics 依然在提議階段,還不能確定其 API 具體內(nèi)容,或者以后是否會存在這一個特性,因此只能說是一個在未來也許可行的文字排版處理方案。
文本垂直居中的問題一直是 CSS 中最常見的問題,但是卻很難引起注意,我個人覺得是因為我們常用的微軟雅黑、蘋方等字體本身在設(shè)計上比較規(guī)范,在通常情況下都顯得比較居中。但是當(dāng)一個字體不是那么“規(guī)范”時,傳統(tǒng)的各種方法似乎就有點無能為力了。
本文分析了導(dǎo)致了文字偏移的因素,并給出尋找文字垂直居中位置的方案。
由于涉及到 IFC 的問題本身就很復(fù)雜[7],關(guān)于內(nèi)聯(lián)元素使用 line-height
與 vertical-align
進行居中的各種小技巧因為與本文不是強相關(guān),所以在文章內(nèi)也沒有提及,如果對這些內(nèi)容比較感興趣,也可以通過下面的參考資料尋找一些相關(guān)介紹。
藍藍設(shè)計( www.teruid.com )是一家專注而深入的界面設(shè)計公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的UI界面設(shè)計、BS界面設(shè)計 、 cs界面設(shè)計 、 ipad界面設(shè)計 、 包裝設(shè)計 、 圖標(biāo)定制 、 用戶體驗 、交互設(shè)計、 網(wǎng)站建設(shè) 、平面設(shè)計服務(wù)
藍藍設(shè)計的小編 http://www.teruid.com