一個(gè)正常的開發(fā)流程中會由設(shè)計(jì)同學(xué)給到設(shè)計(jì)稿夏醉,再有開發(fā)同學(xué)根據(jù)標(biāo)注完成應(yīng)用頁面的開發(fā)肛鹏。不過開發(fā)一段時(shí)間就會發(fā)現(xiàn)在做一些長頁面,有時(shí)候元素已經(jīng)超出屏幕范圍了渣窜,然而在設(shè)計(jì)稿上卻可以剛好放滿一個(gè)頁面吏垮。其實(shí)除了這些還有一些控件障涯,也會感覺出來的效果要比設(shè)計(jì)稿大打折扣,明明都是按照設(shè)計(jì)稿的尺寸做的膳汪,為什么會有人眼可以明顯分辨的差距呢唯蝶。
不看下面的廢話,直接看結(jié)論點(diǎn)這里(簡書跳轉(zhuǎn)不了遗嗽,直接翻到最下面就好)
嘗試解決問題
第一次發(fā)現(xiàn)這個(gè)問題還是去年年初的時(shí)候粘我,發(fā)現(xiàn)問題之后就是通過搜索引擎去查詢有沒有類似的問題,然后找到一個(gè)線索就是Android TextView有默認(rèn)的頂部和底部邊距痹换,所以如果通過上下的Margin去做就會導(dǎo)致一定的誤差征字。里面也給出了一個(gè)解決方案,就是這個(gè)邊距的值大概為字體的0.1倍大小娇豫,雖然這個(gè)經(jīng)驗(yàn)方案很有效匙姜。但是如果手機(jī)更換了比較特殊的字體的話,那么這個(gè)經(jīng)驗(yàn)值也會有較大偏差锤躁。
尋求問題原因
昨天發(fā)現(xiàn)又有同事因?yàn)檫@個(gè)問題再花費(fèi)大量精力調(diào)整界面搁料,看來這個(gè)問題其實(shí)大部分都沒注意到或详。所以有了寫一篇博客簡單分享的想法系羞,查找更正規(guī)的設(shè)置方法
為了找到問題出現(xiàn)的原因,做出了兩種假設(shè):
- 在Java層TextView繪制文字時(shí)造成的
- native層文字繪制的實(shí)現(xiàn)中就有這個(gè)問題
分析Android java層繪制流程
簡單分析TextView代碼霸琴,可以發(fā)現(xiàn)實(shí)際控制文字繪制的是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
一看就是很可疑的變量澎迎。把這兩個(gè)等式有關(guān)的變量找出來如下(我們不關(guān)心真實(shí)的繪制邏輯, 只找出對這個(gè)問題有影響的變量就好了)
above = fm.ascent;
below = fm.descent;
top = fm.top;
bottom = fm.bottom;
...
mTopPadding = top - above;
mBottomPadding = bottom - below;
很明顯這個(gè)值的大小跟字體的不同也會有關(guān)系选调,這和我之前遇到經(jīng)驗(yàn)法不能解決的問題是一致的夹供。關(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)兩個(gè)方法與之有關(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è)置屬性去掉這個(gè)默認(rèn)頭部和底部的距離再层,xml中可以通過android:includeFontPadding="false"
設(shè)置該屬性贸铜。
總結(jié)
造成實(shí)際輸出和設(shè)計(jì)稿不同的原因是TextView的默認(rèn)上下邊距,可以通過調(diào)用下面的方法來移除這個(gè)默認(rèn)的上下邊距:
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" />