故事是這麼開始的,有個產品需求需求,要作一個小紅書文本摺疊的功能,因而就有了後面一系列的東西。不過實現了以後,本身對 TextView 截取文本也瞭解了很多,具體效果以下:java
先總結一下實現的時候須要注意的幾個點:android
若是概括的不完善,還請指出,不想看過程了能夠直接跳到文末查看ExpandableTextView代碼
參考了好些文章,不少實現都是截取文本的最大行,在文本的下一行添加一個按鈕,這個作法並不符合需求,因此直接能夠PASS了。轉換一下思路,會發現其實這個效果與 TextView 設置 android:maxLines 以後,再設置 android:ellipsize 爲 end 很類似,只是 … 替換換成了 …展開 ,遺憾的是系統並無提供直接替換 … 的API。可是,在涉及到 android:ellipsize 屬性處理的 TextView 的源碼中能夠看到使用了 StaticLayout 了一個能夠幫助咱們實現效果的工具類 StaticLayout,StaticLayout 是android中處理文字換行的一個工具類。有BoringLayout、StaticLayout 和 DynamicLayout 三個工具類git
接下來,就須要知道 StaticLayout 怎麼使用了,咱們能夠直接使用的構造函數有三個github
public StaticLayout(CharSequence source, TextPaint paint, int width, Alignment align, float spacingmult, float spacingadd, boolean includepad) { this(source, 0, source.length(), paint, width, align, spacingmult, spacingadd, includepad); } public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad) { this(source, bufstart, bufend, paint, outerwidth, align, spacingmult, spacingadd, includepad, null, 0); } public StaticLayout(CharSequence source, int bufstart, int bufend, TextPaint paint, int outerwidth, Alignment align, float spacingmult, float spacingadd, boolean includepad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) { this(source, bufstart, bufend, paint, outerwidth, align, TextDirectionHeuristics.FIRSTSTRONG_LTR, spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth, Integer.MAX_VALUE); }
使用以前,稍微瞭解一下方法中參數的做用,如下是比較全的參數說明面試
咱們只須要使用參數最少的那個構造方法就能知足了架構
private Layout createStaticLayout(SpannableStringBuilder spannable) { int contentWidth = initWidth - getPaddingLeft() - getPaddingRight(); return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL, getLineSpacingMultiplier(), getLineSpacingExtra(), false); }
獲取到對應文本的 StaticLayout 對象以後,能夠經過 StaticLayout 的 getLineCount() 方法知道文本是否會超出咱們設置的maxLines,配合getLineEnd(int line) 方法能夠找到最後一行的最後一個字符在文本中的位置。關鍵代碼以下:app
Layout layout = createStaticLayout(tempText); mExpandable = layout.getLineCount() > maxLines; if(mExpandable){ //計算原文截取位置 int endPos = layout.getLineEnd(maxLines - 1); mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, endPos)); SpannableStringBuilder tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING); if (mOpenSuffixSpan != null) { tempText2.append(tempText2); } //循環判斷,收起內容添加展開後綴後的內容 Layout tempLayout = createStaticLayout(tempText2); while (tempLayout.getLineCount() > maxLines) { int lastSpace = mCloseSpannableStr.length() - 1; if (lastSpace == -1) { break; } mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace)); tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING); if (mOpenSuffixSpan != null) { tempText2.append(mOpenSuffixSpan); } tempLayout = createStaticLayout(tempText2); } //計算收起的文本高度 mCLoseHeight = tempLayout.getHeight() + getPaddingTop() + getPaddingBottom(); mCloseSpannableStr.append(ELLIPSIS_STRING); if (mOpenSuffixSpan != null) { mCloseSpannableStr.append(mOpenSuffixSpan); } }
這樣一來,文本的截取問題就解決了。代碼中的mCloseSpannableStr就是被摺疊以後須要顯示的文本對象,考慮到文本中會存在表情或者圖片的可能,因此使用SpannableStringBuilder來做爲文本對象。ide
對收起文字的處理,使用SpannableString,設置new AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE)就能夠將收起文字顯示成右對齊,換行的話,須要在原始文本和收起文字之間在添加'n'就能夠了。函數
private void updateCloseSuffixSpan() { if (TextUtils.isEmpty(mCloseSuffixStr)) { mCloseSuffixSpan = null; return; } mCloseSuffixSpan = new SpannableString(mCloseSuffixStr); mCloseSuffixSpan.setSpan(new ForegroundColorSpan(mCloseSuffixColor), 0, mCloseSuffixStr.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); if (mCloseInNewLine) { AlignmentSpan alignmentSpan = new AlignmentSpan.Standard(Layout.Alignment.ALIGN_OPPOSITE); mCloseSuffixSpan.setSpan(alignmentSpan, 0, mCloseSuffixStr.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); } }
動畫效果就比較簡單了,執行動畫時在applyTransformation方法中改變TextView的高度就能夠了工具
class ExpandCollapseAnimation extends Animation { private final View mTargetView;//動畫執行view private final int mStartHeight;//動畫執行的開始高度 private final int mEndHeight;//動畫結束後的高度 ExpandCollapseAnimation(View target, int startHeight, int endHeight) { mTargetView = target; mStartHeight = startHeight; mEndHeight = endHeight; setDuration(400); } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { //計算出每次應該顯示的高度,改變執行view的高度,實現動畫 mTargetView.getLayoutParams().height = (int) ((mEndHeight - mStartHeight) * interpolatedTime + mStartHeight); mTargetView.requestLayout(); } }
而 TextView 在展開和收起狀態的高度就須要在處理文本是經過 StaticLayout 的 getHeight() 來獲取了。還有就是動畫先後 TextView 高度和文本的更新問題,具體代碼以下:
/** 執行展開動畫 */ private void executeOpenAnim() { //建立展開動畫 if (mOpenAnim == null) { mOpenAnim = new ExpandCollapseAnimation(this, mCLoseHeight, mOpenHeight); mOpenAnim.setFillAfter(true); mOpenAnim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { ExpandableTextView.super.setMaxLines(Integer.MAX_VALUE); setText(mOpenSpannableStr); } @Override public void onAnimationEnd(Animation animation) { // 動畫結束後textview設置展開的狀態 getLayoutParams().height = mOpenHeight; requestLayout(); animating = false; } @Override public void onAnimationRepeat(Animation animation) { } }); } if (animating) { return; } animating = true; clearAnimation(); // 執行動畫 startAnimation(mOpenAnim); } /** 執行收起動畫 */ private void executeCloseAnim() { //建立收起動畫 if (mCloseAnim == null) { mCloseAnim = new ExpandCollapseAnimation(this, mOpenHeight, mCLoseHeight); mCloseAnim.setFillAfter(true); mCloseAnim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { animating = false; ExpandableTextView.super.setMaxLines(mMaxLines); setText(mCloseSpannableStr); getLayoutParams().height = mCLoseHeight; requestLayout(); } @Override public void onAnimationRepeat(Animation animation) { } }); } if (animating) { return; } animating = true; clearAnimation(); // 執行動畫 startAnimation(mCloseAnim); }
方法 | 說明 |
---|---|
initWidth(int width) |
初始化ExpandableText 寬度,必須在setOriginalText() 以前調用 |
setMaxLines(int maxLines) |
設置最多顯示行數 |
setOpenSuffix(String openSuffix) |
設置須要展開時顯示的文字,默認爲展開 |
setOpenSuffixColor(@ColorInt int openSuffixColor) |
設置須要展開時顯示的文字的文字顏色 |
setCloseSuffix(String closeSuffix) |
設置須要收起時顯示的文字,默認爲收起 |
setCloseSuffixColor(@ColorInt int closeSuffixColor) |
設置須要收起時顯示的文字的文字顏色 |
setCloseInNewLine(boolean closeInNewLine) |
設置須要收起時收起文字是否另起一行 |
setOpenAndCloseCallback(OpenAndCloseCallback callback) |
設置展開&收起的點擊Callback |
setCharSequenceToSpannableHandler(CharSequenceToSpannableHandler handler) |
設置文本轉換成Spannable 的預處理回調,能夠處理特殊的文本樣式 |
添加動畫效果以後,使用setMovementMethod(LinkMovementMethod.getInstance());爲展開和收起添加點擊事件,致使收起的動畫不正確。最後發現TextView的scrollY的值在執行動畫過程被改變了,因而在屬性動畫監聽的applyTransformation方法中調用TextView的setScrollY(0)方法就能夠解決了,對於需求TextView自己不須要滑動,這麼處理暫時沒有發現什麼問題。同時還考慮到 ExpandableTextView 並不該該處理emoji表情等等一些特殊的文本形式,因此提供了CharSequenceToSpannableHandler 擴展接口,能夠自行擴展處理文本的顯示。以上就是ExpandableTextView整個的實現過程和思路,分享出來,若是有更好的方法歡迎評論中討論源碼地址:https://github.com/MrTrying/E...