先上效果
一学赛、思路
1. 計算text的行數(shù)
實現(xiàn)可折疊的TextView最重要的一點是在setText()前計算出text所需的行數(shù)
計算行數(shù)需要分為兩種情況
1.1 沒有換行符的text
行數(shù)等于text的寬度除于TextView的寬度
再判斷text的寬度對TextView的寬度取余是否為0吞杭,如果不等于0則加1
lines = textWidth / TextViewWidth + textWidth % TextViewWidth == 0 ? 0 : 1
1.2 含有換行符的text
1. 先用換行符拆分
2. 對于拆分后的文本
如果不為空,則然后再按照沒有換行符的方式計算
如果為空芽狗,則行數(shù)為1
3. 累加所有的拆分文本行數(shù)
2. 截取text
計算出text的行數(shù)之后,需要對text進行截取滴劲,截取到text能在指定的行數(shù)內(nèi)顯示完的位置,
首先用換行符對text進行拆分班挖,將text分為若干段落
對拆分后的文本段落循環(huán)計算行數(shù)累加,并累加字符數(shù)
累加的行數(shù)小于指定行數(shù)萧芙,繼續(xù)循環(huán)假丧,直到累加的行數(shù)大于指定行數(shù)或循環(huán)完成双揪;如果在循環(huán)完成之前累加的行數(shù)大于指定行數(shù)包帚,則截取該次循環(huán)的段落
-
調(diào)用TextUtils的ellipsize()方法對指定的段落進行截取,ellipsize()方法中的avail參數(shù)婴噩,傳入剩余的可顯示寬度
因為在文本的最后要拼接上“...提示文本”羽德,所以可顯寬度的計算方式如下:
TextViewWidth * (指定行數(shù) - 累加行數(shù)) - (... + 提示文本)Width
把截取后的文本設置給TextView
二、實現(xiàn)
實現(xiàn)可折疊的TextView需要繼承TextView并重寫setText(CharSequence text, BufferType type)方法
因為setText(CharSequence text)方法是final的宅静,并且setText(CharSequence text)最終調(diào)用的也是setText(CharSequence text, BufferType type)方法章蚣,所以重寫后者即可。
核心代碼
/**
* 末尾省略號
*/
private static final String ELLIPSE = "...";
/**
* 默認的折疊行數(shù)
*/
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 {}
/**
* 折疊的行數(shù)
*/
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;
// 字符數(shù)地粪,用于最后截取字符串
int charCount = 0;
// 剩余行數(shù)
int lastLines = mCollapsedLines;
for (int i = 0; i < paragraphs.length; i++) {
// 每個段落
String paragraph = paragraphs[i];
// 每個段落文本的寬度
textWidth = paint.measureText(paragraph);
// 計算每段的行數(shù)
int paragraphLines = (int) (textWidth / mShowWidth);
// 如果該段為空(表示空行)或還有余琐谤,多加一行
if (TextUtils.isEmpty(paragraph) || textWidth % mShowWidth != 0) {
paragraphLines++;
}
if (paragraphLines < lastLines) {
// 如果該段落行數(shù)小于等于剩余的行數(shù),則減少lastLines斗忌,并增加字符數(shù)
// 這里只計算字符數(shù),并不拼接字符
charCount += paragraph.length() + 1;
lastLines -= paragraphLines;
if (i == paragraphs.length - 1) {
super.setText(mOriginalText, type);
break;
}
} else if (paragraphLines == lastLines && i == paragraphs.length - 1) {
// 如果該段落行數(shù)等于剩余行數(shù)飞蹂,并且是最后一個段落,表示剛好能夠顯示完全
super.setText(mOriginalText, type);
break;
} else {
// 如果該段落的行數(shù)大于等于剩余的行數(shù)陈哑,則格式化文本
// 因設置的文本可能是帶有樣式的文本,如SpannableStringBuilder刽宪,所以根據(jù)計算的字符數(shù)從原始文本中截取
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表示最后一段文本在可顯示范圍內(nèi)毁欣,此時需要手動加上"..."
// 如果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;
// 根據(jù)提示文本需要展示的文字拼接不同的字符
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);
}
}