首先是增添了几个属性;其次,也是最重要的,改进了调用setText()重新设置文本时,其下方的View会发生抖动的问题,也就是onMeasure()中的那段代码。
FolderTextView.java
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
|
package
com.xiaobo.foldertextview;
import
android.content.Context;
import
android.content.res.TypedArray;
import
android.graphics.Canvas;
import
android.graphics.Color;
import
android.text.Layout;
import
android.text.SpannableString;
import
android.text.Spanned;
import
android.text.StaticLayout;
import
android.text.TextPaint;
import
android.text.TextUtils;
import
android.text.method.LinkMovementMethod;
import
android.text.style.ClickableSpan;
import
android.util.AttributeSet;
import
android.util.Log;
import
android.view.View;
import
android.widget.TextView;
/**
* 结尾带“查看全部”的TextView,点击可以展开文字,展开后可收起。
* <p/>
* 目前存在一个问题:外部调用setText()时会造成界面该TextView下方的View抖动;
* <p/>
* 可以先调用getFullText(),当已有文字和要设置的文字不一样才调用setText(),可降低抖动的次数;
* <p/>
* 通过在onMeasure()中设置高度已经修复了该问题了。
* <p/>
* Created by moxiaobo on 16/8/9.
*/
public
class
FolderTextView
extends
TextView {
// TAG
private
static
final
String TAG =
"xiaobo"
;
// 默认打点文字
private
static
final
String DEFAULT_ELLIPSIZE =
"..."
;
// 默认收起文字
private
static
final
String DEFAULT_FOLD_TEXT =
"[收起]"
;
// 默认展开文字
private
static
final
String DEFAULT_UNFOLD_TEXT =
"[查看全部]"
;
// 默认固定行数
private
static
final
int
DEFAULT_FOLD_LINE =
2
;
// 默认收起和展开文字颜色
private
static
final
int
DEFAULT_TAIL_TEXT_COLOR = Color.GRAY;
// 默认是否可以再次收起
private
static
final
boolean
DEFAULT_CAN_FOLD_AGAIN =
true
;
// 收起文字
private
String mFoldText;
// 展开文字
private
String mUnFoldText;
// 固定行数
private
int
mFoldLine;
// 尾部文字颜色
private
int
mTailColor;
// 是否可以再次收起
private
boolean
mCanFoldAgain =
false
;
// 收缩状态
private
boolean
mIsFold =
false
;
// 绘制,防止重复进行绘制
private
boolean
mHasDrawn =
false
;
// 内部绘制
private
boolean
mIsInner =
false
;
// 全文本
private
String mFullText;
// 行间距倍数
private
float
mLineSpacingMultiplier =
1
.0f;
// 行间距额外像素
private
float
mLineSpacingExtra =
0
.0f;
// 统计使用二分法裁剪源文本的次数
private
int
mCountBinary =
0
;
// 统计使用备用方法裁剪源文本的次数
private
int
mCountBackUp =
0
;
// 统计onDraw调用的次数
private
int
mCountOnDraw =
0
;
// 点击处理
private
ClickableSpan clickSpan =
new
ClickableSpan() {
@Override
public
void
onClick(View widget) {
mIsFold = !mIsFold;
mHasDrawn =
false
;
invalidate();
}
@Override
public
void
updateDrawState(TextPaint ds) {
ds.setColor(mTailColor);
}
};
/**
* 构造
*
* @param context 上下文
*/
public
FolderTextView(Context context) {
this
(context,
null
);
}
/**
* 构造
*
* @param context 上下文
* @param attrs 属性
*/
public
FolderTextView(Context context, AttributeSet attrs) {
this
(context, attrs,
0
);
}
/**
* 构造
*
* @param context 上下文
* @param attrs 属性
* @param defStyleAttr 样式
*/
public
FolderTextView(Context context, AttributeSet attrs,
int
defStyleAttr) {
super
(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FolderTextView);
mFoldText = a.getString(R.styleable.FolderTextView_foldText);
if
(
null
== mFoldText) {
mFoldText = DEFAULT_FOLD_TEXT;
}
mUnFoldText = a.getString(R.styleable.FolderTextView_unFoldText);
if
(
null
== mUnFoldText) {
mUnFoldText = DEFAULT_UNFOLD_TEXT;
}
mFoldLine = a.getInt(R.styleable.FolderTextView_foldLine, DEFAULT_FOLD_LINE);
if
(mFoldLine <
1
) {
throw
new
RuntimeException(
"foldLine must not less than 1"
);
}
mTailColor = a.getColor(R.styleable.FolderTextView_tailTextColor, DEFAULT_TAIL_TEXT_COLOR);
mCanFoldAgain = a.getBoolean(R.styleable.FolderTextView_canFoldAgain, DEFAULT_CAN_FOLD_AGAIN);
a.recycle();
}
@Override
public
void
setText(CharSequence text, BufferType type) {
if
(TextUtils.isEmpty(mFullText) || !mIsInner) {
mHasDrawn =
false
;
mFullText = String.valueOf(text);
}
super
.setText(text, type);
}
@Override
public
void
setLineSpacing(
float
extra,
float
multiplier) {
mLineSpacingExtra = extra;
mLineSpacingMultiplier = multiplier;
super
.setLineSpacing(extra, multiplier);
}
@Override
public
void
invalidate() {
super
.invalidate();
}
@Override
protected
void
onMeasure(
int
widthMeasureSpec,
int
heightMeasureSpec) {
super
.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 必须解释下:由于为了得到实际一行的宽度(makeTextLayout()中需要使用),必须要先把源文本设置上,然后再裁剪至指定行数;
// 这就导致了该TextView会先布局一次高度很高(源文本行数高度)的布局,裁剪后再次布局成指定行数高度,因而下方View会抖动;
// 这里的处理是,super.onMeasure()已经计算出了源文本的实际宽高了,取出指定行数的文本再次测量一下其高度,
// 然后把这个高度设置成最终的高度就行了!
if
(!mIsFold) {
Layout layout = getLayout();
int
line = getFoldLine();
if
(line < layout.getLineCount()) {
int
index = layout.getLineEnd(line -
1
);
if
(index >
0
) {
// 得到一个字符串,该字符串恰好占据mFoldLine行数的高度
String strWhichHasExactlyFoldLine = getText().subSequence(
0
, index).toString();
Log.d(TAG,
"strWhichHasExactlyFoldLine-->"
+ strWhichHasExactlyFoldLine);
layout = makeTextLayout(strWhichHasExactlyFoldLine);
// 把这个高度设置成最终的高度,这样下方View就不会抖动了
setMeasuredDimension(getMeasuredWidth(), layout.getHeight() + getPaddingTop() + getPaddingBottom());
}
}
}
}
@Override
protected
void
onDraw(Canvas canvas) {
Log.d(TAG,
"onDraw() "
+ mCountOnDraw++ +
", getMeasuredHeight() "
+ getMeasuredHeight());
if
(!mHasDrawn) {
resetText();
}
super
.onDraw(canvas);
mHasDrawn =
true
;
mIsInner =
false
;
}
/**
* 获取折叠文字
*
* @return 折叠文字
*/
public
String getFoldText() {
return
mFoldText;
}
/**
* 设置折叠文字
*
* @param foldText 折叠文字
*/
public
void
setFoldText(String foldText) {
mFoldText = foldText;
invalidate();
}
/**
* 获取展开文字
*
* @return 展开文字
*/
public
String getUnFoldText() {
return
mUnFoldText;
}
/**
* 设置展开文字
*
* @param unFoldText 展开文字
*/
public
void
setUnFoldText(String unFoldText) {
mUnFoldText = unFoldText;
invalidate();
}
/**
* 获取折叠行数
*
* @return 折叠行数
*/
public
int
getFoldLine() {
return
mFoldLine;
}
/**
* 设置折叠行数
*
* @param foldLine 折叠行数
*/
public
void
setFoldLine(
int
foldLine) {
mFoldLine = foldLine;
invalidate();
}
/**
* 获取尾部文字颜色
*
* @return 尾部文字颜色
*/
public
int
getTailColor() {
return
mTailColor;
}
/**
* 设置尾部文字颜色
*
* @param tailColor 尾部文字颜色
*/
public
void
setTailColor(
int
tailColor) {
mTailColor = tailColor;
invalidate();
}
/**
* 获取是否可以再次折叠
*
* @return 是否可以再次折叠
*/
public
boolean
isCanFoldAgain() {
return
mCanFoldAgain;
}
/**
* 获取全文本
*
* @return 全文本
*/
public
String getFullText() {
return
mFullText;
}
/**
* 设置是否可以再次折叠
*
* @param canFoldAgain 是否可以再次折叠
*/
public
void
setCanFoldAgain(
boolean
canFoldAgain) {
mCanFoldAgain = canFoldAgain;
invalidate();
}
/**
* 获取TextView的Layout,注意这里使用getWidth()得到宽度
*
* @param text 源文本
* @return Layout
*/
private
Layout makeTextLayout(String text) {
return
new
StaticLayout(text, getPaint(), getWidth() - getPaddingLeft() - getPaddingRight(), Layout.Alignment
.ALIGN_NORMAL, mLineSpacingMultiplier, mLineSpacingExtra,
true
);
}
/**
* 重置文字
*/
private
void
resetText() {
// 文字本身就小于固定行数的话,不添加尾部的收起/展开文字
Layout layout = makeTextLayout(mFullText);
if
(layout.getLineCount() <= getFoldLine()) {
setText(mFullText);
return
;
}
SpannableString spanStr =
new
SpannableString(mFullText);
if
(mIsFold) {
// 收缩状态
if
(mCanFoldAgain) {
spanStr = createUnFoldSpan(mFullText);
}
}
else
{
// 展开状态
spanStr = createFoldSpan(mFullText);
}
updateText(spanStr);
setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* 不更新全文本下,进行展开和收缩操作
*
* @param text 源文本
*/
private
void
updateText(CharSequence text) {
mIsInner =
true
;
setText(text);
}
/**
* 创建展开状态下的Span
*
* @param text 源文本
* @return 展开状态下的Span
*/
private
SpannableString createUnFoldSpan(String text) {
String destStr = text + mFoldText;
int
start = destStr.length() - mFoldText.length();
int
end = destStr.length();
SpannableString spanStr =
new
SpannableString(destStr);
spanStr.setSpan(clickSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return
spanStr;
}
/**
* 创建收缩状态下的Span
*
* @param text
* @return 收缩状态下的Span
*/
private
SpannableString createFoldSpan(String text) {
long
startTime = System.currentTimeMillis();
String destStr = tailorText(text);
Log.d(TAG, (System.currentTimeMillis() - startTime) +
"ms"
);
int
start = destStr.length() - mUnFoldText.length();
int
end = destStr.length();
SpannableString spanStr =
new
SpannableString(destStr);
spanStr.setSpan(clickSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return
spanStr;
}
/**
* 裁剪文本至固定行数(备用方法)
*
* @param text 源文本
* @return 裁剪后的文本
*/
private
String tailorTextBackUp(String text) {
Log.d(TAG,
"使用备用方法: tailorTextBackUp() "
+ mCountBackUp++);
String destStr = text + DEFAULT_ELLIPSIZE + mUnFoldText;
Layout layout = makeTextLayout(destStr);
// 如果行数大于固定行数
if
(layout.getLineCount() > getFoldLine()) {
int
index = layout.getLineEnd(getFoldLine() -
1
);
if
(text.length() < index) {
index = text.length();
}
// 从最后一位逐渐试错至固定行数(可以考虑用二分法改进)
if
(index <=
1
) {
return
DEFAULT_ELLIPSIZE + mUnFoldText;
}
String subText = text.substring(
0
, index -
1
);
return
tailorText(subText);
}
else
{
return
destStr;
}
}
/**
* 裁剪文本至固定行数(二分法)。经试验,在文字长度不是很长时,效率比备用方法高不少;当文字长度过长时,备用方法则优势明显。
*
* @param text 源文本
* @return 裁剪后的文本
*/
private
String tailorText(String text) {
// return tailorTextBackUp(text);
int
start =
0
;
int
end = text.length() -
1
;
int
mid = (start + end) /
2
;
int
find = finPos(text, mid);
while
(find !=
0
&& end > start) {
Log.d(TAG,
"使用二分法: tailorText() "
+ mCountBinary++);
if
(find >
0
) {
end = mid -
1
;
}
else
if
(find <
0
) {
start = mid +
1
;
}
mid = (start + end) /
2
;
find = finPos(text, mid);
}
Log.d(TAG,
"mid is: "
+ mid);
String ret;
if
(find ==
0
) {
ret = text.substring(
0
, mid) + DEFAULT_ELLIPSIZE + mUnFoldText;
}
else
{
ret = tailorTextBackUp(text);
}
return
ret;
}
/**
* 查找一个位置P,到P时为mFoldLine这么多行,加上一个字符‘A’后则刚好为mFoldLine+1这么多行
*
* @param text 源文本
* @param pos 位置
* @return 查找结果
*/
private
int
finPos(String text,
int
pos) {
String destStr = text.substring(
0
, pos) + DEFAULT_ELLIPSIZE + mUnFoldText;
Layout layout = makeTextLayout(destStr);
Layout layoutMore = makeTextLayout(destStr +
"A"
);
int
lineCount = layout.getLineCount();
int
lineCountMore = layoutMore.getLineCount();
if
(lineCount == getFoldLine() && (lineCountMore == getFoldLine() +
1
)) {
// 行数刚好到折叠行数
return
0
;
}
else
if
(lineCount > getFoldLine()) {
// 行数比折叠行数多
return
1
;
}
else
{
// 行数比折叠行数少
return
-
1
;
}
}
}
|
attrs.xml
1
2
3
4
5
6
7
8
9
10
|
<!--?xml version=
"1.0"
encoding=
"utf-8"
?-->
<resources>
<declare-styleable name=
"FolderTextView"
>
</attr></attr></attr></attr></attr></declare-styleable>
</resources>
|
上一帧
下一帧
最后一帧
可看到,上方的TextView先是占据了一个很高的高度,然后才会恢复,也就造成了下方View的抖动,修复后无此问题。
注意: 本文由 微信妈妈(公众号买卖ontaobao.cn) 编辑整理,转载地址 http://www.2cto.com/kf/201608/536431.html