Android自定义控件之全文收起TextView(继承TextView法)

前言

因为公司项目需要全文收起的功能,一共有2种UI,所以需要写2个全文收起的控件,关于第一个控件已经在第一篇文章讲述嵌套法实现全文收起TextView,本篇文章主要讲述直接继承至TextView的实现方法

效果图

全文收起TextView效果图

实现原理

通过另外一个方法设置文本,同时在GlobalLayoutListener中计算每行出需要显示的总行数,判断是否需要全文收起功能,如果需要,则计算出每行需要显示多少文本,在设定的最大行计算时,把…+全文加进去计算,得到实际上应该显示的文本,同时把全文设置为可点击的文本,在点击事件中根据状态设置当前TextView显示的文本,如果当前状态是收起状态,点击后就设置显示所有文字+收起,全文状态则设置显示文本为最开始计算出来的文本

代码

COPY
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package wang.raye.library.widge;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.TextView;

import wang.raye.library.R;


/**
* 有全文和收起的TextView ExpandableTextView
* Created by Raye on 2016/6/24.
*/
public class CollapsedTextView extends TextView {
private static final String TAG = CollapsedTextView.class.getName();
/** 收起状态下的最大行数*/
private int maxLine = 2;
/** 截取后,文本末尾的字符串*/
private static final String ELLIPSE = "...";
/** 默认全文的Text*/
private static final String EXPANDEDTEXT = "全文";
/** 默认收起的text*/
private static final String COLLAPSEDTEXT = "收起";
/** 全文的text*/
private String expandedText = EXPANDEDTEXT;
/** 收起的text*/
private String collapsedText = COLLAPSEDTEXT;
/** 所有行数*/
private int allLines;
/** 是否是收起状态,默认收起*/
private boolean collapsed = true;
/** 真实的text*/
private String text;
/** 收起时实际显示的text*/
private CharSequence collapsedCs;
/** 全文和收起的点击事件处理*/
private ReadMoreClickableSpan viewMoreSpan = new ReadMoreClickableSpan();

public CollapsedTextView(Context context) {
super(context);
init(context,null);
}

public CollapsedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context,attrs);
}

public CollapsedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context,attrs);
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CollapsedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context,attrs);
}

@Override
public TextPaint getPaint() {
return super.getPaint();
}

private void init(Context context,AttributeSet attrs){
if(attrs != null){
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CollapsedTextView);
allLines = ta.getInt(R.styleable.CollapsedTextView_trimLines,0);
expandedText = ta.getString(R.styleable.CollapsedTextView_expandedText);
if(TextUtils.isEmpty(expandedText)){
expandedText = EXPANDEDTEXT;
}
collapsedText = ta.getString(R.styleable.CollapsedTextView_collapsedText);
if(TextUtils.isEmpty(collapsedText)){
collapsedText = COLLAPSEDTEXT;
}
}

}
public void setShowText(final String text){
this.text = text;
if(allLines > 0) {
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewTreeObserver obs = getViewTreeObserver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
obs.removeOnGlobalLayoutListener(this);
} else {
obs.removeGlobalOnLayoutListener(this);
}
TextPaint tp = getPaint();
float width = tp.measureText(text);
/* 计算行数 */
//获取显示宽度
int showWidth = getWidth() - getPaddingRight() - getPaddingLeft();
int lines = (int) (width / showWidth);
if (width % showWidth != 0) {
lines++;
}
allLines = (int) (tp.measureText(text + collapsedText) / showWidth);
if (lines > maxLine) {
int expect = text.length() / lines;
int end = 0;
int lastLineEnd = 0;
//...+expandedText的宽度,需要在最后一行加入计算
int expandedTextWidth = (int) tp.measureText(ELLIPSE + expandedText);
//计算每行显示文本数
for (int i = 1; i <= maxLine; i++) {
int tempWidth = 0;
if (i == maxLine) {

tempWidth = expandedTextWidth;
}
end += expect;
if (end > text.length()) {
end = text.length();
}
if (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) {
//预期的第一行超过了实际显示的宽度
end--;
while (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) {
end--;
}
} else {
end++;
while (tp.measureText(text, lastLineEnd, end) < showWidth - tempWidth) {
end++;
}
end--;
}
lastLineEnd = end;
}
SpannableStringBuilder s = new SpannableStringBuilder(text, 0, end)
.append(ELLIPSE)
.append(expandedText);
collapsedCs = addClickableSpan(s, expandedText);
setText(collapsedCs);

setMovementMethod(LinkMovementMethod.getInstance());
} else {
setText(text);
}
}
});
setText("");
}else{
setText(text);
}
}


private CharSequence addClickableSpan(SpannableStringBuilder s, CharSequence trimText) {
s.setSpan(viewMoreSpan, s.length() - trimText.length(), s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return s;
}

private class ReadMoreClickableSpan extends ClickableSpan {
@Override
public void onClick(final View widget) {
if(collapsed){
SpannableStringBuilder s = new SpannableStringBuilder(text)
.append(collapsedText);
setText(addClickableSpan(s,collapsedText));
}else{
setText(collapsedCs);
}
collapsed = !collapsed;
}
}
}

