最近做需求,要实现标题多行情况下,最后一行中间省略号:
image.png
只有一行的情况下,TextView中的属性ellipsize可以实现得到,但是多行的情况下,从android4.0之后该属性失效,大概看了一下TextView源码,当多行的情况下,TextView中的那个makeSingleLayout方法会根据当前的文字长度给分行,但是如果文字中出现特殊字符时(比如有中文),生成的DynamicLayout会截取不争取,导致如下效果:

所以只能自定义TextView,自己将设置的文字给一行行画出来,如下步骤:
- 判断是否超过最大行数
- 将超过最大行数的文字等距离给删除掉并添加省略号(可以自定义)
- 然后重写onDraw方法,将文字一行行画出来
要自己画文字,需要掌握android中TextView的绘制方式:
如何用TextPaint
Paint中有一个类用于文字的字体测量FrontMetrics
/**
* Class that describes the various metrics for a font at a given text size.
* Remember, Y values increase going down, so those values will be positive,
* and values that measure distances going up will be negative. This class
* is returned by getFontMetrics().
*/
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;
}

-
Baseline每一行文字的基线,在Android中,文字的绘制都是从Baseline处开始的,Baseline往上至字符“最高处”的距离我们称之为
ascent(上坡度)
,Baseline往下至字符“最低处”的距离我们称之为descent(下坡度)
; -
leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离;

- top的意思其实就是除了Baseline到字符顶端的距离外还应该包含这些符号的高度,bottom的意思也是一样。一般情况下我们极少使用到类似的符号所以往往会忽略掉这些符号的存在,但是Android依然会在绘制文本的时候在文本外层留出一定的边距,这就是为什么top和bottom总会比ascent和descent大一点的原因。而在TextView中我们可以通过xml设置其属性android:includeFontPadding="false"去掉一定的边距值但是不能完全去掉。
在TextPaint绘制文字时,是从BaseLine往上画的,BaseLine下面为正,上面为负,所以通过paint.getAscent拿到的都是负数,paint.getDescent为正数。
所以通过canvas.drawText(text, x, y, paint),其中(x,y)就是baseline的起始坐标。
了解了TextPiant的文字测量后,接下来就是:
- 判断是否超过最大行数
- 将超过最大行数的文字等距离给删除掉并添加省略号(可以自定义)
- 然后重写onDraw方法,将文字一行行画出来
- 判断是否超过最大行数
每次当设置setText后,都会去调用requestLayout更新布局。
所以在onMeasure方法判断当前是否行数超过最大行,如果超过,截取文字并且重新设置。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setText(mOriginText);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
try {
mIsExactlyMode = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;
final Layout layout = getLayout();
if (layout != null) {
//超过设定的maxLine的时候,删除中间多余的字符,换成‘...’
if (isExceedMaxLine(layout) || isOutOfBounds(layout)) {
adjustEllipsizeMiddleText(layout);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void setText(CharSequence text, BufferType type) {
if (mEnableUpdateOriginText) {
mOriginText = text;
}
super.setText(text, type);
if (mIsExactlyMode) {
requestLayout();
}
}
如果超过最大行数时,从最后一行截取并且给中间添加省略号:
private void adjustEllipsizeMiddleText(Layout layout) {
CharSequence originText = mOriginText;
int width = getContentWidth();
int maxLineCount = Math.max(1, computeMaxLineCount(layout));
int maxLineFirstCharIndex = computeWidthIndex(originText, 0, (maxLineCount - 1) * width);
mEnableUpdateOriginText = false;
int ellipizeTextWidth = (int)Layout.getDesiredWidth(mEllipsizeText, getPaint());
int origninWidth = (int)Layout.getDesiredWidth(mOriginText, getPaint()) + ellipizeTextWidth;
int maxWidth = width * maxLineCount;
if (origninWidth > maxWidth) {
int widthDiff = origninWidth - maxWidth;
//找出最后一行中将字符的下标
int maxLineMidCharIndex = computeWidthIndex(originText, maxLineFirstCharIndex, width >>> 1);
//将超过的widthDiff的长度对应的字符给删掉
int removedMiddleCount = computeRemovedCharCount(originText, maxLineMidCharIndex, widthDiff) + 1;
CharSequence front = originText.subSequence(0, maxLineMidCharIndex);
CharSequence behind = originText.subSequence(maxLineMidCharIndex + removedMiddleCount - 1, mOriginText.length());
String result = new StringBuffer(front).append(mEllipsizeText).append(behind).toString();
//重新设置文字
setText(result);
}
mEnableUpdateOriginText = true;
}
上面就是将多出的文字截取并且添加好省略号,接下来就是一行行第画出来:
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
final Layout layout = getLayout();
if (layout != null) {
int x = getPaddingLeft();
//初始化y的值是第一行中的baseline位置.
int y = getPaddingTop() + (int) Math.abs(getPaint().ascent());
CharSequence text = getText();
int start = 0;
int end = 0;
float charsWidth = 0;
int size = text.length();
int maxWidth = getContentWidth();
for (int i = 0; i < layout.getLineCount(); i++) {
while (charsWidth < maxWidth && end < size) {
charsWidth = getPaint().measureText(text.subSequence(start, (++end)).toString());
}
if (charsWidth > maxWidth) {
end--;
}
canvas.drawText(text, start, end, x, y, getPaint());
start = end;
charsWidth = 0;
//将y的位置移动到下面有行的baseline位置
y = y + (int) Math.abs(getPaint().descent() - getPaint().ascent());
}
}
}
上面就用到了文字测量的acent和descent。
完。
上面代码源码Demo版本EllipsizeMiddleTextView
网友评论