1Pixel的字到底有多高?

在還原UI的時候咱們常會發現一個問題,按照Sketch標註的尺寸去還原設計稿中的文字會產生幾個Px的偏差,字符上下有些許空白,以至於後期設計審查時頻繁微調。css

font
如上圖爲 Android設備上 100Px的不一樣字體顯示的真實高度( includeFontPadding:false,下同),不一樣的字體的實際高度均不一致。

因此,爲了精確還原咱們須要瞭解1Px的字體到底有多高?html

FontMetrics

TrueType字體文件中,每一款字體文件都會定義一個em-square,它被存放於ttf文件中的'head'表中,一個em-square值能夠爲10001024或者2048等。 前端

em

em-square至關於字體的一個基本容器,也是textSize縮放的相對單位。金屬時代一個字符不能超過其所在的容器,可是在數字時代卻沒有這個限制,一個字符能夠擴展到em-square以外,這也是設計一些字體時候挺方便的作法。java

後續的ascentdescent以及lineGap等值都是相對於em-square的相對值。android

asecent

ascent表明單個字符最高處至baseLine的推薦距離,descent表明單個字符最低處至baseLine的推薦距離。字符的高度通常由ascentdescent共同決定,對於em-squareascentdescent咱們能夠經過FontTools解析字體文件得到。git

FontTools

FootTools是一個完善易用的Python字體解析庫,能夠很方便地將TTXTTF等文件轉成文本編輯器打開的XML描述文件github

FontTools
複製代碼

安裝bash

pip install fonttools
複製代碼

轉碼app

ttx Songti.ttf 
複製代碼

轉碼後會在當前目錄生成一個Songti.ttx的文件,咱們用文本編輯器打開並搜索'head'編輯器

<head>
    <!-- Most of this table will be recalculated by the compiler -->
    <tableVersion value="1.0"/>
    <fontRevision value="1.0"/>
    <checkSumAdjustment value="0x7550297b"/>
    <magicNumber value="0x5f0f3cf5"/>
    <flags value="00000000 00001011"/>
    <unitsPerEm value="1000"/>
    <created value="Thu Nov 11 14:47:27 1999"/>
    <modified value="Tue Nov 14 03:02:03 2017"/>
    <xMin value="-99"/>
    <yMin value="-150"/>
    <xMax value="1032"/>
    <yMax value="860"/>
    <macStyle value="00000000 00000000"/>
    <lowestRecPPEM value="12"/>
    <fontDirectionHint value="1"/>
    <indexToLocFormat value="1"/>
    <glyphDataFormat value="0"/>
  </head>
複製代碼

其中unitsPerEm便表明em-square,值爲1000。 在Windows系統中,AscentDescent'OS_2'表中的usWinAscentusWinDescent決定。 可是在MacOS、iOS以及Android中,AscentDescent'hhea'表中的ascentdescent決定。

<hhea>
    <tableVersion value="0x00010000"/>
    <ascent value="1060"/>
    <descent value="-340"/>
    <lineGap value="0"/>
    <advanceWidthMax value="1000"/>
    <minLeftSideBearing value="-99"/>
    <minRightSideBearing value="-50"/>
    <xMaxExtent value="1032"/>
    <caretSlopeRise value="1"/>
    <caretSlopeRun value="0"/>
    <caretOffset value="0"/>
    <reserved0 value="0"/>
    <reserved1 value="0"/>
    <reserved2 value="0"/>
    <reserved3 value="0"/>
    <metricDataFormat value="0"/>
    <numberOfHMetrics value="1236"/>
  </hhea>
複製代碼

AscentDescent的值爲以baseLine做爲原點的座標,根據這三個值,咱們能夠計算出字體的高度。

TextHeight = (Ascent - Descent) / EM-Square * TextSize
LineHeight = (Ascent - Descent + LineGap) / EM-Square * TextSize
複製代碼

