UI之可折疊的TextView

先上效果

一学赛、思路

1. 計算text的行數(shù)

實現(xiàn)可折疊的TextView最重要的一點是在setText()前計算出text所需的行數(shù)
計算行數(shù)需要分為兩種情況

1.1 沒有換行符的text
    行數(shù)等于text的寬度除于TextView的寬度
    再判斷text的寬度對TextView的寬度取余是否為0吞杭,如果不等于0則加1
    lines = textWidth / TextViewWidth + textWidth % TextViewWidth == 0 ? 0 : 1
1.2 含有換行符的text
    1. 先用換行符拆分 
    2. 對于拆分后的文本
        如果不為空,則然后再按照沒有換行符的方式計算   
        如果為空芽狗,則行數(shù)為1
    3. 累加所有的拆分文本行數(shù)

2. 截取text

計算出text的行數(shù)之后,需要對text進行截取滴劲,截取到text能在指定的行數(shù)內(nèi)顯示完的位置,

  1. 首先用換行符對text進行拆分班挖,將text分為若干段落

  2. 對拆分后的文本段落循環(huán)計算行數(shù)累加,并累加字符數(shù)

  3. 累加的行數(shù)小于指定行數(shù)萧芙,繼續(xù)循環(huán)假丧,直到累加的行數(shù)大于指定行數(shù)或循環(huán)完成双揪;如果在循環(huán)完成之前累加的行數(shù)大于指定行數(shù)包帚,則截取該次循環(huán)的段落

  4. 調(diào)用TextUtils的ellipsize()方法對指定的段落進行截取,ellipsize()方法中的avail參數(shù)婴噩,傳入剩余的可顯示寬度

    因為在文本的最后要拼接上“...提示文本”羽德,所以可顯寬度的計算方式如下:

    TextViewWidth * (指定行數(shù) - 累加行數(shù)) - (... + 提示文本)Width
    
  5. 把截取后的文本設置給TextView

二、實現(xiàn)

實現(xiàn)可折疊的TextView需要繼承TextView并重寫setText(CharSequence text, BufferType type)方法

因為setText(CharSequence text)方法是final的宅静,并且setText(CharSequence text)最終調(diào)用的也是setText(CharSequence text, BufferType type)方法章蚣,所以重寫后者即可。

核心代碼

/**
 * 末尾省略號
 */
private static final String ELLIPSE = "...";
/**
 * 默認的折疊行數(shù)
 */
public static final int COLLAPSED_LINES = 4;
/**
 * 折疊時的默認文本
 */
private static final String EXPANDED_TEXT = "展開全文";
/**
 * 展開時的默認文本
 */
private static final String COLLAPSED_TEXT = "收起全文";
/**
 * 在文本末尾
 */
public static final int END = 0;
/**
 * 在文本下方
 */
public static final int BOTTOM = 1;
/**
 * 提示文字展示的位置
 */
@IntDef({END, BOTTOM})
@Retention(RetentionPolicy.SOURCE)
public @interface TipsGravityMode {}
/**
 * 折疊的行數(shù)
 */
private int mCollapsedLines;
/**
 * 折疊時的文本
 */
private String mExpandedText;
/**
 * 展開時的文本
 */
private String mCollapsedText;
/**
 * 折疊時的圖片資源
 */
private Drawable mExpandedDrawabl
/**
 * 展開時的圖片資源
 */
private Drawable mCollapsedDrawab
/**
 * 原始的文本
 */
private CharSequence mOriginalTex
/**
 * TextView中文字可顯示的寬度
 */
private int mShowWidth;
/**
 * 是否是展開的
 */
private boolean mIsExpanded;
/**
 * 提示文字位置
 */
private int mTipsGravity;
/**
 * 提示文字顏色
 */
private int mTipsColor;
/**
 * 提示文字是否顯示下劃線
 */
private boolean mTipsUnderline;
/**
 * 提示是否可點擊
 */
private boolean mTipsClickable;

... 

