最近,写Android代码遇到的需求,需要文本可折叠,通过参考借鉴网上的资料和思路,自己写了一个。写篇博客记录下来,免得以后要用的时候,又要百度。
需求:在折叠状态下,文本如果大于最大显示行数,则在文本框能显示的最后一行加上“... 展开”,并与可显示的文本一起铺满整个文本框;在展开状态下,文本尾加上“ 折叠”。“展开”和“折叠”字体均为红色,且均可点击,点击后文本框变化到对应状态。
上代码(用ClickableSpan实现):
public class ExpandTextView extends AppCompatTextView {
private String originText = ""; //原始内容文本
private int initWidth = 0; //TextView可展示宽度
private int mMaxLines = 2; //TextView最大显示行数
private SpannableString SPAN_CLOSE = null; //收起的文本(颜色处理和点击监听)
private SpannableString SPAN_EXPAND = null; //展开的文本(颜色处理和点击监听)
private String TEXT_EXPAND = " 展开";
private String TEXT_CLOSE = " 收起";
private int SPAN_COLOR = R.color.red;
private boolean isClose = true;
private boolean isWidthModify = true; //用于控件宽高改变时通知重绘的判断,避免循环绘制
public ExpandTextView(Context context) {
super(context);
}
public ExpandTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExpandTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
initWidth = getMeasuredWidth();
if(isWidthModify) {
notifyRefreshView();
isWidthModify = false;
}
}
/**
* 初始化"展开"的Span文本,即收起状态的文本尾
*/
private void initCloseEnd(){
String content = TEXT_EXPAND;
SPAN_CLOSE = new SpannableString(content);
SPAN_CLOSE.setSpan(new NoRefCopySpan() {
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(getResources().getColor(SPAN_COLOR));
ds.setUnderlineText(false);
}
@Override
public void onClick(@NonNull View widget) {
ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
setExpandText(originText);
}
}, 0, content.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
/**
* 初始化"收起"的Span文本,即展开状态的文本尾
*/
private void initExpandEnd(){
String content = TEXT_CLOSE;
SPAN_EXPAND = new SpannableString(content);
SPAN_EXPAND.setSpan(new NoRefCopySpan() {
@Override
public void updateDrawState(TextPaint ds) {
ds.setColor(getResources().getColor(SPAN_COLOR));
ds.setUnderlineText(false);
}
@Override
public void onClick(@NonNull View widget) {
ExpandTextView.super.setMaxLines(mMaxLines);
setCloseText(originText);
}
}, 0, content.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
/**
* 设置收起状态时的文本
*/
private void setCloseText(CharSequence text) {
if(SPAN_CLOSE == null){
initCloseEnd();
}
boolean appendShowAll = false;//true不需要展开收起功能(文件行数小于或等于最大行),false需要展开收起功能
originText = text.toString();
//SDK >= 16 可以直接从xml属性获取最大行数
int maxLines;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
maxLines = getMaxLines();
} else{
maxLines = mMaxLines;
}
String workingText = new StringBuilder(originText).toString();
if (maxLines != -1) {
Layout layout = createWorkingLayout(workingText);
if (layout.getLineCount() > maxLines) {
//获取一行显示字符个数,然后截取字符串数
workingText = originText.substring(0, layout.getLineEnd(maxLines - 1)).trim();// 收起状态原始文本截取展示的部分
String showText = originText.substring(0, layout.getLineEnd(maxLines - 1)).trim() + "..." + SPAN_CLOSE;
Layout layout2 = createWorkingLayout(showText);
//对workingText进行-1截取,直到展示行数==最大行数,并且添加SPAN_CLOSE后刚好占满最后一行
while (layout2.getLineCount() > maxLines) {
int lastSpace = workingText.length()-1;
if (lastSpace == -1) {
break;
}
workingText = workingText.substring(0, lastSpace);
layout2 = createWorkingLayout(workingText + "..." + SPAN_CLOSE);
}
appendShowAll = true;
workingText = workingText + "...";
}
}
setText(workingText);
if (appendShowAll) {
// 必须使用append,不能在上面使用+连接,否则spannable会无效
append(SPAN_CLOSE);
setMovementMethod(LinkMovementMethod.getInstance());
}
}
/**
* 设置展开状态时的文本
*/
private void setExpandText(String text) {
if(SPAN_EXPAND == null){
initExpandEnd();
}
Layout layout1 = createWorkingLayout(text);
Layout layout2 = createWorkingLayout(text + TEXT_CLOSE);
// 展示全部原始内容时 如果 TEXT_CLOSE 需要换行才能显示完整,则直接将TEXT_CLOSE展示在下一行
if(layout2.getLineCount() > layout1.getLineCount()){
setText(originText + "\n");
}else{
setText(originText);
}
append(SPAN_EXPAND);
setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* 返回textview的显示区域的layout,该textview的layout并不会显示出来
* 只是用其宽度来比较要显示的文字是否过长。
*/
private Layout createWorkingLayout(String workingText) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return new StaticLayout(workingText, getPaint(), initWidth - getPaddingLeft() - getPaddingRight(),
Layout.Alignment.ALIGN_NORMAL, getLineSpacingMultiplier(), getLineSpacingExtra(), false);
} else{
return new StaticLayout(workingText, getPaint(), initWidth - getPaddingLeft() - getPaddingRight(),
Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
}
}
/**
* sdk>=16,可以通过xml进行配置,但16以下必须用该方法配置
*/
@Override
public void setMaxLines(int maxLines) {
this.mMaxLines = maxLines;
super.setMaxLines(maxLines);
}
/**
* @param originText 待显示的原文本;
* @param isClose 设置TextView的初始状态是否为收起;
* 设置文本时,需要调用该方法来代替setText。若直接使用setText方法设置文本,
* 该控件只会作为普通TextView显示。
*/
public void setText(String originText, Boolean isClose) {
this.originText = originText;
this.isClose = isClose;
isWidthModify = true;
notifyRefreshView();
}
/**
* 修改设置后通知重绘,setText(String originText, Boolean isClose)方法不需要调用
*/
public void notifyRefreshView() {
if(isClose) {
ExpandTextView.super.setMaxLines(mMaxLines);
setCloseText(originText);
} else {
ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
setExpandText(originText);
}
}
/**
* @param colorId 展开和收起时,文本尾的Span文本的颜色;
* 设置展开和收起时文本尾的Span文本的颜色(若不以该方法设置,则默认为R.color.red)。
*/
public void setLabelTextColor(int colorId) {
SPAN_COLOR = colorId;
initExpandEnd();
initCloseEnd();
}
/**
* @param str 收起时,文本尾代表"展开"的Span文本内容;
* 设置文本尾的"展开"Span文本的内容(若不以该方法设置,则默认为"展开")。
*/
public void setExpandLabelText(String str) {
TEXT_EXPAND = str;
initExpandEnd();
}
/**
* @param str 展开时,文本尾代表"收起"的Span文本内容;
* 设置文本尾的"收起"Span文本的内容(若不以该方法设置,则默认为"收起")。
*/
public void setCLOSELabelText(String str) {
TEXT_CLOSE = str;
initCloseEnd();
}
/**
* 实现NoCopySpan的原因是,为了防止Span在onSaveInstanceState时被复制保存,导致内存泄漏
*/
public static class NoRefCopySpan extends ClickableSpan implements NoCopySpan {
@Override
public void onClick(@NonNull View widget) {
//等待子类继承
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
}
}
}