前言&常用做法
效果類似微信朋友圈 - 查看全文的“展開”和“收縮”效果,這里就不貼圖了,相信大家都不會陌生。
一般情況下,第一個想到的做法是通過 TextView#setMaxLines(int maxLines)
來控制 TextView 顯示的行數。
了解 View 模型的同學都知道,在 View 沒有“呈現”之前,我們是無法獲取到當前 TextView 顯示的文字的具體行數。所以,有了下面的一種方法:
...
boolean expandable = false;
boolean expanded = false;
final TextView tv = findViewById(R.id.tv);
tv.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
tv.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int lineCount = tv.getLineCount();
if (lineCount > 3) {
tv.setMaxLines(3);
expandable = true;
}
}
});
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (expandable) {
if (!expanded) {
tv.setMaxLines(Integer.MAX_VALUE);
} else {
tv.setMaxLines(3);
}
expanded = !expanded;
}
}
});
...
這段代碼如果不在 RecyclerView 或者 ListView 中使用是沒有太大問題的。我們都知道,RV 或者 LV 是會復用 View 的,所以這段代碼有兩個問題:
- 如果某個 View 執行了 ViewTreeObserver.onGlobalLayoutListener 回調,在它再次被復用的時候,是不會再次觸發這個回調了。
- 在滾動的情況下,
TextView#getLineCount()
這個方法返回的行數,可能不是你當前看到的實際行數。
基于以上兩個原因,在 RV 或者 LV 中,通過 TextView#setMaxLines(int maxLines)
來控制的方法就行不通了。
思考
換個思路,如果我們能“測量”出一段文字顯示的行數,和每一行文字顯示需要的“高度”,那么就可以通過改變 TextView 的高度,來讓用戶看到“展開”和“收縮”文字的效果了。
那么這個工具有嗎?很慶幸,Android Api 給我們提供了一個用來測量文字大小、寬度等數據的工具,它就是:StaticLayout。
StaticLayout
引用 hencoder 一篇文章中對 StaticLayout 的介紹:
StaticLayout 的構造方法是 StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad),其中參數里:
width 是文字區域的寬度,文字到達這個寬度后就會自動換行;
align 是文字的對齊方向;
spacingmult 是行間距的倍數,通常情況下填 1 就好;
spacingadd 是行間距的額外增加值,通常情況下填 0 就好;
includeadd 是指是否在文字上下添加額外的空間,來避免某些過高的字符的繪制出現越界。
通過 StaticLayout,這里實現了一個 ExpandTextView,代碼不多,注釋非常全:
public class ExpandTextView extends android.support.v7.widget.AppCompatTextView {
/**
* true:展開,false:收起
*/
boolean mExpanded;
/**
* 狀態回調
*/
Callback mCallback;
/**
* 源文字內容
*/
String mText;
/**
* 最多展示的行數
*/
final int maxLineCount = 3;
/**
* 省略文字
*/
final String ellipsizeText = "...";
public ExpandTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 文字計算輔助工具
StaticLayout sl = new StaticLayout(mText, getPaint(), getMeasuredWidth() - getPaddingLeft() - getPaddingRight()
, Layout.Alignment.ALIGN_CENTER, 1, 0, true);
// 總計行數
int lineCount = sl.getLineCount();
if (lineCount > maxLineCount) {
if (mExpanded) {
setText(mText);
mCallback.onExpand();
} else {
lineCount = maxLineCount;
// 省略文字的寬度
float dotWidth = getPaint().measureText(ellipsizeText);
// 找出第 showLineCount 行的文字
int start = sl.getLineStart(lineCount - 1);
int end = sl.getLineEnd(lineCount - 1);
String lineText = mText.substring(start, end);
// 將第 showLineCount 行最后的文字替換為 ellipsizeText
int endIndex = 0;
for (int i = lineText.length() - 1; i >= 0; i--) {
String str = lineText.substring(i, lineText.length());
// 找出文字寬度大于 ellipsizeText 的字符
if (getPaint().measureText(str) >= dotWidth) {
endIndex = i;
break;
}
}
// 新的第 showLineCount 的文字
String newEndLineText = lineText.substring(0, endIndex) + ellipsizeText;
// 最終顯示的文字
setText(mText.substring(0, start) + newEndLineText);
mCallback.onCollapse();
}
} else {
setText(mText);
mCallback.onLoss();
}
// 重新計算高度
int lineHeight = 0;
for (int i = 0; i < lineCount; i++) {
Rect lineBound = new Rect();
sl.getLineBounds(i, lineBound);
lineHeight += lineBound.height();
}
lineHeight += getPaddingTop() + getPaddingBottom();
setMeasuredDimension(getMeasuredWidth(), lineHeight);
}
/**
* 設置要顯示的文字以及狀態
* @param text
* @param expanded true:展開,false:收起
* @param callback
*/
public void setText(String text, boolean expanded, Callback callback) {
mText = text;
mExpanded = expanded;
mCallback = callback;
// 設置要顯示的文字,這一行必須要,否則 onMeasure 寬度測量不正確
setText(text);
}
/**
* 展開收起狀態變化
* @param expanded
*/
public void setChanged(boolean expanded) {
mExpanded = expanded;
requestLayout();
}
public interface Callback {
/**
* 展開狀態
*/
void onExpand();
/**
* 收起狀態
*/
void onCollapse();
/**
* 行數小于最小行數,不滿足展開或者收起條件
*/
void onLoss();
}
}
在 RecyclerView 中使用:
...
public void onBindViewHolder(VH holder, int position) {
....
/**
* item.getText(): 顯示的文本
* item.isExpanded(): 保存的是當前行是否是展開狀態
*/
tvContent.setText(item.getText(), item.isExpanded(), new ExpandTextView.Callback() {
@Override
public void onExpand() {
// 展開狀態,比如:顯示“收起”按鈕
}
@Override
public void onCollapse() {
// 收縮狀態,比如:顯示“全文”按鈕
}
@Override
public void onLoss() {
// 不滿足展開的條件,比如:隱藏“全文”按鈕
}
});
}
tvContent.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 保存當前行的狀態
item.setExpanded(!item.setExpanded());
// 切換狀態
tvContent.setChanged(item.isExpanded());
}
});
}
...