一直沒有詳細(xì)地去了解android字體的相關(guān)內(nèi)容, 實(shí)際開發(fā)的時(shí)候總是對(duì)設(shè)計(jì)稿上面字體和其他控件的間距, 字體內(nèi)部的行距很疑惑, 直接設(shè)置好像每次都差幾個(gè)像素, 簡直逼死強(qiáng)迫癥患者.
今天我們就一起來看看, 字體的秘密.
字體結(jié)構(gòu)
要想對(duì)攻略字體, 我們先了解清楚字體里面都有些什么.
在分析字體的時(shí)候, 我們基本只需要關(guān)注垂直方向, 如下圖

垂直方向有5條關(guān)鍵橫線.
綠色的橫線是最關(guān)鍵的基線(base line), 字體的位置都是相對(duì)于基線的, 所以從坐標(biāo)的角度看, 基線就是y=0的坐標(biāo)軸.
頂部(ascent)和底部(descent)的紅色橫線分別為字體的上下"邊界".
注意, 雖然說是"邊界", 但是實(shí)際渲染字體的時(shí)候是可能會(huì)超過邊界的, 我個(gè)人理解這兩條線算是字體設(shè)計(jì)者在設(shè)計(jì)層面給程序提供的一個(gè)參考值. 這兩條線在設(shè)計(jì)字體的時(shí)候是可以由設(shè)計(jì)者設(shè)置的.
基線到ascent的區(qū)域稱為升部(ascender), 即圖中右側(cè)紫色的區(qū)域.
基線到descent的區(qū)域稱為降部(descender), 即圖中右側(cè)藍(lán)色的區(qū)域.
黃色的虛線是主線(mean line), 決定無升部的小寫字母的高度, 例如e, z, c等. 這個(gè)高度又叫x字高(x-height), 也就是圖中右側(cè)褐色的區(qū)域.
玫紅色的虛線(叫啥我也不知道)決定了大寫字母的高度. 這個(gè)高度又叫大寫高度(cap height), 也就是圖中右側(cè)綠色的區(qū)域.
Em, UPM
除了上述基本結(jié)構(gòu), 我們還需要搞清楚Em的概念, 有些地方也叫UPM. 簡單地說, 字體設(shè)計(jì)者和程序之間需要有一個(gè)抽象的單位來描述字體的高度, 在金屬活字印刷時(shí)代就有Em來表示一個(gè)金屬塊的高度了, 所以也就沿用了以前Em的說法, 來表示字體的基本單位.
關(guān)鍵知識(shí)點(diǎn): 在Android中, 設(shè)置text size的時(shí)候, 就是設(shè)置1Em的大小.
Em是由字體設(shè)計(jì)者在設(shè)計(jì)的時(shí)候自行決定將1Em劃分成多少份, 然后其他字體中的距離都是用相對(duì)Em的大小來描述的.
上面提到的很多值都是在字體設(shè)計(jì)的時(shí)候設(shè)置的, 顯然這些設(shè)置是保存在字體文件當(dāng)中的, 而在Android中, 最常用的字體文件格式就是.ttf(True Type Font), 所以我們有必要稍微了解一下這種文件.
TTF(True Type Font)文件
TTF簡單地說就是一個(gè)標(biāo)準(zhǔn), 用來統(tǒng)一字體的描述方式.
我們的目的不是為了設(shè)計(jì)字體, 只是希望搞清楚, 字體當(dāng)中的設(shè)置是怎樣影響字體在Android TextView中的顯示的, 尤其想搞清楚如何根據(jù)字體文件計(jì)算垂直方向上字體占用的空間.
注意, 接下來很多關(guān)于Ascent和Descent的結(jié)論都是通過代碼實(shí)測(cè)得到, 能力有限, 并沒有弄清楚其中的原理, 希望知道的朋友可以評(píng)論補(bǔ)充 :P
分析字體文件設(shè)置, 我們需要一個(gè)工具來查看這些.ttf文件, 我這里用的是FontForge, 用這個(gè)軟件打開Android的默認(rèn)字體Roboto Regular, 看看其中的字體信息.
打開文件后, 選擇Element ー> Font Info打開字體信息面板

先看看General選項(xiàng)

上圖可以看出, Roboto中, 把1Em分成了2048份,
實(shí)際上, 大部分
ttf字體都是把1Em分成了2048份. 可能也有部分字體會(huì)分成4096份.
這里還會(huì)看到Ascent和Descent的值, 不過經(jīng)過實(shí)測(cè), 這兩個(gè)并不是真正在Android中用到的Ascent和Descent.(我也很崩潰...這部分的資料很少, 并沒有深究這其中究竟有什么不同)
真正在Android中的Ascent和Descent值需要看OS/2選項(xiàng)

