Android自定义控件之全文收起TextView(控件嵌套法)

前言

因为公司项目需要全文收起的功能,一共有2种UI,所以需要写2个全文收起的控件,之前也是用过一个全文收起的TextView控件,但是因为设计原因,在ListView刷新的时候会闪烁,我估计原因是因为控件本身的设计是需要先让TextView绘制完成,然后获取TextView一共有多少行,再判断是否需要全文收起按钮,如果需要,则吧TextView压缩回最大行数,添加全文按钮,这样就会造成ListView的Item先高后低,所以会发生闪烁,后面我也在网上找了几个,发现和之前的设计都差不多,虽然肯定是有解决了这个问题的控件,但是还是决定自己写了,毕竟找到控件后还需要测试,而现在的项目时间不充分啊(另外欢迎指教如何快速的找到自己需要的控件,有时候在Github上面搜索,都不知道具体该用什么关键字),而且自己写,也是一种锻炼。这里讲述的是布局式的实现,还有一个就直接继承TextView来实现那个会在下一篇文章讲述。Android自定义控件之全文收起TextView(继承TextView法)

效果图

效果图

实现原理

其实很多全文收起的实现原理应该都差不多,首先外部是一个布局,里面放一个显示正文的TextView控件,设置文本后,判断正文TextView的控件到底有多少行,如果达到了全文收起的行数,则将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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package wang.raye.library.widge;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.widget.LinearLayout;
import android.widget.TextView;

import wang.raye.library.R;

/**
* 有全文和收起的TextView
* Created by Raye on 2016/6/24.
*/
public class MoreTextView extends LinearLayout {
/**
* TextView的实际高度
*/
private int textViewHeight;
/**
* 默认全文的Text
*/
private static final String EXPANDEDTEXT = "全文";
/**
* 默认收起的text
*/
private static final String COLLAPSEDTEXT = "收起";
/**
* 全文的text
*/
private String expandedText;
/**
* 收起的text
*/
private String collapsedText;
/**
* 字体大小
*/
private int textSize;
/**
* 字体颜色
*/
private int textColor;
/**
* 超过多少行出现全文、收起按钮
*/
private int trimLines;
/**
* 显示文本的TextView
*/
private TextView showTextView;
/**
* 全文和收起的TextView
*/
private TextView collapseTextView;
/**
* 是否是收起状态,默认收起
*/
private boolean collapsed = true;


public MoreTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context, attrs);
}

public MoreTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs);
}

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

private void initView(Context context, AttributeSet attrs) {
showTextView = new TextView(context);
setOrientation(VERTICAL);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MoreTextView);
textColor = typedArray.getColor(R.styleable.MoreTextView_textColor, Color.GRAY);
textSize = typedArray.getDimensionPixelSize(R.styleable.MoreTextView_textSize, 14);
expandedText = typedArray.getString(R.styleable.MoreTextView_expandedText);
if (TextUtils.isEmpty(expandedText)) {
expandedText = EXPANDEDTEXT;
}
collapsedText = typedArray.getString(R.styleable.MoreTextView_collapsedText);
if (TextUtils.isEmpty(collapsedText)) {
collapsedText = COLLAPSEDTEXT;
}
trimLines = typedArray.getInt(R.styleable.MoreTextView_trimLines, 0);
typedArray.recycle();
showTextView.setTextSize(textSize);
showTextView.setTextColor(textColor);
addView(showTextView);


}

public void setText(CharSequence text) {
globalLayout();
showTextView.setText(text);
}

/**
* 获取控件实际高度,并设置最大行数
*/
private void globalLayout() {
showTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewTreeObserver obs = showTextView.getViewTreeObserver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
obs.removeOnGlobalLayoutListener(this);
} else {
obs.removeGlobalOnLayoutListener(this);
}

int allLine = showTextView.getLineCount();

textViewHeight = showTextView.getLineHeight() * allLine;

if (trimLines > 0 && trimLines < allLine) {
//需要全文和收起
if (collapsed) {
showTextView.setHeight(showTextView.getLineHeight() * trimLines);
}

if (collapseTextView == null) {
//全文和收起的textView
collapseTextView = new TextView(getContext());
collapseTextView.setTextSize(textSize);
collapseTextView.setTextColor(Color.BLUE);
collapseTextView.setText(expandedText);
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM);
collapseTextView.setLayoutParams(lp);
collapseTextView.setOnClickListener(collapseListener);
addView(collapseTextView);

}

}
}
});
}

