实现可折叠的TextView最重要的一点是在setText()前计算出text所需的行数
计算行数须要分为两种状况javascript
行数等于text的宽度除于TextView的宽度
再判断text的宽度对TextView的宽度取余是否为0,若是不等于0则加1
lines = textWidth / TextViewWidth + textWidth % TextViewWidth == 0 ? 0 : 1复制代码
1. 先用换行符拆分
2. 对于拆分后的文本
若是不为空,则而后再按照没有换行符的方式计算
若是为空,则行数为1
3. 累加全部的拆分文本行数复制代码
计算出text的行数以后,须要对text进行截取,截取到text能在指定的行数内显示完的位置,html
调用TextUtils的ellipsize()方法对指定的段落进行截取,ellipsize()方法中的avail参数,传入剩余的可显示宽度 java
由于在文本的最后要拼接上“...提示文本”,因此可显宽度的计算方式以下:android
TextViewWidth * (指定行数 - 累加行数) - (... + 提示文本)Width复制代码
实现可折叠的TextView须要继承TextView并重写setText(CharSequence text, BufferType type)方法git
由于setText(CharSequence text)方法是final的,而且setText(CharSequence text)最终调用的也是setText(CharSequence text, BufferType type)方法,因此重写后者便可。github
核心代码app
/** * 末尾省略号 */
private static final String ELLIPSE = "...";
/** * 默认的折叠行数 */
public static final int COLLAPSED_LINES = 4;
/** * 折叠时的默认文本 */
private static final String EXPANDED_TEXT = "展开全文";
/** * 展开时的默认文本 */
private static final String COLLAPSED_TEXT = "收起全文";
/** * 在文本末尾 */
public static final int END = 0;
/** * 在文本下方 */
public static final int BOTTOM = 1;
/** * 提示文字展现的位置 */
@IntDef({END, BOTTOM})
@Retention(RetentionPolicy.SOURCE)
public @interface TipsGravityMode {}
/** * 折叠的行数 */
private int mCollapsedLines;
/** * 折叠时的文本 */
private String mExpandedText;
/** * 展开时的文本 */
private String mCollapsedText;
/** * 折叠时的图片资源 */
private Drawable mExpandedDrawabl
/** * 展开时的图片资源 */
private Drawable mCollapsedDrawab
/** * 原始的文本 */
private CharSequence mOriginalTex
/** * TextView中文字可显示的宽度 */
private int mShowWidth;
/** * 是不是展开的 */
private boolean mIsExpanded;
/** * 提示文字位置 */
private int mTipsGravity;
/** * 提示文字颜色 */
private int mTipsColor;
/** * 提示文字是否显示下划线 */
private boolean mTipsUnderline;
/** * 提示是否可点击 */
private boolean mTipsClickable;
...
@Override
public void setText(CharSequence text, final BufferType type) {
// 若是text为空或mCollapsedLines为0则直接显示
if (TextUtils.isEmpty(text) || mCollapsedLines == 0) {
super.setText(text, type);
} else if (mIsExpanded) {
// 保存原始文本,去掉文本末尾的空字符
this.mOriginalText = CharUtil.trimFrom(text);
formatExpandedText(type);
} else {
// 保存原始文本,去掉文本末尾的空字符
this.mOriginalText = CharUtil.trimFrom(text);
// 获取TextView中文字显示的宽度,须要在layout以后才能获取到,避免在列表中重复获取
if (mCollapsedLines > 0 && mShowWidth == 0) {
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
mShowWidth = getWidth() - getPaddingLeft() - getPaddingRight();
formatCollapsedText(type);
}
});
} else {
formatCollapsedText(type);
}
}
}
/** * 格式化折叠时的文本 * * @param type ref android.R.styleable#TextView_bufferType */
private void formatCollapsedText(BufferType type) {
// 将原始文本按换行符拆分红段落
String[] paragraphs = mOriginalText.toString().split("\\n");
// 获取paint,用于计算文字宽度
TextPaint paint = getPaint();
// 文字宽度
float textWidth;
// 字符数,用于最后截取字符串
int charCount = 0;
// 剩余行数
int lastLines = mCollapsedLines;
for (int i = 0; i < paragraphs.length; i++) {
// 每一个段落
String paragraph = paragraphs[i];
// 每一个段落文本的宽度
textWidth = paint.measureText(paragraph);
// 计算每段的行数
int paragraphLines = (int) (textWidth / mShowWidth);
// 若是该段为空(表示空行)或还有余,多加一行
if (TextUtils.isEmpty(paragraph) || textWidth % mShowWidth != 0) {
paragraphLines++;
}
if (paragraphLines < lastLines) {
// 若是该段落行数小于等于剩余的行数,则减小lastLines,并增长字符数
// 这里只计算字符数,并不拼接字符
charCount += paragraph.length() + 1;
lastLines -= paragraphLines;
if (i == paragraphs.length - 1) {
super.setText(mOriginalText, type);
break;
}
} else if (paragraphLines == lastLines && i == paragraphs.length - 1) {
// 若是该段落行数等于剩余行数,而且是最后一个段落,表示恰好可以显示彻底
super.setText(mOriginalText, type);
break;
} else {
// 若是该段落的行数大于等于剩余的行数,则格式化文本
// 因设置的文本多是带有样式的文本,如SpannableStringBuilder,因此根据计算的字符数从原始文本中截取
SpannableStringBuilder spannable = new SpannableStringBuilder(mOriginalText, 0, charCount);
// 计算后缀的宽度,因样式的问题对后缀的宽度乘2
int expandedTextWidth = 2 * (int) (paint.measureText(ELLIPSE + mExpandedText));
// 获取最后一段的文本,仍是由于原始文本的样式缘由不能直接使用paragraphs中的文本
CharSequence lastParagraph = mOriginalText.subSequence(charCount, charCount + paragraph.length());
// 对最后一段文本进行截取
CharSequence ellipsizeText = TextUtils.ellipsize(lastParagraph, paint,
mShowWidth * lastLines - expandedTextWidth, TextUtils.TruncateAt.END);
spannable.append(ellipsizeText);
// 若是lastParagraph == ellipsizeText表示最后一段文本在可显示范围内,此时须要手动加上"..."
// 若是lastParagraph != ellipsizeText表示进行了截取TextUtils.ellipsize()方法会自动加上"..."
if (lastParagraph == ellipsizeText) {
spannable.append(ELLIPSE);
}
// 设置样式
setSpan(spannable);
// 使点击有效
setMovementMethod(LinkMovementMethod.getInstance());
super.setText(spannable, type);
break;
}
}
}
/** * 格式化展开式的文本,直接在后面拼接便可 * * @param type */
private void formatExpandedText(BufferType type) {
SpannableStringBuilder spannable = new SpannableStringBuilder(mOriginalText);
setSpan(spannable);
super.setText(spannable, type);
}
/** * 设置提示的样式 * * @param spannable 需修改样式的文本 */
private void setSpan(SpannableStringBuilder spannable) {
Drawable drawable;
// 根据提示文本须要展现的文字拼接不一样的字符
if (mTipsGravity == END) {
spannable.append(" ");
} else {
spannable.append("\n");
}
int tipsLen;
// 判断是展开仍是收起
if (mIsExpanded) {
spannable.append(mCollapsedText);
drawable = mCollapsedDrawable;
tipsLen = mCollapsedText.length();
} else {
spannable.append(mExpandedText);
drawable = mExpandedDrawable;
tipsLen = mExpandedText.length();
}
// 设置点击事件
spannable.setSpan(new ExpandedClickableSpan(), spannable.length() - tipsLen,
spannable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
// 若是提示的图片资源不为空,则使用图片代替提示文本
if (drawable != null) {
spannable.setSpan(new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE),
spannable.length() - tipsLen, spannable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
/** * 提示的点击事件 */
private class ExpandedClickableSpan extends ClickableSpan {
@Override
public void onClick(View widget) {
// 是否可点击
if (mTipsClickable) {
mIsExpanded = !mIsExpanded;
setText(mOriginalText);
}
}
@Override
public void updateDrawState(TextPaint ds) {
// 设置提示文本的颜色和是否须要下划线
ds.setColor(mTipsColor == 0 ? ds.linkColor : mTipsColor);
ds.setUnderlineText(mTipsUnderline);
}
}复制代码
能够从这里获取代码ide