一个正常的开发流程中会由设计同学给到设计稿,再有开发同学根据标注完成应用页面的开发。不过开发一段时间就会发现在做一些长页面,有时候元素已经超出屏幕范围了,然而在设计稿上却可以刚好放满一个页面。其实除了这些还有一些控件,也会感觉出来的效果要比设计稿大打折扣,明明都是按照设计稿的尺寸做的,为什么会有人眼可以明显分辨的差距呢。
不看下面的废话,直接看结论点这里(简书跳转不了,直接翻到最下面就好)
尝试解决问题
第一次发现这个问题还是去年年初的时候,发现问题之后就是通过搜索引擎去查询有没有类似的问题,然后找到一个线索就是Android TextView有默认的顶部和底部边距,所以如果通过上下的Margin去做就会导致一定的误差。里面也给出了一个解决方案,就是这个边距的值大概为字体的0.1倍大小,虽然这个经验方案很有效。但是如果手机更换了比较特殊的字体的话,那么这个经验值也会有较大偏差。
寻求问题原因
昨天发现又有同事因为这个问题再花费大量精力调整界面,看来这个问题其实大部分都没注意到。所以有了写一篇博客简单分享的想法,查找更正规的设置方法
为了找到问题出现的原因,做出了两种假设:
- 在Java层TextView绘制文字时造成的
- native层文字绘制的实现中就有这个问题
分析Android java层绘制流程
简单分析TextView代码,可以发现实际控制文字绘制的是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) {
/*省略无关代码*/
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;
}
/*省略无关代码*/
mLineCount++;
return v;
}
上面方法中的mTopPadding
和mBottomPadding
一看就是很可疑的变量。把这两个等式有关的变量找出来如下(我们不关心真实的绘制逻辑, 只找出对这个问题有影响的变量就好了)
above = fm.ascent;
below = fm.descent;
top = fm.top;
bottom = fm.bottom;
...
mTopPadding = top - above;
mBottomPadding = bottom - below;
很明显这个值的大小跟字体的不同也会有关系,这和我之前遇到经验法不能解决的问题是一致的。关于字体参数的意义可以查看FontMetrics(fm就是FontMetrics类型)。
看来上面代码就是问题的原因了,但我们更希望能在TextView中找到解决问题的方法,查询调用了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
的值是外部参数传递过来的(trackpad是判断是否设置mTopPadding/mBottomPadding的条件,这也是我们的线索),搜索generate
方法,发现是在构造函数中调用,所以下一步查询TextView中构建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();
再结合Builder的代码,我们会发现mIncludePad
的值即trackpad
的值。查询mIncludePad
的值我们会发现两个方法与之有关:
/**
* 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;
}
根据注释也知道了,这就是所有问题的答案了,遗憾的是没有通过xml中设置属性去掉这个默认头部和底部的距离,xml中可以通过android:includeFontPadding="false"
设置该属性。
总结
造成实际输出和设计稿不同的原因是TextView的默认上下边距,可以通过调用下面的方法来移除这个默认的上下边距:
TextView#setIncludeFontPadding(false)
或者xml中设置includeFontPadding
为false
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:includeFontPadding="false" />
网友评论