@Override
public void setText(CharSequence text, final BufferType type) {
    // 如果text為空或mCollapsedLines為0則直接顯示
    if (TextUtils.isEmpty(text) || mCollapsedLines == 0) {
        super.setText(text, type);
    } else if (mIsExpanded) {
        // 保存原始文本矾策,去掉文本末尾的空字符
        this.mOriginalText = CharUtil.trimFrom(text);
        formatExpandedText(type);
    } else {
        // 保存原始文本峭沦,去掉文本末尾的空字符
        this.mOriginalText = CharUtil.trimFrom(text);
        // 獲取TextView中文字顯示的寬度,需要在layout之后才能獲取到吼鱼,避免在列表中重復獲取
        if (mCollapsedLines > 0 && mShowWidth == 0) {
            getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    mShowWidth = getWidth() - getPaddingLeft() - getPaddingRight();
                    formatCollapsedText(type);
                }
            });
        } else {
            formatCollapsedText(type);
        }
    }
}

/**
 * 格式化折疊時的文本
 *
 * @param type ref android.R.styleable#TextView_bufferType
 */
private void formatCollapsedText(BufferType type) {
    // 將原始文本按換行符拆分成段落
    String[] paragraphs = mOriginalText.toString().split("\\n");
    // 獲取paint,用于計算文字寬度
    TextPaint paint = getPaint();
    // 文字寬度
    float textWidth;
    // 字符數(shù)地粪,用于最后截取字符串
    int charCount = 0;
    // 剩余行數(shù)
    int lastLines = mCollapsedLines;
    for (int i = 0; i < paragraphs.length; i++) {
        // 每個段落
        String paragraph = paragraphs[i];
        // 每個段落文本的寬度
        textWidth = paint.measureText(paragraph);
        // 計算每段的行數(shù)
        int paragraphLines = (int) (textWidth / mShowWidth);
        // 如果該段為空(表示空行)或還有余琐谤,多加一行
        if (TextUtils.isEmpty(paragraph) || textWidth % mShowWidth != 0) {
            paragraphLines++;
        }
        if (paragraphLines < lastLines) {
            // 如果該段落行數(shù)小于等于剩余的行數(shù),則減少lastLines斗忌,并增加字符數(shù)
            // 這里只計算字符數(shù),并不拼接字符
            charCount += paragraph.length() + 1;
            lastLines -= paragraphLines;
            if (i == paragraphs.length - 1) {
                super.setText(mOriginalText, type);
                break;
            }
        } else if (paragraphLines == lastLines && i == paragraphs.length - 1) {
            // 如果該段落行數(shù)等于剩余行數(shù)飞蹂,并且是最后一個段落,表示剛好能夠顯示完全
            super.setText(mOriginalText, type);
            break;
        } else {
            // 如果該段落的行數(shù)大于等于剩余的行數(shù)陈哑,則格式化文本
            // 因設置的文本可能是帶有樣式的文本,如SpannableStringBuilder刽宪,所以根據(jù)計算的字符數(shù)從原始文本中截取
            SpannableStringBuilder spannable = new SpannableStringBuilder(mOriginalText, 0, charCount);
            // 計算后綴的寬度,因樣式的問題對后綴的寬度乘2
            int expandedTextWidth = 2 * (int) (paint.measureText(ELLIPSE + mExpandedText));
            // 獲取最后一段的文本圣拄,還是因為原始文本的樣式原因不能直接使用paragraphs中的文本
            CharSequence lastParagraph = mOriginalText.subSequence(charCount, charCount + paragraph.length());
            // 對最后一段文本進行截取
            CharSequence ellipsizeText = TextUtils.ellipsize(lastParagraph, paint,
                    mShowWidth * lastLines - expandedTextWidth, TextUtils.TruncateAt.END);
            spannable.append(ellipsizeText);
            // 如果lastParagraph == ellipsizeText表示最后一段文本在可顯示范圍內(nèi)毁欣,此時需要手動加上"..."
            // 如果lastParagraph != ellipsizeText表示進行了截取TextUtils.ellipsize()方法會自動加上"..."
            if (lastParagraph == ellipsizeText) {
                spannable.append(ELLIPSE);
            }
            // 設置樣式
            setSpan(spannable);
            // 使點擊有效
            setMovementMethod(LinkMovementMethod.getInstance());
            super.setText(spannable, type);
            break;
        }
    }
}

