出处:http://www.raye.wang/2016/07/14/androidzi-ding-yi-kong-jian-zhi-quan-wen-shou-qi-textview-ji-cheng-textviewfa/
因为公司项目需要全文收起的功能,一共有2种UI,所以需要写2个全文收起的控件,关于第一个控件已经在第一篇文章讲述嵌套法实现全文收起TextView,本篇文章主要讲述直接继承至TextView的实现方法
通过另外一个方法设置文本,同时在GlobalLayoutListener中计算每行出需要显示的总行数,判断是否需要全文收起功能,如果需要,则计算出每行需要显示多少文本,在设定的最大行计算时,把...+全文加进去计算,得到实际上应该显示的文本,同时把全文设置为可点击的文本,在点击事件中根据状态设置当前TextView显示的文本,如果当前状态是收起状态,点击后就设置显示所有文字+收起,全文状态则设置显示文本为最开始计算出来的文本
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;
}
}
}
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了,同时保存计算出来的文本,避免再次计算。
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 来实现点击事件,事件中根据当前的状态,判断需要设置什么文本,如果是收起状态,则设置文本显示内容为实际内容+"收起",同时给收起添加点击事件,如果是全文状态,则设置显示的文本为之前计算出来的文本。
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)