實(shí)測(cè)結(jié)論就是, 紅框中的這兩個(gè)值才是Android中的Ascent和Descent.
圖中頂部的Win Ascent和Win Descent是表示所有字中最高和最低的邊界, 但是這兩個(gè)值并不能對(duì)應(yīng)上Android中的值, 原因不明...
在這圖中也能看到x-height和cap height的值.
那么這個(gè)1900和-500是什么意思呢?
像素計(jì)算
要計(jì)算字體的高度, 需要記住以下幾點(diǎn):
- 設(shè)置text size的時(shí)候是設(shè)置1Em的值
- Roboto把1Em分成了2048份
- 在Roboto中, Ascent為1900, Descent為-500
- 在字體中, 基線(base line)是y=0的坐標(biāo)軸
根據(jù)1, 2兩點(diǎn), 可以知道, 1份的值是(textSize / 2048) px, 假設(shè)text size是2048px, 那么1份就是1px.
而1900表示Ascent在基線上方, 距離是1900份. -500表示Descent在基線的下方, 距離是500份.
所以理論上, 如果在字體的text size是2048px, 那么對(duì)于這份Roboto Regular字體來說
ascender = 2048px / 2048 * 1900 = 1900px
// 同理
cap height = 1456px
x-height = 1082px
descender = 500px
總高度 = ascender + descender = 1900px + 500px = 2400px
隨便打開一個(gè)軟件, 使用Roboto Regular字體在文本框中輸入一段文字, 很容易就能驗(yàn)證這個(gè)結(jié)論是正確的, 下圖是使用Sketch驗(yàn)證的截圖

基線為0, 左側(cè)可以看到各條線距離基線的距離, 右側(cè)可以看到文本框總高度為2400px, 和計(jì)算值一致.
那么在Android的TextView中顯示是不是也是這樣呢?
Android TextView中的字體結(jié)構(gòu)
在Android中實(shí)測(cè)得到的各個(gè)區(qū)域的值也是一致的, 但是字體的高度卻不等于TextView的高度, 如下圖