上表中,咱們已知宋體-常規的ascent1060descent-340

TextSize爲100Pixcel的宋體常規字符高度爲
height = (1060 - (-340)) / 1000 * 100 = 140px
複製代碼

因此對於宋體,1Px的字高爲1.4Px

常見字體LineGap通常均爲0,因此通常lineHeight = textHeight

經常使用字體參數

iOS默認字體 - [San Francisco]

<unitsPerEm value="2048"/>
  <ascent value="1950"/>
  <descent value="-494"/>
  <lineGap value="0"/>
複製代碼
TextHeight = 1.193359375 TextSize
複製代碼

Android默認字體 - [Roboto - Regular]

<unitsPerEm value="2048"/>
  <ascent value="1900"/>
  <descent value="-500"/>
  <lineGap value="0"/>
  <yMax value="2163"/>
  <yMin value="-555"/>
複製代碼
TextHeight = 1.17187502 TextSize
複製代碼

UI適配誤區

如上圖 Sketch設計稿中,字體爲 28px,字體居上下邊框爲 32px,若是按照這樣的參數進行UI還原的話,以 Android默認設備爲例,外圍背景會比原來高 28 * (1.17 - 1) = 4.76個像素( Android IncludeFontPadding = false)。

這是由於該設計稿中框選的lineHeight = textSize,這在通常的字體中是不正確的!會致使一些文字顯示不下或者兩行文字的上下端部分疊加。同理,用字的高度去得出TextSize也是不正確的!框選文字的時候不能剛剛夠框選中文,實際上這種作法輸入框輸入個'j'便會超出選框,雖然仍能顯示。

正確作法應該將lineHeight設置爲 28 * 1.17 = 33,而後再測出上下邊距。

如圖,文字的實際位置並無變化,可是文字的 lineHeight變大了,上下邊距相應減小爲 29px30px

對於設計稿中LineHeight > 字體實際高度(如1.17 * textSize)的狀況下,咱們能夠設置lineSpace = lineHeight - 1.17 textSize 去精確還原行間距。

結論:UI中字體還原不到位通常是對字體高度理解有誤解,實際上1Px的字體在客戶端中通常不等於1Px,而等於1.19(iOS) or 1.17 (Android) 個Px。

Android IncludeFontPadding

/**
     * Set whether the TextView includes extra top and bottom padding to make
     * room for accents that go above the normal ascent and descent.
     * The default is true.
     *
     * @see #getIncludeFontPadding()
     *
     * @attr ref android.R.styleable#TextView_includeFontPadding
     */
    public void setIncludeFontPadding(boolean includepad) {
        if (mIncludePad != includepad) {
            mIncludePad = includepad;

            if (mLayout != null) {
                nullLayouts();
                requestLayout();
                invalidate();
            }
        }
    }
複製代碼

Android TextView 默認IncludeFontPadding爲開啓狀態,會在每一行字的上下方留出更多的空間。

if (getIncludeFontPadding()) {
            fontMetricsTop = fontMetrics.top;
        } else {
            fontMetricsTop = fontMetrics.ascent;
        }
        
        
 if (getIncludeFontPadding()) {
            fontMetricsBottom = fontMetrics.bottom;
        } else {
            fontMetricsBottom = fontMetrics.descent;
        }
複製代碼

咱們經過Textview的源碼能夠發現,只有IncludeFontPadding = false的狀況下,textHeight計算方式才與iOS端與前端相統一。默認true狀況會選取topbottom,這兩個值在通常狀況下會大於ascentdescent,但也不是絕對的,在一些字體中會小於ascentdescent

public static class FontMetrics {
        /**
         * The maximum distance above the baseline for the tallest glyph in
         * the font at a given text size.
         */
        public float   top;
        /**
         * The recommended distance above the baseline for singled spaced text.
         */
        public float   ascent;
        /**
         * The recommended distance below the baseline for singled spaced text.
         */
        public float   descent;
        /**
         * The maximum distance below the baseline for the lowest glyph in
         * the font at a given text size.
         */
        public float   bottom;
        /**
         * The recommended additional space to add between lines of text.
         */
        public float   leading;
    }
