Android尾部帶“檢視更多”的TextView
阿新 • • 發佈:2019-01-25
http://blog.csdn.net/tanxuewe/article/details/50793630
宣告一下,最開始是從這篇文章看到的,拿過來後呢,做了一些改進。
首先是增添了幾個屬性;其次,也是最重要的,改進了呼叫setText()重新設定文字時,其下方的View會發生抖動的問題,也就是onMeasure()中的那段程式碼。
FolderTextView.java
package com.xiaobo.foldertextview; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.text.Layout; import android.text.SpannableString; import android.text.Spanned; import android.text.StaticLayout; import android.text.TextPaint; import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.TextView; /** * 結尾帶“檢視全部”的TextView,點選可以展開文字,展開後可收起。 * <p/> * 目前存在一個問題:外部呼叫setText()時會造成介面該TextView下方的View抖動; * <p/> * 可以先呼叫getFullText(),當已有文字和要設定的文字不一樣才呼叫setText(),可降低抖動的次數; * <p/> * 通過在onMeasure()中設定高度已經修復了該問題了。 * <p/> * Created by moxiaobo on 16/8/9. */ public class FolderTextView extends TextView { // TAG private static final String TAG = "xiaobo"; // 預設打點文字 private static final String DEFAULT_ELLIPSIZE = "..."; // 預設收起文字 private static final String DEFAULT_FOLD_TEXT = "[收起]"; // 預設展開文字 private static final String DEFAULT_UNFOLD_TEXT = "[檢視全部]"; // 預設固定行數 private static final int DEFAULT_FOLD_LINE = 2; // 預設收起和展開文字顏色 private static final int DEFAULT_TAIL_TEXT_COLOR = Color.GRAY; // 預設是否可以再次收起 private static final boolean DEFAULT_CAN_FOLD_AGAIN = true; // 收起文字 private String mFoldText; // 展開文字 private String mUnFoldText; // 固定行數 private int mFoldLine; // 尾部文字顏色 private int mTailColor; // 是否可以再次收起 private boolean mCanFoldAgain = false; // 收縮狀態 private boolean mIsFold = false; // 繪製,防止重複進行繪製 private boolean mHasDrawn = false; // 內部繪製 private boolean mIsInner = false; // 全文字 private String mFullText; // 行間距倍數 private float mLineSpacingMultiplier = 1.0f; // 行間距額外畫素 private float mLineSpacingExtra = 0.0f; // 統計使用二分法裁剪源文字的次數 private int mCountBinary = 0; // 統計使用備用方法裁剪源文字的次數 private int mCountBackUp = 0; // 統計onDraw呼叫的次數 private int mCountOnDraw = 0; // 點選處理 private ClickableSpan clickSpan = new ClickableSpan() { @Override public void onClick(View widget) { mIsFold = !mIsFold; mHasDrawn = false; invalidate(); } @Override public void updateDrawState(TextPaint ds) { ds.setColor(mTailColor); } }; /** * 構造 * * @param context 上下文 */ public FolderTextView(Context context) { this(context, null); } /** * 構造 * * @param context 上下文 * @param attrs 屬性 */ public FolderTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } /** * 構造 * * @param context 上下文 * @param attrs 屬性 * @param defStyleAttr 樣式 */ public FolderTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FolderTextView); mFoldText = a.getString(R.styleable.FolderTextView_foldText); if (null == mFoldText) { mFoldText = DEFAULT_FOLD_TEXT; } mUnFoldText = a.getString(R.styleable.FolderTextView_unFoldText); if (null == mUnFoldText) { mUnFoldText = DEFAULT_UNFOLD_TEXT; } mFoldLine = a.getInt(R.styleable.FolderTextView_foldLine, DEFAULT_FOLD_LINE); if (mFoldLine < 1) { throw new RuntimeException("foldLine must not less than 1"); } mTailColor = a.getColor(R.styleable.FolderTextView_tailTextColor, DEFAULT_TAIL_TEXT_COLOR); mCanFoldAgain = a.getBoolean(R.styleable.FolderTextView_canFoldAgain, DEFAULT_CAN_FOLD_AGAIN); a.recycle(); } @Override public void setText(CharSequence text, BufferType type) { if (TextUtils.isEmpty(mFullText) || !mIsInner) { mHasDrawn = false; mFullText = String.valueOf(text); } super.setText(text, type); } @Override public void setLineSpacing(float extra, float multiplier) { mLineSpacingExtra = extra; mLineSpacingMultiplier = multiplier; super.setLineSpacing(extra, multiplier); } @Override public void invalidate() { super.invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // 必須解釋下:由於為了得到實際一行的寬度(makeTextLayout()中需要使用),必須要先把源文字設定上,然後再裁剪至指定行數; // 這就導致了該TextView會先佈局一次高度很高(源文字行數高度)的佈局,裁剪後再次佈局成指定行數高度,因而下方View會抖動; // 這裡的處理是,super.onMeasure()已經計算出了源文字的實際寬高了,取出指定行數的文字再次測量一下其高度, // 然後把這個高度設定成最終的高度就行了! if (!mIsFold) { Layout layout = getLayout(); int line = getFoldLine(); if (line < layout.getLineCount()) { int index = layout.getLineEnd(line - 1); if (index > 0) { // 得到一個字串,該字串恰好佔據mFoldLine行數的高度 String strWhichHasExactlyFoldLine = getText().subSequence(0, index).toString(); Log.d(TAG, "strWhichHasExactlyFoldLine-->" + strWhichHasExactlyFoldLine); layout = makeTextLayout(strWhichHasExactlyFoldLine); // 把這個高度設定成最終的高度,這樣下方View就不會抖動了 setMeasuredDimension(getMeasuredWidth(), layout.getHeight() + getPaddingTop() + getPaddingBottom()); } } } } @Override protected void onDraw(Canvas canvas) { Log.d(TAG, "onDraw() " + mCountOnDraw++ + ", getMeasuredHeight() " + getMeasuredHeight()); if (!mHasDrawn) { resetText(); } super.onDraw(canvas); mHasDrawn = true; mIsInner = false; } /** * 獲取摺疊文字 * * @return 摺疊文字 */ public String getFoldText() { return mFoldText; } /** * 設定摺疊文字 * * @param foldText 摺疊文字 */ public void setFoldText(String foldText) { mFoldText = foldText; invalidate(); } /** * 獲取展開文字 * * @return 展開文字 */ public String getUnFoldText() { return mUnFoldText; } /** * 設定展開文字 * * @param unFoldText 展開文字 */ public void setUnFoldText(String unFoldText) { mUnFoldText = unFoldText; invalidate(); } /** * 獲取摺疊行數 * * @return 摺疊行數 */ public int getFoldLine() { return mFoldLine; } /** * 設定摺疊行數 * * @param foldLine 摺疊行數 */ public void setFoldLine(int foldLine) { mFoldLine = foldLine; invalidate(); } /** * 獲取尾部文字顏色 * * @return 尾部文字顏色 */ public int getTailColor() { return mTailColor; } /** * 設定尾部文字顏色 * * @param tailColor 尾部文字顏色 */ public void setTailColor(int tailColor) { mTailColor = tailColor; invalidate(); } /** * 獲取是否可以再次摺疊 * * @return 是否可以再次摺疊 */ public boolean isCanFoldAgain() { return mCanFoldAgain; } /** * 獲取全文字 * * @return 全文字 */ public String getFullText() { return mFullText; } /** * 設定是否可以再次摺疊 * * @param canFoldAgain 是否可以再次摺疊 */ public void setCanFoldAgain(boolean canFoldAgain) { mCanFoldAgain = canFoldAgain; invalidate(); } /** * 獲取TextView的Layout,注意這裡使用getWidth()得到寬度 * * @param text 源文字 * @return Layout */ private Layout makeTextLayout(String text) { return new StaticLayout(text, getPaint(), getWidth() - getPaddingLeft() - getPaddingRight(), Layout.Alignment .ALIGN_NORMAL, mLineSpacingMultiplier, mLineSpacingExtra, true); } /** * 重置文字 */ private void resetText() { // 文字本身就小於固定行數的話,不新增尾部的收起/展開文字 Layout layout = makeTextLayout(mFullText); if (layout.getLineCount() <= getFoldLine()) { setText(mFullText); return; } SpannableString spanStr = new SpannableString(mFullText); if (mIsFold) { // 收縮狀態 if (mCanFoldAgain) { spanStr = createUnFoldSpan(mFullText); } } else { // 展開狀態 spanStr = createFoldSpan(mFullText); } updateText(spanStr); setMovementMethod(LinkMovementMethod.getInstance()); } /** * 不更新全文字下,進行展開和收縮操作 * * @param text 源文字 */ private void updateText(CharSequence text) { mIsInner = true; setText(text); } /** * 建立展開狀態下的Span * * @param text 源文字 * @return 展開狀態下的Span */ private SpannableString createUnFoldSpan(String text) { String destStr = text + mFoldText; int start = destStr.length() - mFoldText.length(); int end = destStr.length(); SpannableString spanStr = new SpannableString(destStr); spanStr.setSpan(clickSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return spanStr; } /** * 建立收縮狀態下的Span * * @param text * @return 收縮狀態下的Span */ private SpannableString createFoldSpan(String text) { long startTime = System.currentTimeMillis(); String destStr = tailorText(text); Log.d(TAG, (System.currentTimeMillis() - startTime) + "ms"); int start = destStr.length() - mUnFoldText.length(); int end = destStr.length(); SpannableString spanStr = new SpannableString(destStr); spanStr.setSpan(clickSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); return spanStr; } /** * 裁剪文字至固定行數(備用方法) * * @param text 源文字 * @return 裁剪後的文字 */ private String tailorTextBackUp(String text) { Log.d(TAG, "使用備用方法: tailorTextBackUp() " + mCountBackUp++); String destStr = text + DEFAULT_ELLIPSIZE + mUnFoldText; Layout layout = makeTextLayout(destStr); // 如果行數大於固定行數 if (layout.getLineCount() > getFoldLine()) { int index = layout.getLineEnd(getFoldLine() - 1); if (text.length() < index) { index = text.length(); } // 從最後一位逐漸試錯至固定行數(可以考慮用二分法改進) if (index <= 1) { return DEFAULT_ELLIPSIZE + mUnFoldText; } String subText = text.substring(0, index - 1); return tailorText(subText); } else { return destStr; } } /** * 裁剪文字至固定行數(二分法)。經試驗,在文字長度不是很長時,效率比備用方法高不少;當文字長度過長時,備用方法則優勢明顯。 * * @param text 源文字 * @return 裁剪後的文字 */ private String tailorText(String text) { // return tailorTextBackUp(text); int start = 0; int end = text.length() - 1; int mid = (start + end) / 2; int find = finPos(text, mid); while (find != 0 && end > start) { Log.d(TAG, "使用二分法: tailorText() " + mCountBinary++); if (find > 0) { end = mid - 1; } else if (find < 0) { start = mid + 1; } mid = (start + end) / 2; find = finPos(text, mid); } Log.d(TAG, "mid is: " + mid); String ret; if (find == 0) { ret = text.substring(0, mid) + DEFAULT_ELLIPSIZE + mUnFoldText; } else { ret = tailorTextBackUp(text); } return ret; } /** * 查詢一個位置P,到P時為mFoldLine這麼多行,加上一個字元‘A’後則剛好為mFoldLine+1這麼多行 * * @param text 源文字 * @param pos 位置 * @return 查詢結果 */ private int finPos(String text, int pos) { String destStr = text.substring(0, pos) + DEFAULT_ELLIPSIZE + mUnFoldText; Layout layout = makeTextLayout(destStr); Layout layoutMore = makeTextLayout(destStr + "A"); int lineCount = layout.getLineCount(); int lineCountMore = layoutMore.getLineCount(); if (lineCount == getFoldLine() && (lineCountMore == getFoldLine() + 1)) { // 行數剛好到摺疊行數 return 0; } else if (lineCount > getFoldLine()) { // 行數比摺疊行數多 return 1; } else { // 行數比摺疊行數少 return -1; } } }
attrs.xml
通過螢幕錄製可以看出,改進前的抖動是這樣的:<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="FolderTextView"> <attr name="foldText" format="string"/> <attr name="unFoldText" format="string"/> <attr name="foldLine" format="integer"/> <attr name="tailTextColor" format="color"/> <attr name="canFoldAgain" format="boolean"/> </declare-styleable> </resources>
上一幀
下一幀
最後一幀
可看到,上方的TextView先是佔據了一個很高的高度,然後才會恢復,也就造成了下方View的抖動,修復後無此問題。