/**
 * 格式化展開式的文本,直接在后面拼接即可
 *
 * @param type
 */
private void formatExpandedText(BufferType type) {
    SpannableStringBuilder spannable = new SpannableStringBuilder(mOriginalText);
    setSpan(spannable);
    super.setText(spannable, type);
}

/**
 * 設置提示的樣式
 *
 * @param spannable 需修改樣式的文本
 */
private void setSpan(SpannableStringBuilder spannable) {
    Drawable drawable;
    // 根據(jù)提示文本需要展示的文字拼接不同的字符
    if (mTipsGravity == END) {
        spannable.append(" ");
    } else {
        spannable.append("\n");
    }
    int tipsLen;
    // 判斷是展開還是收起
    if (mIsExpanded) {
        spannable.append(mCollapsedText);
        drawable = mCollapsedDrawable;
        tipsLen = mCollapsedText.length();
    } else {
        spannable.append(mExpandedText);
        drawable = mExpandedDrawable;
        tipsLen = mExpandedText.length();
    }
    // 設置點擊事件
    spannable.setSpan(new ExpandedClickableSpan(), spannable.length() - tipsLen,
            spannable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    // 如果提示的圖片資源不為空饭耳,則使用圖片代替提示文本
    if (drawable != null) {
        spannable.setSpan(new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE),
                spannable.length() - tipsLen, spannable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    }
}

/**
 * 提示的點擊事件
 */
private class ExpandedClickableSpan extends ClickableSpan {
    @Override
    public void onClick(View widget) {
        // 是否可點擊
        if (mTipsClickable) {
            mIsExpanded = !mIsExpanded;
            setText(mOriginalText);
        }
    }
    @Override
    public void updateDrawState(TextPaint ds) {
        // 設置提示文本的顏色和是否需要下劃線
        ds.setColor(mTipsColor == 0 ? ds.linkColor : mTipsColor);
        ds.setUnderlineText(mTipsUnderline);
    }
}
因為用戶設置給TextView的文本可能是含有樣式的文本执解,即實現(xiàn)了Spannable接口的文本寞肖,所以在拆分并拼接文本的時候不能直接使用拆分后的字符串,會丟失原有樣式新蟆,需要重新在原始文本中截取

可以從這里獲取代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市吮螺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌欣簇,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件莫鸭,死亡現(xiàn)場離奇詭異横殴,居然都是意外死亡被因,警方通過查閱死者的電腦和手機衫仑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來文狱,“玉大人,你說我怎么就攤上這事瞄崇。” “怎么了等浊?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵摹蘑,是天一觀的道長筹燕。 經(jīng)常有香客問我衅鹿,道長撒踪,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任大渤,我火速辦了婚禮,結(jié)果婚禮上兼犯,老公的妹妹穿的比我還像新娘。我一直安慰自己砸脊,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布凌埂。 她就那樣靜靜地躺著诗芜,像睡著了一般。 火紅的嫁衣襯著肌膚如雪伏恐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天翠桦,我揣著相機與錄音,去河邊找鬼销凑。 笑死,一個胖子當著我的面吹牛澎蛛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谋逻,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼渠羞,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了次询?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤屯吊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后盒卸,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡摘投,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了犀呼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡坐儿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出貌矿,到底是詐尸還是另有隱情罪佳,我是刑警寧澤逛漫,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布菇民,位于F島的核電站,受9級特大地震影響第练,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜娇掏,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望下梢。 院中可真熱鬧,春花似錦孽江、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽这刷。三九已至娩井,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間洞辣,已是汗流浹背昙衅。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工定鸟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绒尊,地道東北人仔粥。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓蟹但,卻偏偏與公主長得像,于是被迫代替她去往敵國和親华糖。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容