前言&常用做法
效果類似微信朋友圈 - 查看全文的“展開”和“收縮”效果锋玲,這里就不貼圖了竟宋,相信大家都不會陌生。
一般情況下,第一個想到的做法是通過 TextView#setMaxLines(int maxLines)
來控制 TextView 顯示的行數(shù)妖滔。
了解 View 模型的同學(xué)都知道,在 View 沒有“呈現(xiàn)”之前接校,我們是無法獲取到當(dāng)前 TextView 顯示的文字的具體行數(shù)袁铐。所以杨伙,有了下面的一種方法:
...
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 是會復(fù)用 View 的,所以這段代碼有兩個問題:
- 如果某個 View 執(zhí)行了 ViewTreeObserver.onGlobalLayoutListener 回調(diào)岳掐,在它再次被復(fù)用的時候凭疮,是不會再次觸發(fā)這個回調(diào)了饭耳。
- 在滾動的情況下串述,
TextView#getLineCount()
這個方法返回的行數(shù),可能不是你當(dāng)前看到的實際行數(shù)寞肖。
基于以上兩個原因纲酗,在 RV 或者 LV 中,通過 TextView#setMaxLines(int maxLines)
來控制的方法就行不通了新蟆。
思考
換個思路觅赊,如果我們能“測量”出一段文字顯示的行數(shù),和每一行文字顯示需要的“高度”琼稻,那么就可以通過改變 TextView 的高度吮螺,來讓用戶看到“展開”和“收縮”文字的效果了。
那么這個工具有嗎帕翻?很慶幸鸠补,Android Api 給我們提供了一個用來測量文字大小、寬度等數(shù)據(jù)的工具嘀掸,它就是:StaticLayout紫岩。
StaticLayout
引用 hencoder 一篇文章中對 StaticLayout 的介紹:
StaticLayout 的構(gòu)造方法是 StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad),其中參數(shù)里:
width 是文字區(qū)域的寬度睬塌,文字到達這個寬度后就會自動換行泉蝌;
align 是文字的對齊方向;
spacingmult 是行間距的倍數(shù)揩晴,通常情況下填 1 就好勋陪;
spacingadd 是行間距的額外增加值,通常情況下填 0 就好硫兰;
includeadd 是指是否在文字上下添加額外的空間粥鞋,來避免某些過高的字符的繪制出現(xiàn)越界。
通過 StaticLayout瞄崇,這里實現(xiàn)了一個 ExpandTextView呻粹,代碼不多壕曼,注釋非常全:
public class ExpandTextView extends android.support.v7.widget.AppCompatTextView {
/**
* true:展開,false:收起
*/
boolean mExpanded;
/**
* 狀態(tài)回調(diào)
*/
Callback mCallback;
/**
* 源文字內(nèi)容
*/
String mText;
/**
* 最多展示的行數(shù)
*/
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);
// 總計行數(shù)
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);
}
/**
* 設(shè)置要顯示的文字以及狀態(tài)
* @param text
* @param expanded true:展開等浊,false:收起
* @param callback
*/
public void setText(String text, boolean expanded, Callback callback) {
mText = text;
mExpanded = expanded;
mCallback = callback;
// 設(shè)置要顯示的文字腮郊,這一行必須要,否則 onMeasure 寬度測量不正確
setText(text);
}
/**
* 展開收起狀態(tài)變化
* @param expanded
*/
public void setChanged(boolean expanded) {
mExpanded = expanded;
requestLayout();
}
public interface Callback {
/**
* 展開狀態(tài)
*/
void onExpand();
/**
* 收起狀態(tài)
*/
void onCollapse();
/**
* 行數(shù)小于最小行數(shù)筹燕,不滿足展開或者收起條件
*/
void onLoss();
}
}
在 RecyclerView 中使用:
...
public void onBindViewHolder(VH holder, int position) {
....
/**
* item.getText(): 顯示的文本
* item.isExpanded(): 保存的是當(dāng)前行是否是展開狀態(tài)
*/
tvContent.setText(item.getText(), item.isExpanded(), new ExpandTextView.Callback() {
@Override
public void onExpand() {
// 展開狀態(tài)轧飞,比如:顯示“收起”按鈕
}
@Override
public void onCollapse() {
// 收縮狀態(tài),比如:顯示“全文”按鈕
}
@Override
public void onLoss() {
// 不滿足展開的條件撒踪,比如:隱藏“全文”按鈕
}
});
}
tvContent.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 保存當(dāng)前行的狀態(tài)
item.setExpanded(!item.setExpanded());
// 切換狀態(tài)
tvContent.setChanged(item.isExpanded());
}
});
}
...