複製代碼

對於topbottom,這兩個值在 ttc/ttf 字體中並無同名的屬性,應該是Android獨有的名稱。咱們能夠尋找獲取FontMetrics的方法(getFontMetrics)進行溯源。

public float getFontMetrics(FontMetrics metrics) {
        return nGetFontMetrics(mNativePaint, metrics);
    }
    
    @FastNative
    private static native float nGetFontMetrics(long paintPtr, FontMetrics metrics);
複製代碼

PaintgetFontMetrics最終調用了native方法nGetFontMetricsnGetFontMetrics的實如今Android源碼中的Paint_Delegate.java

@LayoutlibDelegate
 /*package*/
static float nGetFontMetrics ( long nativePaint, long nativeTypeface,FontMetrics metrics){
            // get the delegate
            Paint_Delegate delegate = sManager.getDelegate(nativePaint);
            if (delegate == null) {
                return 0;
            }
            return delegate.getFontMetrics(metrics);
}
        
        
private float getFontMetrics (FontMetrics metrics){
            if (mFonts.size() > 0) {
                java.awt.FontMetrics javaMetrics = mFonts.get(0).mMetrics;
                if (metrics != null) {
                    // Android expects negative ascent so we invert the value from Java.
                    metrics.top = -javaMetrics.getMaxAscent();
                    metrics.ascent = -javaMetrics.getAscent();
                    metrics.descent = javaMetrics.getDescent();
                    metrics.bottom = javaMetrics.getMaxDescent();
                    metrics.leading = javaMetrics.getLeading();
                }

                return javaMetrics.getHeight();
            }

            return 0;
        }

複製代碼

由上可知topbottom實際上取得是Java FontMetrics中的MaxAscentMaxDescent,對於MaxAscent的取值OpenJDK官網論壇給出了答案:

Ideally JDK 1.2 should have used the OS/2 table value for usWinAscent,
or perhaps sTypoAscender (so there's at least three choices here, see http://www.microsoft.com/typography/otspec/recom.htm#tad for more info). For max ascent we could use the yMax field in the font header. In most fonts I think this is equivalent to the value we retrieve from the hhea table, hence the observation that both methods return the max ascent. 複製代碼

因此咱們能夠獲知,Android默認取的是字體的yMax高度,經過查找Apple Font手冊咱們能夠知道yMax是字符的邊界框範圍,因此咱們能夠得出如下公式:

includeFontPadding default true
TextHeight = (yMax - yMin) / EM-Square * TextSize

includeFontPadding false
TextHeight = (ascent - descent) / EM-Square * TextSize
複製代碼

Android默認字體roboto在默認includeFontPadding = true狀況下,textHeight = 1.32714844 textSize

因此Android UI適配,若是不改變includeFontPadding,能夠將係數調整爲1.327

總結

相同textSize的字體,高度由字體自己決定

字體公式

TextHeight = (Ascent - Descent) / EM-Square * TextSize
LineHeight = (Ascent - Descent + LineGap) / EM-Square * TextSize

Android - includeFontPadding true
TextHeight = (yMax - yMin) / EM-Square * TextSize
複製代碼

客戶端默認字體下,1個Px的高度值並不爲1Px

iOS TextHeight = 1.193359375 TextSize 
Android - IncludePadding : true  TextHeight = 1.32714844 TextSize
Android - IncludePadding : false TextHeight = 1.17187502 TextSize
複製代碼

參考資料

Apple - TrueTypeReference Manual

Microsoft - TrueType

Github - FontTools

Open JDK

AndroidXRef

Deep dive CSS: font metrics, line-height and vertical-align

相關文章
相關標籤/搜索