編輯:關於Android編程
首先是增添了幾個屬性;其次,也是最重要的,改進了調用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
通過屏幕錄制可以看出,改進前的抖動是這樣的:
上一幀
下一幀
最後一幀
可看到,上方的TextView先是占據了一個很高的高度,然後才會恢復,也就造成了下方View的抖動,修復後無此問題。
當你建立Unity 的手機游戲你最可能渴望設置Script Call Optimization為Fast But No Exceptions,只要你相信你能做到。Fast
在Part 4我們回顧了一下Fragment的基本概念,在本節中我們就來學習Fragment應用的簡單例子吧! 就是使用Fragment來實現
上一篇博客中我們已經繪制出了一個直角三角形,雖然我們相對於坐標,我們設置的直角三角形的兩腰是相等的,但是實際上展示出來的卻並不是這樣,雖然通過計算,我們可以把三角形的兩腰
最近Google在自己推出的Material design中增加了Bottom Navigation導航控制。Android一直沒有官方的導航控制器,自己實現確實是五花八