条件:设置行间距5dp && 设置MaxLines=2 && 实际行数3大于MaxLines
<TextView
android:id="@+id/tvUseTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="5dp"
android:maxLines="2"
android:textColor="@color/grgray"
android:text="男士衬衣2018秋季寸衣潮流休闲加厚加绒保暖韩版修身条纹长袖衬衫/男士衬衣2018秋季寸衣潮流休闲加厚加绒保暖韩版修身条纹长袖衬衫"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/holo_blue_bright"
android:text="我是下方控件"/>
现象:视觉可见最后一行底部存在间距
android 4.4.4 、android 7.1.1 视觉可见最后一行(这里具体是第二行)存在行间距5dp
android 10.0 视觉可见最后一行(这里具体是第二行),存在间距,但是远小于我们设置的行间距
猜测:底部间距是行间距?
分析:
android:lineSpacingExtra在TextView源码中对应的变量是mSpacingAdd,对mSpacingAdd进行搜索,发现mSpacingAdd会被传入BoringLayout、DynamicLayout、StaticLayout中,这三个都是Layout的子类。
Layout
BoringLayout:主要用于适配单行文字展示;测量文字宽度小于等于可展示的宽度(单行)
DynamicLayout:文字内容可选或者文本是Spannable时,可使用
StaticLayout:不符合BoringLayout和DynamicLayout的,都使用StaticLayout;所以多行文字的测量和布局可以看这个
。
TextView的测量绘制都是由Layout来完成的。
测量:获取每行的信息,保存在lines数组中
(1)Layout=null
onMessure ->makeNewLayout ->makeSingleLayout ->StaticLayout ->android.text.StaticLayout#generate
->android.text.StaticLayout#out
(2)layout != null
onMessure ->getDesiredHeight() -> android.text.Layout#getHeight
绘制:通过获取的每一行的测量信息进行文字绘制
(1)Layout=null
onDraw ->assumeLayout() ->makeNewLayout ->makeSingleLayout ->StaticLayout ->android.text.StaticLayout#generate
->android.text.StaticLayout#out ->android.text.Layout#draw(android.graphics.Canvas, android.graphics.Path, android.graphics.Paint, int)
->android.text.Layout#drawText
(2)layout != null
onDraw ->android.text.Layout#draw(android.graphics.Canvas, android.graphics.Path, android.graphics.Paint, int) ->android.text.Layout#drawText
android.text.StaticLayout#out函数源码分析
以下是android4.4.4、android7.1.1、android 10代码逻辑一致部分
(1)源码中变量与文字度量距离的对应关系
if (chooseHt != null) {
fm.ascent = above;
fm.descent = below;
fm.top = top;
fm.bottom = bottom;
for (int i = 0; i < chooseHt.length; i++) {
if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
((LineHeightSpan.WithDensity) chooseHt[i])
.chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
} else {
chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
}
}
above = fm.ascent;
below = fm.descent;
top = fm.top;
bottom = fm.bottom;
}
FontMetricsInt文字度量与above_below_top_bottom对应关系.png
(2)首尾行增加pading源码,三个版本代码逻辑一样
if (firstLine) {
if (trackPad) {
mTopPadding = top - above;
}
if (includePad) {
//above是文字度量的ascent,将top距离赋值给above,就是增加了ascent到基线base距离
above = top;
}
}
int extra;
if (lastLine) {
if (trackPad) {
mBottomPadding = bottom - below;
}
if (includePad) {
below = bottom;
}
}
(3)每行信息保存,三个版本代码逻辑一样
唯一不同的点
:android10多了一个mMaxLineHeight,保存视觉可见最后一行(这里对应的是第二行)文字度量bottom的Y坐标高度,即视觉可见TextView的最大总高度
//相同部分
lines[off + START] = start;
lines[off + TOP] = v;
lines[off + DESCENT] = below + extra;
//下一行绘制的top = 当前top坐标 + 上一行的文字descent - 上一行文字的ascent + 行间距 (即 top Y坐标 + 文字高度(lastLine=true是会包含bottom和descent之间的距离) + 行间距)
v += (below - above) + extra;
lines[off + mColumns + START] = end;
lines[off + mColumns + TOP] = v;
// TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
// one bit for start field
lines[off + TAB] |= flags & TAB_MASK;
lines[off + HYPHEN] = flags;
lines[off + DIR] |= dir << DIR_SHIFT;
//android 10源码
lines[off + START] = start;
lines[off + TOP] = v;
lines[off + DESCENT] = below + extra;
lines[off + EXTRA] = extra;
// special case for non-ellipsized last visible line when maxLines is set
// store the height as if it was ellipsized
if (!mEllipsized && currentLineIsTheLastVisibleOne) {
// below calculation as if it was the last line
int maxLineBelow = includePad ? bottom : below;
// similar to the calculation of v below, without the extra.
mMaxLineHeight = v + (maxLineBelow - above);
}
v += (below - above) + extra;
lines[off + mColumns + START] = end;
lines[off + mColumns + TOP] = v;
各版本造成原因:
1、android 4.4.4
(1)android.text.StaticLayout#out
只要设置了行间距,都会在每一行底部增加行间距,若是实际文本最后一行文字,还会在最后一行文字底部增加pading(即bottom到descent之间的距离)。
按我们上面的条件,视觉可见最后一行(这里具体是第二行)底部会增加一个5dp的行间距。
if (j == 0) {
if (trackPad) {
mTopPadding = top - above;
}
if (includePad) {
above = top;
}
}
if (end == bufEnd) {
//实际文本最后一行文字
if (trackPad) {
mBottomPadding = bottom - below;
}
if (includePad) {
below = bottom;
}
}
int extra;
if (needMultiply) {
//只要有设置了行间距,不论哪一行都会设置行间距(包括文本最后一行)
double ex = (below - above) * (spacingmult - 1) + spacingadd;
if (ex >= 0) {
extra = (int)(ex + EXTRA_ROUNDING);
} else {
extra = -(int)(-ex + EXTRA_ROUNDING);
}
} else {
extra = 0;
}
(2)获取的视觉可见的总高度是最后一行再下一行的Top坐标高度,
视觉可见的最后一行非实际文本最后一行,所以这个top不包含bottom和descent之间的距离,包含我们设置的5dp的间距。
1)android.widget.TextView#onMeasure
int desired = getDesiredHeight();
2)android.widget.TextView#getDesiredHeight(android.text.Layout, boolean)
if (mMaxMode == LINES) {
/*
* Don't cap the hint to a certain number of lines.
* (Do cap it, though, if we have a maximum pixel height.)
*/
if (cap) {
if (linecount > mMaximum) {
//获取最大行的下一行Top坐标高度
desired = layout.getLineTop(mMaximum);
if (dr != null) {
desired = Math.max(desired, dr.mDrawableHeightLeft);
desired = Math.max(desired, dr.mDrawableHeightRight);
}
desired += pad;
linecount = mMaximum;
}
}
}
2、android 7.1.1
maxLine(即mMaximumVisibleLineCount)与ellipsize设置有关,未设置ellipsize时,不会往Layout的设置maxLine的值,所以mMaximumVisibleLineCount取的是默认值Integer.MAX_VALUE。
按我们上面的条件,lastLine=false,会在我们视觉可见的最后一行(这里具体指第二行)底部增加5dp的行间距,下一行的Top坐标也不含bottom到descent的距离。
(1)android.widget.TextView#makeNewLayout
boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
我们没有设置省略处理,所以mEllipsize=null,shouldEllipsize=false
(2)android.widget.TextView#makeSingleLayout
if (result == null) {
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();
}
// shouldEllipsize=false,不会调用setMaxLines,若有调用setMaxLines,mMaximum值会复制给android.text.StaticLayout#mMaximumVisibleLineCount
// 由于这里没有调用setMaxLines,所以android.text.StaticLayout#mMaximumVisibleLineCount取的是默认值
前面我们已经知道了shouldEllipsize=false,所以不会调用setMaxLines,若有调用setMaxLines,mMaximum值会复制给android.text.StaticLayout#mMaximumVisibleLineCount。
由于这里没有调用setMaxLines,所以android.text.StaticLayout#mMaximumVisibleLineCount取的是默认值。
(3)android.text.StaticLayout#out
//private int mMaximumVisibleLineCount = Integer.MAX_VALUE;
boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
boolean lastLine = currentLineIsTheLastVisibleOne || (end == bufEnd);
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;
}
//mMaximumVisibleLineCount取的是默认值,所以在我们布局中设置的maxline的最大行,也就是第二行的时候,j + 1 != mMaximumVisibleLineCount,
//所以可见的最后一行lastline会被标记为false,底部会增加间距
mMaximumVisibleLineCount取的是默认值,所以在我们布局中设置的maxline的最大行,也就是第二行的时候,j + 1 != mMaximumVisibleLineCount,所以可见的最后一行lastline会被标记为false,底部会增加间距。
(4)获取的视觉可见的总高度是最后一行再下一行的Top坐标高度
1)android.widget.TextView#onMeasure
int desired = getDesiredHeight();
2)android.widget.TextView#getDesiredHeight(android.text.Layout, boolean)
if (mMaxMode == LINES) {
/*
* Don't cap the hint to a certain number of lines.
* (Do cap it, though, if we have a maximum pixel height.)
*/
if (cap) {
if (linecount > mMaximum) {
//获取最大行的下一行Top坐标高度
desired = layout.getLineTop(mMaximum);
if (dr != null) {
desired = Math.max(desired, dr.mDrawableHeightLeft);
desired = Math.max(desired, dr.mDrawableHeightRight);
}
desired += pad;
linecount = mMaximum;
}
}
}
3)android.text.StaticLayout#getLineTop
@Override
public int getLineTop(int line) {
return mLines[mColumns * line + TOP];
}
3、android 10
(1)android.text.StaticLayout#out
按我们上面的条件,视觉可见最后一行(这里具体是第二行),lastLine=false,在视觉可见最后一行增加行间距。
mMaxLineHeight保存TextView可见的最大总高度,这个总高度包含了文字度量bottom到descent的距离,不包括视觉可见最后一行增加的行间距。
if (mEllipsized) {
lastLine = true;
} else {
final boolean lastCharIsNewLine = widthStart != bufEnd && bufEnd > 0
&& text.charAt(bufEnd - 1) == CHAR_NEW_LINE;
if (end == bufEnd && !lastCharIsNewLine) {
//bufEnd:实际文本结束位置 end:当前这一行文字结束位置
//实际文本最后一行文字 && 最后一个字符非换行符,标记为最后一行,即实际文本最后一行
lastLine = true;
} else if (start == bufEnd && lastCharIsNewLine) {
//当前行只有一个换行符,标记为最后一行,即实际文本最后一行
lastLine = true;
} else {
lastLine = false;
}
}
.....此处省略
if (needMultiply && (addLastLineLineSpacing || !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;
}
lines[off + START] = start;
lines[off + TOP] = v;
lines[off + DESCENT] = below + extra;
lines[off + EXTRA] = extra;
// special case for non-ellipsized last visible line when maxLines is set
// store the height as if it was ellipsized
if (!mEllipsized && currentLineIsTheLastVisibleOne) {
//includePad默认为true,所以mMaxLineHeight=当前top的坐标 + 当前从文字度量bottom到ascent的距离
// below calculation as if it was the last line
int maxLineBelow = includePad ? bottom : below;
// similar to the calculation of v below, without the extra.
mMaxLineHeight = v + (maxLineBelow - above);
}
v += (below - above) + extra;
lines[off + mColumns + START] = end;
lines[off + mColumns + TOP] = v;
(2)按我们上面的条件,获取的视觉可见的总高度是mMaxLineHeight
1)android.widget.TextView#onMeasure
int desired = getDesiredHeight();
2)android.widget.TextView#getDesiredHeight(android.text.Layout, boolean)
int desired = layout.getHeight(cap);
3)android.text.StaticLayout#getHeight
android 9(api 28)时启用以下方法
实际文本行数大于maxLine设置的最大行数时,视觉可见的测量高度取的是mMaxLineHeight的值,这个值不包括视觉可见最后一行底部的行间距,但是包含文字度量bottom到descent之间的pading值,所以上面我们在android10上看到的最后一行底部有一个远小于我们设置的5dp行间距的距离。
/**
* Return the total height of this layout.
*
* @param cap if true and max lines is set, returns the height of the layout at the max lines.
*
* @hide
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public int getHeight(boolean cap) {
if (cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight == -1
&& Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "maxLineHeight should not be -1. "
+ " maxLines:" + mMaximumVisibleLineCount
+ " lineCount:" + mLineCount);
}
return cap && mLineCount > mMaximumVisibleLineCount && mMaxLineHeight != -1
? mMaxLineHeight : super.getHeight();
}
4)android10的mMaximumVisibleLineCount等于maxLine设置的值
android7.1.1,maxLine的设置与省略设置Ellipsize有关系,如果没有设置Ellipsize,也不会往layout中设置maxLine,导致MaximumVisibleLineCount取的是默认值
android10,maxLine的设置与省略设置Ellipsize没有任何关系了,有设置maxLine时,也会往layout中设置maxLine,MaximumVisibleLineCount = maxLine;
android 10 android.widget.TextView#makeSingleLayout源码
//android 10 android.widget.TextView#makeSingleLayout源
if (result == null) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
0, mTransformed.length(), mTextPaint, wantWidth)
.setAlignment(alignment)
.setTextDirection(mTextDir)
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency)
.setJustificationMode(mJustificationMode)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)
.setEllipsizedWidth(ellipsisWidth);
}
result = builder.build();
}
解决方案:
对系统测量的视觉可见总高度进行修正,有两种情况需要修复
1、系统测量的视觉高度是通过layout.getLineTop(mMaximum)获取的,需要扣除的高度 = 最后一行底部坐标到文字度量descent Y坐标之间的距离
2、系统测量的视觉高度是通过mMaxLineHeight获取的,需要扣除的高度 = 文字度量bottom与descent之间的距离
关键辅助方法
android.widget.TextView#getLineBounds
返回指定行的基线baseline的Y坐标,若传入bounds(Rect),当layout已经被创建的前提下,可以返回这一行精确的上下左右精确坐标
关键代码
public class LineSpaceExtraTextView extends AppCompatTextView {
.....代码省略
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
this.setMeasuredDimension(this.getMeasuredWidth(), getFixDesiredHeight());
}
private int getFixDesiredHeight(){
int lastLineExtraSpace = this.calculateExtraSpace();
return this.getMeasuredHeight() - lastLineExtraSpace;
}
private int calculateExtraSpace() {
int lastRowSpace = 0;
try {
if (Build.VERSION.SDK_INT >= 16 && this.getLineCount() > 0) {
int actualLastRowIndex = this.getLineCount() - 1;
int lastRowIndex = Math.min(this.getMaxLines(), this.getLineCount()) - 1;
if (lastRowIndex >= 0) {
Layout layout = this.getLayout();
//getLineBounds返回指定行的基线baseline的Y坐标,若传入bounds(Rect),当layout已经被创建的前提下,可以返回这一行精确的上下左右精确坐标
int baseline = this.getLineBounds(lastRowIndex, this.mLastLineShowRect);
this.getLineBounds(actualLastRowIndex, this.mLastLineActualIndexRect);
int extra = (int) getLineSpacingExtra();
Log.e(TAG,"===========START=======");
Log.e(TAG,"xml中设置的行间距LineSpacingExtra:"+extra);
Log.e(TAG,"视觉可见文本高度 MeasuredHeight:"+this.getMeasuredHeight());
Log.e(TAG,"文本总高度 Height:"+layout.getHeight());
Log.e(TAG,"===========Rect=======");
Log.e(TAG,"实际文本最后一行的底部Y坐标 ActualLastLineRect bottom:"+mLastLineActualIndexRect.bottom);
Log.e(TAG,"视觉可见最后一行的底部Y坐标 LastLineRect bottom:"+this.mLastLineShowRect.bottom);
int fontDescentY = baseline + layout.getPaint().getFontMetricsInt().descent;
int fontBottomY = baseline + layout.getPaint().getFontMetricsInt().bottom;
Log.e(TAG,"===========视觉可见最后一行 FontMetrics=======");
Log.e(TAG,"FontMetrics baseline Y坐标:"+baseline);
Log.e(TAG,"FontMetrics descent Y坐标:"+ fontDescentY);
Log.e(TAG,"FontMetrics bottom Y坐标:"+ fontBottomY);
if (this.getMeasuredHeight() == layout.getHeight() - (this.mLastLineActualIndexRect.bottom - this.mLastLineShowRect.bottom)) {
//getMeasuredHeight() 系统测量的TextView视觉可见高度
//实际未展示部分文本高度 = 实际最后一行的底部Y坐标 - 视觉可见最后一行的底部Y坐标
//手动计算视觉可见总高度 = 文本实际总高度 - 实际未展示部分文本高度
//api28以下,没有mMaxLineHeight,手动计算的视觉可将高度与系统测量出来的视觉可见高度一致时,说明系统测量的高度是通过layout.getLineTop(mMaximum)获取,需要对测量高度进行修复
//视觉可见最后一行底部行间距 = 最后一行底部Y坐标 - 文字度量descent的Y坐标
//得到的最后一行行间距,包括了xml中设置的行间距、文字度量的bottom与descent之间的间距
lastRowSpace = this.mLastLineShowRect.bottom - (baseline + layout.getPaint().getFontMetricsInt().descent);
Log.e(TAG,"视觉可见最后一行底部行间距(包括了xml中设置的行间距、文字度量的bottom与descent之间的间距):"+lastRowSpace);
}else{
//api28及以上,通过mMaxLineHeight修复TextView视觉可见高度,mMaxLineHeight包含了文字度量的bottom与descent之间的间距
//若系统测量的TextView视觉可见高度 == 文字度量Bottom的Y坐标高度,说明存在文字度量的bottom与descent之间的间距
if(this.getMeasuredHeight() == fontBottomY){
lastRowSpace = layout.getPaint().getFontMetricsInt().bottom - layout.getPaint().getFontMetricsInt().descent;
Log.e(TAG,"去掉文字度量的bottom与descent之间的间距:"+lastRowSpace);
}
}
}
Log.e(TAG,"===========END=======");
}
return lastRowSpace;
} catch (Exception var6) {
return lastRowSpace;
}
}
}
系统源码变量说明
trackPad、includePad:在xml中对应android:includeFontPadding,系统默认是true;作用:防止某些语言文字被裁剪,用于给首尾行默认加pading
mMaximumVisibleLineCount、mMaximum:在xml中对应android:maxLines
mEllipsize:在xml中对应android:ellipsize
参考资料:Android源码调试方法
版权声明: 转载请注明出处
网友评论