Android尾部带“查看更多”的TextView

首先是增添了几个属性;其次,也是最重要的,改进了调用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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值