private OnClickListener collapseListener = new OnClickListener() {
@Override
public void onClick(final View v) {
v.setEnabled(false);
final int startValue = showTextView.getHeight();
final int deltaValue;

if (collapsed) {
//是放大
deltaValue = textViewHeight - startValue;

} else {
deltaValue = showTextView.getLineHeight() * trimLines - startValue;
}
Animation animation = new Animation() {
protected void applyTransformation(float interpolatedTime, Transformation t) {
showTextView.setHeight((int) (startValue + deltaValue * interpolatedTime));
}
};
animation.setDuration(500);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {

}

@Override
public void onAnimationEnd(Animation animation) {
v.setEnabled(true);
collapsed = !collapsed;
collapseTextView.setText(collapsed ? expandedText : collapsedText);
}

@Override
public void onAnimationRepeat(Animation animation) {

}
});
showTextView.startAnimation(animation);
}
};
}

这里主要是获取自定义参数的属性,并且在布局中添加一个显示正文的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
/**
* 获取控件实际高度,并设置最大行数
*/
private void globalLayout() {
showTextView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewTreeObserver obs = showTextView.getViewTreeObserver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
obs.removeOnGlobalLayoutListener(this);
} else {
obs.removeGlobalOnLayoutListener(this);
}

int allLine = showTextView.getLineCount();

textViewHeight = showTextView.getLineHeight() * allLine;

if (trimLines > 0 && trimLines < allLine) {
//需要全文和收起
if (collapsed) {
showTextView.setHeight(showTextView.getLineHeight() * trimLines);
}

if (collapseTextView == null) {
//全文和收起的textView
collapseTextView = new TextView(getContext());
collapseTextView.setTextSize(textSize);
collapseTextView.setTextColor(Color.BLUE);
collapseTextView.setText(expandedText);
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT, Gravity.RIGHT | Gravity.BOTTOM);
collapseTextView.setLayoutParams(lp);
collapseTextView.setOnClickListener(collapseListener);
addView(collapseTextView);

}

}
}
});
}

这里主要是在GlobalLayoutListener监听中,获取控件的实际高度,因为第一次GlobalLayoutListener会在onDraw方法前面调用,所以不会造成闪烁,同时判断总行数是否达到了需要收起的行数,如果达到了收起的行数,则设置textView的高度为行高*指定行数,因为没有padding等属性,所以不需要考虑,同时判断全文收起的按钮是否为空,为空就初始化控件,并添加到布局

点击事件
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
private OnClickListener collapseListener = new OnClickListener() {
@Override
public void onClick(final View v) {
v.setEnabled(false);
final int startValue = showTextView.getHeight();
final int deltaValue ;

if(collapsed){
//是放大
deltaValue = textViewHeight - startValue;

}else{
deltaValue = showTextView.getLineHeight() * trimLines - startValue;
}
Animation animation = new Animation() {
protected void applyTransformation(float interpolatedTime, Transformation t) {
showTextView.setHeight((int) (startValue + deltaValue * interpolatedTime));
}
};
animation.setDuration(500);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {

}

@Override
public void onAnimationEnd(Animation animation) {
v.setEnabled(true);
collapsed = !collapsed;
collapseTextView.setText(collapsed?expandedText:collapsedText);
}

@Override
public void onAnimationRepeat(Animation animation) {

}
});
showTextView.startAnimation(animation);
}
};

这里是全文收起按钮的点击事件,获取控件目前的高度,同时判断目前的状态,根据状态判断是收起还是展开,获取应该添加的高度(收起的,高度是负数),同时设置动画,并启动动画, 动画过程中设置正文的高度。这样一个全文收起的TextView就实现了。

结语

当然这个控件是非常简陋的,而且还有一两个bug,大家可以猜一下到底是啥问题。另外,我想知道就是到底TextView绘制的时候能不能获取到正确的行数,因为我测试过程中发现在onGlobalLayout中获取的每行文字数量会多次变化,但是最后一次获取到的是正确的,所以想知道为啥获取每行字数的时候会有误差,希望知道的解答一下,当然我自己也会查询资料了解,同时附上本控件源码和demo github链接

Authorship: 作者
Article Link: https://raye.wang/2016/06/29/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%E6%8E%A7%E4%BB%B6%E5%B5%8C%E5%A5%97%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 !