超詳細!安卓巴士開發者大會嘉賓及主題介紹
一直沒有詳細地去了解android字體的相關內容, 實際開發的時候總是對設計稿上面字體和其他控件的間距, 字體內部的行距很疑惑, 直接設置好像每次都差幾個像素, 簡直逼死強迫症患者.
今天我們就一起來看看, 字體的秘密.
字體結構
要想對攻略字體, 我們先了解清楚字體裡面都有些什麼.
在分析字體的時候, 我們基本只需要關注垂直方向, 如下圖
垂直方向有5條關鍵橫線.
綠色的橫線是最關鍵的基線(base line), 字體的位置都是相對於基線的, 所以從坐標的角度看, 基線就是y=0的坐標軸.
頂部(ascent)和底部(descent )的紅色橫線分別為字體的上下'邊界'.
注意, 雖然說是'邊界', 但是實際渲染字體的時候是可能會超過邊界的, 我個人理解這兩條線算是字體設計者在設計層面< /strong>給程序提供的一個參考值. 這兩條線在設計字體的時候是可以由設計者設置的.
基線到ascent的區域稱為升部(ascender), 即圖中右側紫色的區域.
基線到descent的區域稱為降部(descender), 即圖中右側藍色的區域.
黃色的虛線是主線(mean line), 決定無升部的小寫字母的高度, 例如e, z, c等. 這個高度又叫x字高(x-height ), 也就是圖中右側褐色的區域.
玫紅色的虛線大寫高度(cap height), 也就是圖中右側綠色的區域.
Em, UPM
除了上述基本結構, 我們還需要搞清楚Em的概念, 有些地方也叫UPM. 簡單地說, 字體設計者和程序之間需要有一個抽象的單位來描述字體的高度, 在金屬活字印刷時代就有Em來表示一個金屬塊的高度了, 所以也就沿用了以前Em的說法, 來表示字體的基本單位.
關鍵知識點: 在Android中, 設置text size的時候, 就是設置1Em的大小.
Em是由字體設計者在設計的時候自行決定將1Em劃分成多少份, 然後其他字體中的距離都是用相對Em的大小來描述的 strong>.
上面提到的很多值都是在字體設計的時候設置的, 顯然這些設置是保存在字體文件當中的, 而在Android中, 最常用的字體文件格式就是.ttf
(True Type Font), 所以我們有必要稍微了解一下這種文件.
TTF(True Type Font)文件
TTF簡單地說就是一個標準, 用來統一字體的描述方式.
我們的目的不是為了設計字體, 只是希望搞清楚, 字體當中的設置是怎樣影響字體在Android TextView中的顯示的, 尤其想搞清楚如何根據字體文件計算垂直方向上字體佔用的空間.
注意, 接下來很多關於Ascent和Descent的結論都是通過代碼實測得到, 能力有限, 並沒有弄清楚其中的原理, 希望知道的朋友可以評論補充 :P
分析字體文件設置, 我們需要一個工具來查看這些.ttf
文件, 我這裡用的是FontForge, 用這個軟件打開Android的默認字體Roboto Regular, 看看其中的字體信息.
打開文件後, 選擇Element ー> Font Info打開字體信息面板
先看看General選項
上圖可以看出, Roboto中, 把1Em分成了2048份,
實際上, 大部分ttf
字體都是把1Em分成了2048份. 可能也有部分字體會分成4096份.
這裡還會看到Ascent和Descent的值, 不過經過實測, 這兩個並不是真正在Android中用到的Ascent和Descent.(我也很崩潰…這部分的資料很少, 並沒有深究這其中究竟有什麼不同)
真正在Android中的Ascent和Descent值需要看OS/2選項
實測結論就是, 紅框中的這兩個值才是Android中的Ascent和Descent.
圖中頂部的Win Ascent和Win Descent是表示所有字中最高和最低的邊界, 但是這兩個值並不能對應上Android中的值, 原因不明…
在這圖中也能看到x-height和cap height的值.
那麼這個1900和-500是什麼意思呢?
像素計算
要計算字體的高度, 需要記住以下幾點:
設置text size的時候是設置1Em的值
Roboto把1Em分成了2048份
在Roboto中, Ascent為1900, Descent為-500
在字體中, 基線(base line)是y=0的坐標軸
根據1, 2兩點, 可以知道, 1份的值是(textSize / 2048) px
, 假設text size是2048px, 那麼1份就是1px.
而1900表示Ascent在基線上方, 距離是1900份. -500表示Descent在基線的下方, 距離是500份.
所以理論上, 如果在字體的text size是2048px, 那麼對於這份Roboto Regular字體來說
ascender = 2048px / 2048 * 1900 = 1900px
// 同理
cap height = 1456px
x -height = 1082px
descender = 500px
總高度 = ascender + descender = 1900px + 500px = 2400px
隨便打開一個軟件, 使用Roboto Regular字體在文本框中輸入一段文字, 很容易就能驗證這個結論是正確的, 下圖是使用Sketch驗證的截圖
基線為0, 左側可以看到各條線距離基線的距離, 右側可以看到文本框總高度為2400px, 和計算值一致.
那麼在Android的TextView
中顯示是不是也是這樣呢?
Android TextView中的字體結構
在Android中實測得到的各個區域的值也是一致的, 但是字體的高度卻不等於TextView code>的高度, 如下圖
粉紅色就是TextView
的背景色, 可以看到在Ascent和Descent之外分別還有一點距離才到TextView
的邊緣, 也就是右側使用橙色方塊標出的fontPadding.
看到這個fontPadding, 不禁有幾點疑問
這個fontPadding是什麼東西? 有什麼用?
這兩個距離是由誰加上去的? 是字體設計者還是Android自己?
還有我們最關心的問題, 這兩個距離的值怎麼計算?
我們一個一個問題來看.
font padding
設計字體的時候設置的Ascent和Descent我認為只是一個參考值, 因為世界上的除了字母和數字外還有其他一些字體,例如頂部有變音符的, 藝術字體這類需要佔用額外空間的字體, 所以font padding就是這個額外空間, 來確保所有字體都能顯示在區域內.
實際上, 上面提到的, ttf
文件中的Win-Ascent和Win-Descent就是這個作用, 但是和Android中實際讀取到的值並不一致.
那麼這兩個值怎麼算? 我目前找到的辦法是通過代碼, 利用Paint#getFontMetrics獲取這兩個值.
FontMetrics
先簡單介紹下這個類, 包含了5個變量
top
: 即上邊界, 因為在Android中, y軸正方向是向下的, 而基準線是y=0, 所以這個值是一個負數.ascent
: 字體文件中設置的Ascent值(即上文提到的在FontForge中查看到的HHead Ascent), 也是負數, 理由同上descent
: 字體文件中設置的Descent值(即上文提到的在FontForge中查看到的HHead Descent), 正數bottom
: 下邊界, 正數leading
: 兩行之間, 上一行的bottom和下一行的top的間距, 然而這個值總是0, 可以忽略.
更具體的說明可以看看這個回答Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics
而頂部的font padding就是|top - ascent|
, 底部的font padding就是bottom - ascent
我們來實測以下, 通過以下方法讀取字體的相關值
public static void printFontMetrics(Context context, @ FontRes int fontRes, int emSize) {
Paint paint = new Paint();
// 設置字體, 使用兼容庫來通過font資源id獲取Typeface實例 span>
paint.setTypeface(ResourcesCompat.getFont(context, fontRes));
// 把字體大小設置成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);
}
對於Rotobo Regular , 調用
// 從上面可以知道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
文件得到的值一樣, 由於坐標系的不同, 符號相反.
但是top
和bottom
我並沒有找到規律, 希望知道的朋友指教一下.
不過不影響結論, 當textSize=2048的時候, 上面的Android font結構圖中的fontPadding, 頂部的值是2163 - 1900 = 263
, 底部的值是550 - 500 = 55
, 可以自行截圖驗證, 得到以上值之後, 我們就可以通過計算得到字體的上下font padding了
// Rotobo Regular字體
topFontPadding = textSzie * (2163 - 1900) / 2048
bottomFontPadding = textSize * (550 - 500) / 2048
< /code>
同時還能知道字體的實際高度
// Rotobo Regular字體
height = textSize * (2163 + 550) / 2048 = textSize * 1.3247
那麼為什麼是由top
和bottom
決定字體的高度的呢?那麼我們就要看TextView
的實現了, 而對於普通的文本, 繪製是由android.text.BoringLayout
負責的.
BoringLayout
決定文本高度的關鍵代碼在於init
方法, 其實很簡單, 不看下面的代碼也沒關係
void init(CharSequence source,
TextPaint paint, int outerwidth,
Alignment align,
float spacingmult, float spacingadd,
BoringLayout.Metrics metrics, boolean includepad,
boolean trustWidth) {
int spacing;
// 忽略非重點代碼
// 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;
// 忽略非重點代碼
/ / 記錄上下font padding
if (includepad) {
mTopPadding = metrics.top - metrics.ascent;
mBottomPadding = metrics .bottom - metrics.descent;
}
}
邏輯很簡單, 關鍵在includepad
, 這個值其實就是android:includeFontPadding
的值, 這個值默認是true
的, 所以默認情況下
Android中的字體高度是|bottom| + |top|
, 而普通軟件(例如word, Sketch或者其他設計軟件)中, 字體高度使用的是|descent| + |ascent|
, 所以Android中的字體在垂直方向上總是比設計稿的多佔一點空間.
分析到這裡, 解決方案也很明顯了
對於普通的字體, 要完美復刻設計稿的字體高度, 應該把android:includeFontPadding
設置為false
當然你也可以手動計算這個font padding, 然後做偏移.
不過這個值默認為true
是有原因的, 因為這個距離是為了保證
字體中所有'符號'都能顯示完全, 因此對於特殊的字體, 如果把這個值設為false
, 有可能導致部分字母顯示不全, 例如Heavenly Font, 對比如下
右側是把android:includeFontPadding
設置為false
後的情況, 部分字母顯示不完整.
因此使用這個方法前先確定下字體的能夠正常顯示, 不過實際上大部分常規字體都不需要這個額外空間的, 大部分情況下還是能夠放心使用的.
注意, 對於指定的字體文件不支持的文字, 例如使用英文字體文件輸入中文, 樣式會使用系統默認字體的樣式, 但是空間計算的時候還是會按照指定的字體文件的參數來計算, 而不是默認字體的參數.
行距
行距就是相鄰兩行的基線之間的距離.
默認行距的實際值等於字體設置中的|Descent| + |Aescent|
例如對於Roboto Regular來說, textSize為2048px時, 行距為500 + |-1900| = 2400px
在Android的TextView中, 可以通過android:lineSpacingExtra
和android:lineSpacingMultiplier
修改行距. 其中lineSpacingExtra
默認值為0, < code >lineSpacingMultiplier默認值為1, 有以下公式
行距=默認行距 * lineSpacingMultiplier + lineSpacingExtra
希望大家看完, 都能了解清楚字體在Android中, 佔用高度的計算規則, 如有紕漏, 歡迎評論討論。
大家都在看
Flutter完整開發實戰詳解(三、 打包與填坑篇)
Android中高級面試題準備整理分享
Framework學習Android系統源碼下載與編譯
Android Dex分包之旅
歡迎前往安卓巴士博客區投稿,技術成長於分享
期待巴友留言< em >,共同探討學習
文章為用戶上傳,僅供非商業瀏覽。發布者:Lomu,轉轉請註明出處: https://www.daogebangong.com/zh-Hant/articles/detail/Android%20font%20a%20complete%20guide%20to%20Android%20fonts.html
评论列表(196条)
测试