TextView实现类型微信的全文功能

在最近的项目中,有个列表需要展示类似微信朋友圈的全文功能,点击全文展示所有的内容,点击隐藏全文,展示原本的文本。

设置TextView最多只能展示5行,超过5行的时候展示省略号(TextView设置最大行数的同时支持Span属性的一些坑,已经在LinkMovementMethod那些坑中描述)。我们只要知道当前展示的文字长度超过了5行,就可以知道是否要展示全文按钮,可以使用addOnGlobalLayoutListener()在TextView绘制完成后进行展示文字长度的判断,我选择自定义TextView,在onMeasure()中进行判断。

一开始在网上搜罗了一些方法后,感觉还挺简单的,码上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SpannableTextView extends TextView {
private TextViewShowCallback callback;

public SpannableTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Layout mainLayout = getLayout();
int lineCount = mainLayout.getLineCount(); // 获取当前TextView展示的行数
// mainLayout.getLineEnd(lineCount - 1)获取当前TextView展示的最后一个字符的位置
callback.configMoreText(lineCount, mainLayout.getLineEnd(lineCount - 1));
}

public void setCallback(TextViewShowCallback callback) {
this.callback = callback;
}

// 一个callback来告诉textview的父view展示了多少,当前行末位置
public interface TextViewShowCallback {
void configMoreText(int lineCount, long lineEnd);
}
}

在使用了这个TextView的RecyclerView的adapter里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
linkText.setCallback(new SpannableTextView.TextViewShowCallback() {
@Override
public void configMoreText(int lineCount, long lineEnd) {
// 不展开的时候最多只有5行,少于5行不展示加载更多,等于5行才判断是否要展示
if (lineCount == 5) {
// 文字已经全部展示不需要显示下边展示更多按钮
if (msg.length() <= lineEnd) {
return;
}
linkMore.setVisibility(View.VISIBLE);
linkMore.setText("全文");
} else if (lineCount > 5) {
// 展开后才会超过五行
linkMore.setVisibility(View.VISIBLE);
linkMore.setText("收起");
}
}
});

跑了一下,看到全文和收起可以正常使用了,美滋滋的以为so easy啊。然鹅,过不了多久就收到qa的bug邮件,说她的文本都没有全文按钮。不能啊,我这明明好好的呢,一定是见鬼了(我的代码不存在bug的,不接受反驳╭(๑¯д¯๑)╮)。多拿了几个机型跑了一下,发现Android 8.0以上的机器都不能正常显示全文按钮,跟debug后发现是lineEnd的值不准,返回的是全文的长度。翻google无果后,就想还是得靠自己,开始翻源码,然后!!!就发现了一个大坑。。

在sdk 27的源码中,getLineEnd()源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Return the text offset after the last character on the specified line.
*/
public final int getLineEnd(int line) {
return getLineStart(line + 1);
}

/**
* Return the text offset of the beginning of the specified line (
* 0&hellip;getLineCount()). If the specified line is equal to the line
* count, returns the length of the text.
*/
public abstract int getLineStart(int line);

看到getLineStart上面写着,如果参数line和目前TextView的line一致的话,就返回文本的全部长度,我整个人陷入了沉思。。。这么存在的一个方法有什么用啊,什么用。。。为什么Android 5.0可以用啊,可以用。。。都这样了,那只能自己想办法计算了,最后计算方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class SpannableTextView extends TextView {
private TextViewShowCallback callback;

public SpannableTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Layout mainLayout = getLayout();
int lineCount = mainLayout.getLineCount();
int leftLineWidth = 0; // 第五行的文本宽度
TextPaint textPaint = getPaint();
// 文字刚好等于5行的情况,需要另外计算已经显示的文字长度
if (lineCount == getMaxLines()) {
// 第四行末尾的文字长度
int lineEnd = mainLayout.getLineEnd(lineCount - 2);
// 剩下的文本
String leftStr = getText().subSequence(lineEnd, getText().length()).toString();
int i = 0;
for (; i < leftStr.length(); i++) {
// 获取每个字符的宽度,单位为pixel
leftLineWidth += textPaint.measureText(leftStr, i, i + 1);
// 如果超过TextView的宽度,说明第五行展示完了
if (leftLineWidth > getMeasuredWidth()) {
break;
}
}
// 加上第五行展示的字符个数,得到最终的lineEnd
lineEnd += i;
callback.configMoreText(lineCount, lineEnd);
return;
}
callback.configMoreText(lineCount, mainLayout.getLineEnd(lineCount - 1));
}

public void setCallback(TextViewShowCallback callback) {
this.callback = callback;
}

public interface TextViewShowCallback {
void configMoreText(int lineCount, long lineEnd);
}
}

在Android 5.0 7.0 8.0都跑了一下,这下真的可以正常展示。。。

写代码一分钟,解bug一天。。。╮(﹀_﹀)╭

最后附上在RecyclerView中的item里,点击全文和收起的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 标志展开的item的位置
int opened = -1;

// 在相关item的初始化位置
// 是否是已经展开的
if (opened == position) {
linkText.setMaxLines(Integer.MAX_VALUE);
linkText.setEllipsize(null);
} else {
linkText.setMaxLines(5);
linkText.setEllipsize(TextUtils.TruncateAt.END);
}

// 全文或者收起按钮点击监听
linkMore.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (opened == getAdapterPosition()) { // 已经展开,就关闭
opened = -1;
notifyItemChanged(getAdapterPosition());
} else {
int oldOpened = opened;
opened = getAdapterPosition();
notifyItemChanged(oldOpened);
notifyItemChanged(opened);
}
}
});

// 若需要自己修改itemchanged的动画时长等
linkRecyclerview.getItemAnimator().setChangeDuration(300);
0%