Android 自定义 View-->TextView 的展开 & 收起(文本折叠)
Posted Kevin_小飞象
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 自定义 View-->TextView 的展开 & 收起(文本折叠)相关的知识,希望对你有一定的参考价值。
前言
我们经常会碰到这样一个需求:文本内容过多,可以展开和收起。
效果图
注意:
- 显示 “…展开” 时,是截取的一定行数之后,在最后一行的末尾直接显示;
- “收起” 显示在全部文本的下一行,并且是右对齐;
- 展开和收起的动画效果。
代码实现
1. ExpandTextView.java
public class ExpandTextView extends AppCompatTextView
public static final String ELLIPSIS_STRING = new String(new char[]'\\u2026');
private static final int DEFAULT_MAX_LINE = 3;
private static final String DEFAULT_OPEN_SUFFIX = " 展开";
private static final String DEFAULT_CLOSE_SUFFIX = " 收起";
volatile boolean animating = false;
boolean isClosed = false;
private int mMaxLines = DEFAULT_MAX_LINE;
/** TextView可展示宽度,包含paddingLeft和paddingRight */
private int initWidth = 0;
/** 原始文本 */
private CharSequence originalText;
private SpannableStringBuilder mOpenSpannableStr, mCloseSpannableStr;
private boolean hasAnimation = false;
private Animation mOpenAnim, mCloseAnim;
private int mOpenHeight, mCLoseHeight;
private boolean mExpandable;
private boolean mCloseInNewLine;
@Nullable
private SpannableString mOpenSuffixSpan, mCloseSuffixSpan;
private String mOpenSuffixStr = DEFAULT_OPEN_SUFFIX;
private String mCloseSuffixStr = DEFAULT_CLOSE_SUFFIX;
private int mOpenSuffixColor, mCloseSuffixColor;
private View.OnClickListener mOnClickListener;
private CharSequenceToSpannableHandler mCharSequenceToSpannableHandler;
public ExpandTextView(Context context)
this(context,null);
public ExpandTextView(Context context, AttributeSet attrs)
this(context, attrs,0);
public ExpandTextView(Context context, AttributeSet attrs, int defStyleAttr)
super(context, attrs, defStyleAttr);
initialize();
/** 初始化 */
private void initialize()
mOpenSuffixColor = mCloseSuffixColor = Color.parseColor("#F23030");
setMovementMethod(OverLinkMovementMethod.getInstance());
setIncludeFontPadding(false);
updateOpenSuffixSpan();
updateCloseSuffixSpan();
@Override
public boolean hasOverlappingRendering()
return false;
public void setOriginalText(CharSequence originalText)
this.originalText = originalText;
mExpandable = false;
mCloseSpannableStr = new SpannableStringBuilder();
final int maxLines = mMaxLines;
SpannableStringBuilder tempText = charSequenceToSpannable(originalText);
mOpenSpannableStr = charSequenceToSpannable(originalText);
if (maxLines != -1)
Layout layout = createStaticLayout(tempText);
mExpandable = layout.getLineCount() > maxLines;
if (mExpandable)
//拼接展开内容
if (mCloseInNewLine)
mOpenSpannableStr.append("\\n");
if (mCloseSuffixSpan != null)
mOpenSpannableStr.append(mCloseSuffixSpan);
//计算原文截取位置
int endPos = layout.getLineEnd(maxLines - 1);
if (originalText.length() <= endPos)
mCloseSpannableStr = charSequenceToSpannable(originalText);
else
mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, endPos));
SpannableStringBuilder tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
if (mOpenSuffixSpan != null)
tempText2.append(mOpenSuffixSpan);
//循环判断,收起内容添加展开后缀后的内容
Layout tempLayout = createStaticLayout(tempText2);
while (tempLayout.getLineCount() > maxLines)
int lastSpace = mCloseSpannableStr.length() - 1;
if (lastSpace == -1)
break;
if (originalText.length() <= lastSpace)
mCloseSpannableStr = charSequenceToSpannable(originalText);
else
mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));
tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
if (mOpenSuffixSpan != null)
tempText2.append(mOpenSuffixSpan);
tempLayout = createStaticLayout(tempText2);
int lastSpace = mCloseSpannableStr.length() - mOpenSuffixSpan.length();
if(lastSpace >= 0 && originalText.length() > lastSpace)
CharSequence redundantChar = originalText.subSequence(lastSpace, lastSpace + mOpenSuffixSpan.length());
int offset = hasEnCharCount(redundantChar) - hasEnCharCount(mOpenSuffixSpan) + 1;
lastSpace = offset <= 0 ? lastSpace : lastSpace - offset;
mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));
//计算收起的文本高度
mCLoseHeight = tempLayout.getHeight() + getPaddingTop() + getPaddingBottom();
mCloseSpannableStr.append(ELLIPSIS_STRING);
if (mOpenSuffixSpan != null)
mCloseSpannableStr.append(mOpenSuffixSpan);
isClosed = mExpandable;
if (mExpandable)
setText(mCloseSpannableStr);
//设置监听
super.setOnClickListener(new OnClickListener()
@Override
public void onClick(View v)
// switchOpenClose();
// if (mOnClickListener != null)
// mOnClickListener.onClick(v);
//
);
else
setText(mOpenSpannableStr);
private int hasEnCharCount(CharSequence str)
int count = 0;
if(!TextUtils.isEmpty(str))
for (int i = 0; i < str.length(); i++)
char c = str.charAt(i);
if(c >= ' ' && c <= '~')
count++;
return count;
private void switchOpenClose()
if (mExpandable)
isClosed = !isClosed;
if (isClosed)
close();
else
open();
/**
* 设置是否有动画
*
* @param hasAnimation
*/
public void setHasAnimation(boolean hasAnimation)
this.hasAnimation = hasAnimation;
/** 展开 */
private void open()
if (hasAnimation)
Layout layout = createStaticLayout(mOpenSpannableStr);
mOpenHeight = layout.getHeight() + getPaddingTop() + getPaddingBottom();
executeOpenAnim();
else
ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
setText(mOpenSpannableStr);
if (mOpenCloseCallback != null)
mOpenCloseCallback.onOpen();
/** 收起 */
private void close()
if (hasAnimation)
executeCloseAnim();
else
ExpandTextView.super.setMaxLines(mMaxLines);
setText(mCloseSpannableStr);
if (mOpenCloseCallback != null)
mOpenCloseCallback.onClose();
/** 执行展开动画 */
private void executeOpenAnim()
//创建展开动画
if (mOpenAnim == null)
mOpenAnim = new ExpandCollapseAnimation(this, mCLoseHeight, mOpenHeight);
mOpenAnim.setFillAfter(true);
mOpenAnim.setAnimationListener(new Animation.AnimationListener()
@Override
public void onAnimationStart(Animation animation)
ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
setText(mOpenSpannableStr);
@Override
public void onAnimationEnd(Animation animation)
// 动画结束后textview设置展开的状态
getLayoutParams().height = mOpenHeight;
requestLayout();
animating = false;
@Override
public void onAnimationRepeat(Animation animation)
);
if (animating)
return;
animating = true;
clearAnimation();
// 执行动画
startAnimation(mOpenAnim);
/** 执行收起动画 */
private void executeCloseAnim()
//创建收起动画
if (mCloseAnim == null)
mCloseAnim = new ExpandCollapseAnimation(this, mOpenHeight, mCLoseHeight);
mCloseAnim.setFillAfter(true);
mCloseAnim.setAnimationListener(new Animation.AnimationListener()
@Override
public void onAnimationStart(Animation animation)
@Override
public void onAnimationEnd(Animation animation)
animating = false;
ExpandTextView.super.setMaxLines(mMaxLines);
setText(mCloseSpannableStr);
getLayoutParams().height = mCLoseHeight;
requestLayout();
@Override
public void onAnimationRepeat(Animation animation)
);
if (animating)
return;
animating = true;
clearAnimation();
// 执行动画
startAnimation(mCloseAnim);
/**
* @param spannable
*
* @return
*/
private Layout createStaticLayout(SpannableStringBuilder spannable)
int contentWidth = initWidth - getPaddingLeft() - getPaddingRight();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
StaticLayout.Builder builder = StaticLayout.Builder.obtain(spannable, 0, spannable.length(), getPaint(), contentWidth);
builder.setAlignment(Layout.Alignment.ALIGN_NORMAL);
builder.setIncludePad(getIncludeFontPadding());
builder.setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier());
return builder.build();
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
getLineSpacingMultiplier(), getLineSpacingExtra(), getIncludeFontPadding());
else
return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
getFloatField("mSpacingMult",1f), getFloatField("mSpacingAdd",0f), getIncludeFontPadding());
private float getFloatField(String fieldName,float defaultValue)
float value = defaultValue;
if(TextUtils.isEmpty(fieldName))
return value;
try
// 获取该类的所有属性值域
Field[] fields = this.getClass().getDeclaredFields();
for (Field field:fields)
if(TextUtils.equals(fieldName,field.getName()))
value = field.getFloat(this);
break;
catch (IllegalAccessException e)
e.printStackTrace();
return value;
/**
* @param charSequence
*
* @return
*/
private SpannableStringBuilder charSequenceToSpannable(@NonNull CharSequence charSequence)
SpannableStringBuilder spannableStringBuilder = null;
if (mCharSequenceToSpannableHandler != null)
spannableStringBuilder = mCharSequenceToSpannableHandler.charSequenceToSpannable(charSequence);
if (spannableStringBuilder == null)
spannableStringBuilder = new SpannableStringBuilder(charSequence);
return spannableStringBuilder;
/**
* 初始化TextView的可展示宽度
*
* @param width
*/
public void initWidth(int width)
initWidth = width;
@Override
public void setMaxLines(int maxLines)
Android自定义View