一個正常的開發(fā)流程中會由設(shè)計同學(xué)給到設(shè)計稿,再有開發(fā)同學(xué)根據(jù)標注完成應(yīng)用頁面的開發(fā)。不過開發(fā)一段時間就會發(fā)現(xiàn)在做一些長頁面,有時候元素已經(jīng)超出屏幕范圍了,然而在設(shè)計稿上卻可以剛好放滿一個頁面。其實除了這些還有一些控件,也會感覺出來的效果要比設(shè)計稿大打折扣,明明都是按照設(shè)計稿的尺寸做的,為什么會有人眼可以明顯分辨的差距呢。
不看下面的廢話,直接看結(jié)論點這里(簡書跳轉(zhuǎn)不了,直接翻到最下面就好)
嘗試解決問題
第一次發(fā)現(xiàn)這個問題還是去年年初的時候,發(fā)現(xiàn)問題之后就是通過搜索引擎去查詢有沒有類似的問題,然后找到一個線索就是Android TextView有默認的頂部和底部邊距,所以如果通過上下的Margin去做就會導(dǎo)致一定的誤差。里面也給出了一個解決方案,就是這個邊距的值大概為字體的0.1倍大小,雖然這個經(jīng)驗方案很有效。但是如果手機更換了比較特殊的字體的話,那么這個經(jīng)驗值也會有較大偏差。
尋求問題原因
昨天發(fā)現(xiàn)又有同事因為這個問題再花費大量精力調(diào)整界面,看來這個問題其實大部分都沒注意到。所以有了寫一篇博客簡單分享的想法,查找更正規(guī)的設(shè)置方法
為了找到問題出現(xiàn)的原因,做出了兩種假設(shè):
- 在Java層TextView繪制文字時造成的
- native層文字繪制的實現(xiàn)中就有這個問題
分析Android java層繪制流程
簡單分析TextView代碼,可以發(fā)現(xiàn)實際控制文字繪制的是StaticLayout。由于問題是TextView上下的間距,所以首先分析StaticLayout中對行的處理,搜索下對行有寫處理的方法:
private int out(CharSequence text, int start, int end,
int above, int below, int top, int bottom, int v,
float spacingmult, float spacingadd,
LineHeightSpan[] chooseHt, int[] chooseHtv,
Paint.FontMetricsInt fm, int flags,
boolean needMultiply, byte[] chdirs, int dir,
boolean easy, int bufEnd, boolean includePad,
boolean trackPad, char[] chs,
float[] widths, int widthStart, TextUtils.TruncateAt ellipsize,
float ellipsisWidth, float textWidth,
TextPaint paint, boolean moreChars) {
/*省略無關(guān)代碼*/
if (firstLine) {
if (trackPad) {
mTopPadding = top - above; // 看起來很可疑
}
if (includePad) {
above = top;
}
}
int extra;
if (lastLine) {
if (trackPad) {
mBottomPadding = bottom - below; // 看起來很可疑
}
if (includePad) {
below = bottom;
}
}
if (needMultiply && !lastLine) {
double ex = (below - above) * (spacingmult - 1) + spacingadd;
if (ex >= 0) {
extra = (int)(ex + EXTRA_ROUNDING);
} else {
extra = -(int)(-ex + EXTRA_ROUNDING);
}
} else {
extra = 0;
}
/*省略無關(guān)代碼*/
mLineCount++;
return v;
}
上面方法中的mTopPadding和mBottomPadding一看就是很可疑的變量。把這兩個等式有關(guān)的變量找出來如下(我們不關(guān)心真實的繪制邏輯, 只找出對這個問題有影響的變量就好了)
above = fm.ascent;
below = fm.descent;
top = fm.top;
bottom = fm.bottom;
...
mTopPadding = top - above;
mBottomPadding = bottom - below;
很明顯這個值的大小跟字體的不同也會有關(guān)系,這和我之前遇到經(jīng)驗法不能解決的問題是一致的。關(guān)于字體參數(shù)的意義可以查看FontMetrics(fm就是FontMetrics類型)。

看來上面代碼就是問題的原因了,但我們更希望能在TextView中找到解決問題的方法,查詢調(diào)用了out方法的地方:
void generate(Builder b, boolean includepad, boolean trackpad) {
...
if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
mLineCount < mMaximumVisibleLineCount) {
// Log.e("text", "output last " + bufEnd);
measured.setPara(source, bufEnd, bufEnd, textDir, b);
paint.getFontMetricsInt(fm);
v = out(source,
bufEnd, bufEnd, fm.ascent, fm.descent,
fm.top, fm.bottom,
v,
spacingmult, spacingadd, null,
null, fm, 0,
needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
includepad, trackpad, null,
null, bufStart, ellipsize,
ellipsizedWidth, 0, paint, false);
}
trackpad的值是外部參數(shù)傳遞過來的(trackpad是判斷是否設(shè)置mTopPadding/mBottomPadding的條件,這也是我們的線索),搜索generate方法,發(fā)現(xiàn)是在構(gòu)造函數(shù)中調(diào)用,所以下一步查詢TextView中構(gòu)建StaticLayout的代碼:
StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
0, mTransformed.length(), mTextPaint, wantWidth)
.setAlignment(alignment)
.setTextDirection(mTextDir)
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)
.setEllipsizedWidth(ellipsisWidth)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
}
// TODO: explore always setting maxLines
result = builder.build();
再結(jié)合Builder的代碼,我們會發(fā)現(xiàn)mIncludePad的值即trackpad的值。查詢mIncludePad的值我們會發(fā)現(xiàn)兩個方法與之有關(guān):
/**
* 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();
}
}
}
/**
* Gets whether the TextView includes extra top and bottom padding to make
* room for accents that go above the normal ascent and descent.
*
* @see #setIncludeFontPadding(boolean)
*
* @attr ref android.R.styleable#TextView_includeFontPadding
*/
public boolean getIncludeFontPadding() {
return mIncludePad;
}
根據(jù)注釋也知道了,這就是所有問題的答案了,遺憾的是沒有通過xml中設(shè)置屬性去掉這個默認頭部和底部的距離,xml中可以通過android:includeFontPadding="false"設(shè)置該屬性。
總結(jié)
造成實際輸出和設(shè)計稿不同的原因是TextView的默認上下邊距,可以通過調(diào)用下面的方法來移除這個默認的上下邊距:
TextView#setIncludeFontPadding(false)
或者xml中設(shè)置includeFontPadding為false
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:includeFontPadding="false" />