Android font, 字體全攻略

一直沒有詳細(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)注垂直方向, 如下圖

font結(jié)構(gòu)

垂直方向有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打開字體信息面板

FontForge

先看看General選項(xiàng)
Em信息

上圖可以看出, 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):

  1. 設(shè)置text size的時(shí)候是設(shè)置1Em的值
  2. Roboto把1Em分成了2048份
  3. 在Roboto中, Ascent為1900, Descent為-500
  4. 在字體中, 基線(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)證的截圖

Sketch 2048px

基線為0, 左側(cè)可以看到各條線距離基線的距離, 右側(cè)可以看到文本框總高度為2400px, 和計(jì)算值一致.

那么在Android的TextView中顯示是不是也是這樣呢?

Android TextView中的字體結(jié)構(gòu)

在Android中實(shí)測(cè)得到的各個(gè)區(qū)域的值也是一致的, 但是字體的高度卻不等于TextView的高度, 如下圖

Android font結(jié)構(gòu)

粉紅色就是TextView的背景色, 可以看到在Ascent和Descent之外分別還有一點(diǎn)距離才到TextView的邊緣, 也就是右側(cè)使用橙色方塊標(biāo)出的fontPadding.

看到這個(gè)fontPadding, 不禁有幾點(diǎn)疑問

  1. 這個(gè)fontPadding是什么東西? 有什么用?
  2. 這兩個(gè)距離是由誰加上去的? 是字體設(shè)計(jì)者還是Android自己?
  3. 還有我們最關(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è)變量

  1. top: 即上邊界, 因?yàn)樵贏ndroid中, y軸正方向是向下的, 而基準(zhǔn)線是y=0, 所以這個(gè)值是一個(gè)負(fù)數(shù).
  2. ascent: 字體文件中設(shè)置的Ascent值(即上文提到的在FontForge中查看到的HHead Ascent), 也是負(fù)數(shù), 理由同上
  3. descent: 字體文件中設(shè)置的Descent值(即上文提到的在FontForge中查看到的HHead Descent), 正數(shù)
  4. bottom: 下邊界, 正數(shù)
  5. 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

ascentdescent的值和我們從FontForge中查看ttf文件得到的值一樣, 由于坐標(biāo)系的不同, 符號(hào)相反.

但是topbottom我并沒有找到規(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

那么為什么是由topbottom決定字體的高度的呢? 那么我們就要看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ì)比如下

Heavenly Font

右側(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:lineSpacingExtraandroid:lineSpacingMultiplier修改行距. 其中lineSpacingExtra默認(rèn)值為0, lineSpacingMultiplier默認(rèn)值為1, 有以下公式

行距=默認(rèn)行距 * lineSpacingMultiplier + lineSpacingExtra

希望大家看完, 都能了解清楚字體在Android中, 占用高度的計(jì)算規(guī)則, 如有紕漏, 歡迎評(píng)論討論 :D

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容