详细说明

核心代码
COPY
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
46
47
48
49
50
51
52
TextPaint tp = getPaint();
float width = tp.measureText(text);
/* 计算行数 */
//获取显示宽度
int showWidth = getWidth() - getPaddingRight() - getPaddingLeft();
int lines = (int) (width / showWidth);
if (width % showWidth != 0) {
lines++;
}
allLines = (int) (tp.measureText(text + collapsedText) / showWidth);
if (lines > maxLine) {
int expect = text.length() / lines;
int end = 0;
int lastLineEnd = 0;
//...+expandedText的宽度,需要在最后一行加入计算
int expandedTextWidth = (int) tp.measureText(ELLIPSE + expandedText);
//计算每行显示文本数
for (int i = 1; i <= maxLine; i++) {
int tempWidth = 0;
if (i == maxLine) {

tempWidth = expandedTextWidth;
}
end += expect;
if (end > text.length()) {
end = text.length();
}
if (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) {
//预期的第一行超过了实际显示的宽度
end--;
while (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) {
end--;
}
} else {
end++;
while (tp.measureText(text, lastLineEnd, end) < showWidth - tempWidth) {
end++;
}
end--;
}
lastLineEnd = end;
}
SpannableStringBuilder s = new SpannableStringBuilder(text, 0, end)
.append(ELLIPSE)
.append(expandedText);
collapsedCs = addClickableSpan(s, expandedText);
setText(collapsedCs);

setMovementMethod(LinkMovementMethod.getInstance());
} else {
setText(text);
}

通过TextPaint计算出文本的总宽度,粗略计算出一共需要多少行来显示,判断是否需要收起和全文功能,如果需要,则计算出每行实际展示的文本的宽度(因为通过Layout获取到的只有完全绘制成功后,才能正确获取到),同时在计算的最后一行(也就是超过多少行需要收起的最后一行),需要把”…全文”的宽度加入计算,这样才能计算出正确值,把计算出来的字符数截取出来,加入”…全文”,同时针对”全文”本身,添加点击的ClickableSpan,使”全文”具有点击事件,最后设置控件展示的文本为截取的文本+”…全文”,如果行数没有超过最大行数,则设置正常显示就ok了,同时保存计算出来的文本,避免再次计算。

点击事件
COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private CharSequence addClickableSpan(SpannableStringBuilder s, CharSequence trimText) {
s.setSpan(viewMoreSpan, s.length() - trimText.length(), s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return s;
}

private class ReadMoreClickableSpan extends ClickableSpan {
@Override
public void onClick(final View widget) {
if(collapsed){
SpannableStringBuilder s = new SpannableStringBuilder(text)
.append(collapsedText);
setText(addClickableSpan(s,collapsedText));
}else{
setText(collapsedCs);
}
collapsed = !collapsed;
}
}

通过setSpan设置”全文”的点击事件,同时通过继承ClickableSpan 来实现点击事件,事件中根据当前的状态,判断需要设置什么文本,如果是收起状态,则设置文本显示内容为实际内容+”收起”,同时给收起添加点击事件,如果是全文状态,则设置显示的文本为之前计算出来的文本。

自定义属性
COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void init(Context context,AttributeSet attrs){
if(attrs != null){
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CollapsedTextView);
allLines = ta.getInt(R.styleable.CollapsedTextView_trimLines,0);
expandedText = ta.getString(R.styleable.CollapsedTextView_expandedText);
if(TextUtils.isEmpty(expandedText)){
expandedText = EXPANDEDTEXT;
}
collapsedText = ta.getString(R.styleable.CollapsedTextView_collapsedText);
if(TextUtils.isEmpty(collapsedText)){
collapsedText = COLLAPSEDTEXT;
}
}

}

这里就很简单了,就是自义定最大多少行,”全文”的文本和”收起”的文本,相信不用多少

最后说两句

最近因为太忙,所以文章也写的有点水,而且总是感觉累,是身体加心累,每天躺床上就不想起床,也不喜欢敲代码,效率自热底下。同时也建议各位同行注意身体,身体才是革命的本钱,同时也要注意放松,不然心一旦累了,就很难调整过来了(对于我来说是这样),敲会代码就起身走动走动,前几天因为一直坐着敲代码,脖子痛的要命,所以适当的休息是必要的,好了,就说这么多吧,你们懂的。同时附上本控件源码和demo github链接,另外同时也欢迎大家吐槽交流(QQ群:123965382)

Authorship: 作者
Article Link: https://raye.wang/2016/07/14/Android%E8%87%AA%E5%AE%9A%E4%B9%89%E6%8E%A7%E4%BB%B6%E4%B9%8B%E5%85%A8%E6%96%87%E6%94%B6%E8%B5%B7TextView%EF%BC%88%E7%BB%A7%E6%89%BFTextView%E6%B3%95%EF%BC%89/
Copyright: All posts on this blog are licensed under the CC BY-NC-SA 4.0 license unless otherwise stated. Please cite Raye Blog !