粉紅色就是TextView的背景色, 可以看到在Ascent和Descent之外分別還有一點(diǎn)距離才到TextView的邊緣, 也就是右側(cè)使用橙色方塊標(biāo)出的fontPadding.
看到這個(gè)fontPadding, 不禁有幾點(diǎn)疑問
- 這個(gè)fontPadding是什么東西? 有什么用?
- 這兩個(gè)距離是由誰加上去的? 是字體設(shè)計(jì)者還是Android自己?
- 還有我們最關(guān)心的問題, 這兩個(gè)距離的值怎么計(jì)算?
我們一個(gè)一個(gè)問題來看.
font padding
設(shè)計(jì)字體的時(shí)候設(shè)置的Ascent和Descent我認(rèn)為只是一個(gè)參考值, 因?yàn)槭澜缟系某俗帜负蛿?shù)字外還有其他一些字體, 例如頂部有變音符的, 藝術(shù)字體這類需要占用額外空間的字體, 所以font padding就是這個(gè)額外空間, 來確保所有字體都能顯示在區(qū)域內(nèi).
實(shí)際上, 上面提到的,
ttf文件中的Win-Ascent和Win-Descent就是這個(gè)作用, 但是和Android中實(shí)際讀取到的值并不一致.
那么這兩個(gè)值怎么算? 我目前找到的辦法是通過代碼, 利用Paint#getFontMetrics獲取這兩個(gè)值.
FontMetrics
先簡單介紹下這個(gè)類, 包含了5個(gè)變量
-
top: 即上邊界, 因?yàn)樵贏ndroid中, y軸正方向是向下的, 而基準(zhǔn)線是y=0, 所以這個(gè)值是一個(gè)負(fù)數(shù). -
ascent: 字體文件中設(shè)置的Ascent值(即上文提到的在FontForge中查看到的HHead Ascent), 也是負(fù)數(shù), 理由同上 -
descent: 字體文件中設(shè)置的Descent值(即上文提到的在FontForge中查看到的HHead Descent), 正數(shù) -
bottom: 下邊界, 正數(shù) -
leading: 兩行之間, 上一行的bottom和下一行的top的間距, 然而這個(gè)值總是0, 可以忽略.
更具體的說明可以看看這個(gè)回答 Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics
而頂部的font padding就是|top - ascent|, 底部的font padding就是bottom - ascent
我們來實(shí)測(cè)以下, 通過以下方法讀取字體的相關(guān)值
public static void printFontMetrics(Context context, @FontRes int fontRes, int emSize) {
Paint paint = new Paint();
// 設(shè)置字體, 使用兼容庫來通過font資源id獲取Typeface實(shí)例
paint.setTypeface(ResourcesCompat.getFont(context, fontRes));
// 把字體大小設(shè)置成em size方便查看
paint.setTextSize(emSize);
FontMetrics metrics = paint.getFontMetrics();
Log.d("metrics",
"top = " + metrics.top +
", ascent = " + metrics.ascent +
", descent = " + metrics.descent +
", bottom = " + metrics.bottom +
", leading = " + metrics.leading);
}
對(duì)于Rotobo Regular, 調(diào)用
// 從上面可以知道Rotobo Regular的em size是2048
printFontMetrics(context, R.font.roboto_regular, 2048);
輸出為
D/metrics: top = -2163.0, ascent = -1900.0, descent = 500.0, bottom = 555.0, leading = 0.0
ascent和descent的值和我們從FontForge中查看ttf文件得到的值一樣, 由于坐標(biāo)系的不同, 符號(hào)相反.
但是
top和bottom我并沒有找到規(guī)律, 希望知道的朋友指教一下.
不過不影響結(jié)論, 當(dāng)textSize=2048的時(shí)候, 上面的Android font結(jié)構(gòu)圖中的fontPadding, 頂部的值是2163 - 1900 = 263, 底部的值是550 - 500 = 55, 可以自行截圖驗(yàn)證, 得到以上值之后, 我們就可以通過計(jì)算得到字體的上下font padding了
// Rotobo Regular字體
topFontPadding = textSzie * (2163 - 1900) / 2048
bottomFontPadding = textSize * (550 - 500) / 2048
同時(shí)還能知道字體的實(shí)際高度
// Rotobo Regular字體
height = textSize * (2163 + 550) / 2048 = textSize * 1.3247
那么為什么是由top和bottom決定字體的高度的呢? 那么我們就要看TextView的實(shí)現(xiàn)了, 而對(duì)于普通的文本, 繪制是由android.text.BoringLayout負(fù)責(zé)的.
BoringLayout
決定文本高度的關(guān)鍵代碼在于init方法, 其實(shí)很簡單, 不看下面的代碼也沒關(guān)系
void init(CharSequence source,
TextPaint paint, int outerwidth,
Alignment align,
float spacingmult, float spacingadd,
BoringLayout.Metrics metrics, boolean includepad,
boolean trustWidth) {
int spacing;
// 忽略非重點(diǎn)代碼
// metrics雖然不是FontMetrics, 但含義一致
// spacing就是字體單行所占高度
// mDesc就是字體的下邊界
if (includepad) {
spacing = metrics.bottom - metrics.top;
mDesc = metrics.bottom;
} else {
spacing = metrics.descent - metrics.ascent;
mDesc = metrics.descent;
}
mBottom = spacing;
// 忽略非重點(diǎn)代碼
// 記錄上下font padding
if (includepad) {
mTopPadding = metrics.top - metrics.ascent;
mBottomPadding = metrics.bottom - metrics.descent;
}
}
邏輯很簡單, 關(guān)鍵在includepad, 這個(gè)值其實(shí)就是android:includeFontPadding的值, 這個(gè)值默認(rèn)是true的, 所以默認(rèn)情況下
Android中的字體高度是
|bottom| + |top|, 而普通軟件(例如word, Sketch或者其他設(shè)計(jì)軟件)中, 字體高度使用的是|descent| + |ascent|, 所以Android中的字體在垂直方向上總是比設(shè)計(jì)稿的多占一點(diǎn)空間.
分析到這里, 解決方案也很明顯了
對(duì)于普通的字體, 要完美復(fù)刻設(shè)計(jì)稿的字體高度, 應(yīng)該把
android:includeFontPadding設(shè)置為false
當(dāng)然你也可以手動(dòng)計(jì)算這個(gè)font padding, 然后做偏移.
不過這個(gè)值默認(rèn)為true是有原因的, 因?yàn)檫@個(gè)距離是為了保證
字體中所有"符號(hào)"都能顯示完全, 因此對(duì)于特殊的字體, 如果把這個(gè)值設(shè)為false, 有可能導(dǎo)致部分字母顯示不全, 例如Heavenly Font, 對(duì)比如下

右側(cè)是把
android:includeFontPadding設(shè)置為false后的情況, 部分字母顯示不完整.因此使用這個(gè)方法前先確定下字體的能夠正常顯示, 不過實(shí)際上大部分常規(guī)字體都不需要這個(gè)額外空間的, 大部分情況下還是能夠放心使用的.
注意, 對(duì)于指定的字體文件不支持的文字, 例如使用英文字體文件輸入中文, 樣式會(huì)使用系統(tǒng)默認(rèn)字體的樣式, 但是空間計(jì)算的時(shí)候還是會(huì)按照指定的字體文件的參數(shù)來計(jì)算, 而不是默認(rèn)字體的參數(shù).
行距
行距就是相鄰兩行的基線之間的距離.
默認(rèn)行距的實(shí)際值等于字體設(shè)置中的|Descent| + |Aescent|
例如對(duì)于Roboto Regular來說, textSize為2048px時(shí), 行距為500 + |-1900| = 2400px
在Android的TextView中, 可以通過android:lineSpacingExtra和android:lineSpacingMultiplier修改行距. 其中lineSpacingExtra默認(rèn)值為0, lineSpacingMultiplier默認(rèn)值為1, 有以下公式
行距=默認(rèn)行距 * lineSpacingMultiplier + lineSpacingExtra
希望大家看完, 都能了解清楚字體在Android中, 占用高度的計(jì)算規(guī)則, 如有紕漏, 歡迎評(píng)論討論 :D