前言
項(xiàng)目中個(gè)人負(fù)責(zé)的多個(gè)列表頁用到類似微博及小紅書如下圖的這種超過縮進(jìn)行數(shù)文末添加" ...全文" 展開的控件熔脂。在頁面的優(yōu)化同城中逸吵,通過systrace跟蹤發(fā)現(xiàn)項(xiàng)目中該自定義控件有個(gè)方法會反復(fù)調(diào)用多次(具體以下詳解) 峦萎,本以為這種控件應(yīng)該是有比較成熟的解決方案,于是github一頓搜索,唯一一個(gè)星數(shù)上千的庫就是ExpandableTextView,查看后實(shí)現(xiàn)原理是:2個(gè)控件不是span的形式添加到textview尾部,然后獲取4行時(shí)textview需顯示的高度倒彰,按鈕點(diǎn)下時(shí)動畫控制view的height屬性;且并無文末添加“...全文”的功能莱睁,與需求不符待讳。無奈只能自己動手,優(yōu)化現(xiàn)有控件仰剿。
思路及原理
- 發(fā)現(xiàn)其實(shí)這個(gè)效果與 TextView 設(shè)置 android:maxLines 之后创淡,再設(shè)置 android:ellipsize 為 end 很相似,只是 … 替換換成了 …展開 南吮,遺憾的是系統(tǒng)并沒有提供直接替換 … 的API琳彩。
但是,在涉及到 android:ellipsize 屬性處理的 TextView 的源碼中可以看到使用了 StaticLayout 了一個(gè)可以幫助我們實(shí)現(xiàn)效果的工具類 StaticLayout,StaticLayout 是android中處理文字換行的一個(gè)工具類露乏。
有BoringLayout碧浊、StaticLayout 和 DynamicLayout 三個(gè)工具類
- BoringLayout 是單行顯示時(shí)使用的
- StaticLayout 是針對不可以變的文本(不同系統(tǒng)版本構(gòu)造方式不大一樣,)
- DynamicLayout 則是針對可編輯改變的文本瘟仿,并且會更新自身箱锐。
具體這些類及方法本文不詳細(xì)展開,有興趣的同學(xué)可自行查看相關(guān)文檔
于是得出最終方案
2:動態(tài)截取文字劳较,加上“...全文”后剛好撐滿縮進(jìn)驹止,然后將新的CharSequence設(shè)入textview即可。
細(xì)節(jié)及注意點(diǎn)
- 一定要先測量一次如果本身文字行數(shù)就不會超過鎖進(jìn)行數(shù)观蜗,則什么都不要再做任何處理臊恋,浪費(fèi)性能,直接走textview的方法即可(需求上也是如此)
- 記錄一份原數(shù)據(jù)墓捻,如果有些地方是顯示的是"...展開"抖仅,點(diǎn)擊后的效果是直接展開顯示全文的話
- 新文字設(shè)置進(jìn)來時(shí),對比下當(dāng)前記錄的原數(shù)據(jù)與設(shè)入的新數(shù)據(jù)是否一致毙替,一致則不再做多余處理岸售,算是性能的優(yōu)化践樱。還取決于控件實(shí)現(xiàn)方式厂画,像原項(xiàng)目中控件的處理,是在onDraw方法(當(dāng)然做了其它限制拷邢,保證每次更改文字只觸發(fā)一次袱院,不能每次ondraw都去觸發(fā),否則性能就廢了)時(shí)才取攔截瞭稼,因?yàn)閛nDraw方法已經(jīng)在measure和layout方法之后忽洛,如果不做該操作當(dāng)控件放在listview或recyclerview列表中,當(dāng)notify或者滑動操作時(shí)环肘,會造成先高度測量差異而抖動一下欲虚。
項(xiàng)目中多次調(diào)用的方法優(yōu)化
有了以上思路后,根據(jù)systrac顯示悔雹,多次調(diào)用耗時(shí)复哆,跟蹤項(xiàng)目中調(diào)用多次的方法,發(fā)現(xiàn)他是一個(gè)循環(huán)腌零,一直去嘗試截取原文中的不同長度的文字去與“...全文”拼成后剛好布滿指定縮進(jìn)行數(shù)的文字梯找。
一開始看到此處一臉懵逼,為啥要一直循環(huán)遍歷去嘗試益涧,而不是直接先通過StaticLayout.getLineEnd方法锈锤,直接獲取縮進(jìn)行數(shù)的末尾offset,然后截取原文字,再對這個(gè)截取后的文字久免,刪減“...全文”的長度浅辙,最后再將這個(gè)刪減后的文字拼接上"...全文",不就是我們想要的最終結(jié)果妄壶,不就可以了
大概如下
...
int lineEnd = getLayout().getLineEnd(mCollapsedLines - 1);
CharSequence suffix = "...全文";
int newEnd = lineEnd - suffix.length() - 1;
int end = newEnd > 0 ? newEnd : lineEnd;
CharSequence finalSequence = note.subSequence(0, end);
...
然而摔握,實(shí)際結(jié)果令人啪啪打臉[捂臉哭],會有的還有間距丁寄,有的超過氨淌。因?yàn)槁┝艘粋€(gè)重要因素,就是同樣length的文字伊磺,在繪制時(shí)所占用的寬度不一定一致盛正。
各種搜索網(wǎng)上其它大佬的解決方案,也都是只能遍歷一直去嘗試屑埋,看截到多少能剛好鋪滿豪筝。
如
1:
TextPaint paint = getPaint();
int maxWidth = mCollapsedLines * (getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
while (paint.measureText(note.substring(0, end) + suffix) > maxWidth)
end--;
note = note.substring(0, end);
而且這代碼還有個(gè)問題就是忽律了各種span長度問題
2:ExpandableText-Example
//計(jì)算原文截取位置
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);
}
//循環(huán)判斷,收起內(nèi)容添加展開后綴后的內(nèi)容
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);
}
還有其它多個(gè)庫摘能,不一一鏈接续崖,區(qū)別在于如何去測量,如果去逼近求出最終字符串而已团搞。所以現(xiàn)在能優(yōu)化的重點(diǎn)就在于严望,如何盡量地去減少遍歷的次數(shù)。
上方庫的方法比較簡單逻恐,也與項(xiàng)目中用到的方法類似像吻。即:通過StaticLayout.getLineEnd方法,直接獲取縮進(jìn)行數(shù)的末尾offset复隆,然后截取原文字拨匆,直接拼接上"...全文"span,然后依次往前遞減字符去逼近挽拂。
項(xiàng)目中的是直接全字段二分查找去逼近惭每,以上開源庫方法做為備用方案。經(jīng)試驗(yàn)亏栈,在文字長度不是很長時(shí)台腥,效率比備用方法高不少;當(dāng)文字長度過長時(shí)仑扑,備用方法則優(yōu)勢明顯览爵。
其實(shí)還可以進(jìn)一部優(yōu)化,即二分查找法的起始位置不要全字串二分镇饮,從 截取后的文字蜓竹,刪減“...全文”的長度,開始到最后拼接上的 這個(gè)小范圍去二分查找。
優(yōu)化后打log方法及效果如下:
//優(yōu)化前方式
CharSequence destStr = tailorText(text,false);
long newEnd = System.currentTimeMillis();
//優(yōu)化后方式
CharSequence destStrNewMethod = tailorText(text,true);
long newEnd2 = System.currentTimeMillis();
Log.d(TAG, ("oldMethod--->"+(newEnd - startTime))+"|NewMethod="+(newEnd2-newEnd) + "ms");
這幾毫秒的時(shí)間在一個(gè)布局中并無關(guān)緊要俱济,但因?yàn)轫?xiàng)目中是放在listview及recyclerview中使用嘶是,一次滑動及來回操作便會調(diào)用反復(fù)調(diào)用多次,積累起來便很可觀蛛碌。
尾言
- 關(guān)于源碼聂喇,由于本次只是做一些優(yōu)化思路,具體控件有很多舊的冗余代碼蔚携,未做清理希太,故不附上該控件的全部源碼