Android自定義View--ExpandLayout:TextView行末添加點擊展開更多或收縮文本的佈局,支持查看全部和收起功能

一、ExpandLayout

ExpandLayout是支持在TextView行末添加點擊展開更多或收縮文本的佈局,支持點擊查看全部和收起功能,同時提供了大量自定義屬性以支持更多的個性化屬性配置,效果展示GIF如下,由於錄製工具的問題,視頻轉換爲GIF結束後會顯示一段黑屏,大家勿噴,忽略就好~

二、前言

在接到相關需求時,也在網上參考了一些TextView點擊展開更多/收縮文本的例子,大多都是在TextView文本最後一行接着顯示展開或收縮文本或圖片的實現方案,大多是通過SpannableString或子類直接拼接操作即可,很少有在文本所在佈局右底下顯示點擊展開更多/收縮佈局的實現,而且部分沒有處理好文本分行,存在英文單詞整體換行到下一行顯示而導致重疊的問題,所以本人決定直接擼了一個,也在項目中的實踐中不斷的優化和完善,並整理成文,分享出來,希望對大家有參考和學習作用。

三、支持特性

ExpandLayout支持如下特性:

1、文本行末更多佈局支持圖標+文字、圖標、文字三種樣式,默認是圖標+文字樣式;

2、支持配置展開和收縮提示圖標,展開和收縮的顯示的提示文字;

3、支持配置內容顯示文本、行末展開/收縮提示文字的字體大小、字體顏色、行間距等;

4、支持設置縮略文本展示時與展開/收縮佈局的間距;

5、支持收縮和展示狀態的回調監聽;

6、支持設置縮略文本展示的最大行數,並處理存在換行符時的特殊情況

 

四、實現原理及思路

實現思路和步驟:

1)自定義樣式佈局和屬性,以滿足在TextView行末顯示展開和收縮提示佈局

2)繼承RelativeLayout獲取相應控件和以及自定義屬性,完成屬性和佈局控件初始化

3)根據設置的文本,獲取控件寬度,根據配置的自定義屬性,計算並截取要顯示的縮略顯示的文本

上述步驟中,個人感覺難點在於如下三點:

1)如何在TextView顯示文本前,獲取文本展示的行數是否超過了設置的最大縮略展示行數

2)如何在TextView顯示文本前,根據要顯示的文本獲取要最後一行文字的長度和字符下標

3)如何保證展開和收縮提示佈局的圖標與文字與顯示內容最後一行文字保持居中顯示,而且最後一行文字與展開/收縮提示佈局不重疊

下面根據這些步驟及問題進行一一分析和處理

1、聲明佈局,滿足在TextView行末顯示展開和收縮提示佈局,xml佈局如下:

<?xml version="1.0" encoding="utf-8"?>

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/expand_root_fl"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <LinearLayout
        android:id="@+id/expand_ll"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|right"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <ImageView
            android:id="@+id/expand_iv"
            android:layout_width="15dp"
            android:layout_height="15dp"
            android:scaleType="fitXY"
            android:src="@drawable/splitter_more"
            android:visibility="visible" />

        <TextView
            android:id="@+id/expand_tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="展開"
            android:textColor="#333333"
            android:textSize="14sp"
            android:visibility="visible" />

        <TextView
            android:id="@+id/expand_helper_tv"
            android:layout_width="0.5dp"
            android:layout_height="wrap_content"
            android:text=""
            android:textColor="#333333"
            android:textSize="14sp"
            android:visibility="invisible" />

    </LinearLayout>

    <TextView
        android:id="@+id/expand_content_tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        android:textColor="#333333"
        android:textSize="14sp" />

</FrameLayout>

在佈局中,整體採用FrameLayout,其中顯示的文本expand_content_tv填充佔滿整個佈局,可展開/收縮提示佈局expand_ll在FrameLayout右下角展示,可能在這裏很多朋友有疑問,這樣子顯示的最後一行文字與展開/收縮提示佈局不就是重疊在一起了嗎?在預覽顯示的效果,文本與展開收縮提示佈局是存在重疊的,需要在代碼中進行文本截取,下面也會進行講解,預覽結果如下:

在這裏,爲了保證展開和收縮提示佈局的圖標與文字與顯示內容最後一行文字保持居中顯示(也是上面提到的第3個問題裏面),在佈局中添加了可見狀態爲invisible的expand_helper_tv輔助TextView,其屬性設置設置保持與內容TextView一致,同時設置輔助TextView與展示/收縮圖標和文字三者保持居中顯示,即可實現展開和收縮提示佈局的圖標與文字與顯示內容最後一行文字保持居中顯示。

2、自定義屬性

在attrs.xml通過declare-styleable根標籤定義可以配置的自定義屬性,每個屬性的含義請見註釋

<!--可展開佈局自定義屬性-->
    <declare-styleable name="ExpandLayout">
        <!--文本最大縮略行數,對應於TextView的maxLines-->
        <attr name="maxLines" format="integer" />
        <!--展開時對應的圖片-->
        <attr name="expandIconResId" format="reference" />
        <!--不展開時對應的圖片-->
        <attr name="collapseIconResId" format="reference" />
        <!--展開時對應末尾提示的文字-->
        <attr name="expandMoreText" format="string" />
        <!--不展開時對應末尾提示的文字-->
        <attr name="collapseLessText" format="string" />
        <!--內容文字的顏色-->
        <attr name="contentTextColor" format="reference|color" />
        <!--更多或收起文字的顏色-->
        <attr name="expandTextColor" format="reference|color" />
        <!--文字大小-->
        <attr name="contentTextSize" format="dimension" />
        <!--更多/收起文字大小-->
        <attr name="expandTextSize" format="dimension" />
        <!--展開/收縮佈局對應圖標的寬度-->
        <attr name="expandIconWidth" format="dimension" />
        <!--縮略文本展示時與展開/收縮佈局的間距-->
        <attr name="spaceMargin" format="dimension" />
        <!--文本顯示的lineSpacingExtra,對應於TextView的lineSpacingExtra屬性-->
        <attr name="lineSpacingExtra" format="dimension" />
        <!--文本顯示的lineSpacingMultiplier,對應於TextView的lineSpacingMultiplier屬性-->
        <attr name="lineSpacingMultiplier" format="float" />
        <!--展開樣式-->
        <attr name="expandStyle" format="integer">
            <!--默認樣式:圖標+文字-->
            <enum name="DEFAULT" value="0" />
            <enum name="ICON" value="1" />
            <enum name="TEXT" value="2" />
        </attr>
    </declare-styleable>

3、繼承RelativeLayout獲取相應控件和以及自定義屬性,完成屬性和佈局控件初始化

public class ExpandLayout extends RelativeLayout implements View.OnClickListener {
    
    public ExpandLayout(Context context) {
        this(context, null);
    }

    public ExpandLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ExpandLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initAttributeSet(context, attrs);
        initView();
    }

    /**
     * 初始化自定義屬性
     * @param context
     * @param attrs
     */
    private void initAttributeSet(Context context, AttributeSet attrs){
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ExpandLayout);
        if (ta != null) {
            mMaxLines = ta.getInt(R.styleable.ExpandLayout_maxLines, 2);
            mExpandIconResId = ta.getResourceId(R.styleable.ExpandLayout_expandIconResId, 0);
            mCollapseIconResId = ta.getResourceId(R.styleable.ExpandLayout_collapseIconResId, 0);
            mExpandMoreStr = ta.getString(R.styleable.ExpandLayout_expandMoreText);
            mCollapseLessStr = ta.getString(R.styleable.ExpandLayout_collapseLessText);
            mContentTextSize = ta.getDimensionPixelSize(R.styleable.ExpandLayout_contentTextSize, sp2px(context, 14));
            mContentTextColor = ta.getColor(R.styleable.ExpandLayout_contentTextColor, 0);
            mExpandTextSize = ta.getDimensionPixelSize(R.styleable.ExpandLayout_expandTextSize, sp2px(context, 14));
            mExpandTextColor = ta.getColor(R.styleable.ExpandLayout_expandTextColor, 0);
            mExpandStyle = ta.getInt(R.styleable.ExpandLayout_expandStyle, STYLE_DEFAULT);
            mExpandIconWidth = ta.getDimensionPixelSize(R.styleable.ExpandLayout_expandIconWidth, dp2px(context, 15));
            mSpaceMargin = ta.getDimensionPixelSize(R.styleable.ExpandLayout_spaceMargin, dp2px(context, 20));
            mLineSpacingExtra = ta.getDimensionPixelSize(R.styleable.ExpandLayout_lineSpacingExtra, 0);
            mLineSpacingMultiplier = ta.getFloat(R.styleable.ExpandLayout_lineSpacingMultiplier, 1.0f);
            ta.recycle();
        }
        // mMaxLines應該保證大於等於1
        if (mMaxLines < 1) {
            mMaxLines = 1;
        }
    }

    /**
     * 初始化View
     */
    private void initView() {
        mRootView = inflate(mContext, R.layout.layout_expand, this);
        mTvContent = findViewById(R.id.expand_content_tv);
        mLayoutExpandMore = findViewById(R.id.expand_ll);
        mIconExpand = findViewById(R.id.expand_iv);
        mTvExpand = findViewById(R.id.expand_tv);
        mTvExpandHelper = findViewById(R.id.expand_helper_tv);

        mTvExpand.setText(mExpandMoreStr);
        mTvContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContentTextSize);
        // 輔助TextView,與內容TextView大小相等,保證末尾圖標和文字與內容文字居中顯示
        mTvExpandHelper.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContentTextSize);
        mTvExpand.setTextSize(TypedValue.COMPLEX_UNIT_PX, mExpandTextSize);
        mTvContent.setLineSpacing(mLineSpacingExtra, mLineSpacingMultiplier);
        mTvExpandHelper.setLineSpacing(mLineSpacingExtra, mLineSpacingMultiplier);
        mTvExpand.setLineSpacing(mLineSpacingExtra, mLineSpacingMultiplier);
        //默認設置展開圖標
        setExpandMoreIcon(mExpandIconResId);
        setContentTextColor(mContentTextColor);
        setExpandTextColor(mExpandTextColor);
        switch (mExpandStyle) {
            case STYLE_ICON:
                mIconExpand.setVisibility(VISIBLE);
                mTvExpand.setVisibility(GONE);
                break;
            case STYLE_TEXT:
                mIconExpand.setVisibility(GONE);
                mTvExpand.setVisibility(VISIBLE);
                break;
            default:
                mIconExpand.setVisibility(VISIBLE);
                mTvExpand.setVisibility(VISIBLE);
                break;
        }
    }

上面主要是通過繼承RelativeLayout,並通過obtainStyledAttributes()以及inflate()方法完成自定義屬性的初始化以及要顯示佈局的加載以及控件的獲取,在這裏只貼出關鍵部分代碼,完整代碼請將本人GitHub項目,鏈接請見在文章尾部

4、根據設置的文本,獲取控件寬度,根據配置的自定義屬性,計算並截取要顯示的縮略顯示的文本

在上面提及的三個問題中,都是與本步驟相關,下面也分點進行處理和分析,即

1)如何在TextView顯示文本前,獲取文本展示的行數是否超過了設置的最大縮略展示行數

2)如何在TextView顯示文本前,根據要顯示的文本獲取要最後一行文字的長度和字符下標

3)如何保證最後一行文字與展開/收縮提示佈局不重疊

A、如何獲取文本顯示的寬度?

在設置文本時,需要先確定文本顯示的寬度,通過上面佈局xml已經可以看到,在本實現中,TextView控件的寬度與外層佈局寬度一致,也就是文本顯示的寬度與外層佈局測量寬度大小一樣,只需要測量出外層佈局寬度,就可以得出文本顯示的寬度,獲取控件的寬度也有很多方法,本文采用View的ViewTreeObserver來獲取控件的寬度,通過給控件添加相應OnGlobalLayoutListener監聽器,在相應的回調方法中獲取佈局的測量寬度即可,如下面所示:

/**
     * 設置文本內容
     *
     * @param contentStr
     * @param onExpandStateChangeListener 狀態回調監聽器
     */
    public void setContent(String contentStr, final OnExpandStateChangeListener onExpandStateChangeListener) {
        if (TextUtils.isEmpty(contentStr) || mRootView == null) {
            return;
        }
        mOriginContentStr = contentStr;
        mOnExpandStateChangeListener = onExpandStateChangeListener;
        // 此處需要先設置mTvContent的text屬性,防止在列表中,由於沒有獲取到控件寬度mMeasuredWidth,先執行onMeasure方法測量時,導致文本只能顯示一行的問題
        // 提前設置好text,再執行onMeasure,則沒有該問題
        mTvContent.setMaxLines(mMaxLines);
        mTvContent.setText(mOriginContentStr);
        // 獲取文字的寬度
        if (mMeasuredWidth <= 0) {
            Log.d(TAG, "寬度尚未獲取到,第一次加載");
            getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    // 用完後立即移除監聽,防止多次回調的問題
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                        getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    } else {
                        getViewTreeObserver().removeGlobalOnLayoutListener(this);
                    }
                    mMeasuredWidth = getMeasuredWidth();
                    Log.d(TAG, "onGlobalLayout,控件寬度 = " + mMeasuredWidth);
                    measureEllipsizeText(mMeasuredWidth);
                }
            });
        } else {
            Log.d(TAG, "寬度已獲取到,非第一次加載");
            measureEllipsizeText(mMeasuredWidth);
        }
    }

上面代碼執行邏輯主要是:

1)如果沒有獲取到文本顯示寬度,通過給根佈局的ViewTreeObserver添加OnGlobalLayoutListener監聽器,在onGlobalLayout()回調時,也即佈局已經測量完成時,獲取根佈局的寬度,再根據文本顯示寬度執行measureEllipsizeText(int)去處理文本測量和分行處理,需要注意的是,OnGlobalLayoutListener監聽會回調多次,需要在第一次監聽回調後,remove移除OnGlobalLayoutListener的監聽,而且不同SDK版本方法不一樣,具體請參見代碼;

2)如果先前已確定文本寬度,直接執行measureEllipsizeText(int)去處理文本測量和分行處理。

在獲取文本顯示的寬度遇到的問題:

在實際項目使用時,發現通過上述方法,在普通佈局使用時,可以正常獲取到文本顯示的寬度,但在ListView或RecyclerView時,第一屏的Item中的ExpandLayout也可以正常獲取到文本的寬度,但往下滾動時,由於Item的複用,導致後面加載顯示的Item,在給根佈局的ViewTreeObserver添加OnGlobalLayoutListener監聽器時,無法正常回調onGlobalLayout(),導致無法顯示文本內容,針對這個問題,發現後加載的Item的ExpandLayout會正常回調執行onMeasure(int widthMeasureSpec, int heightMeasureSpec),因此考慮在這個時機,會獲取文本顯示寬度,從而解決了在ListView或RecyclerView中部分Item的ExpandLayout無法正常回調onGlobalLayout(),導致無法顯示文本內容的問題:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, "onMeasure,measureWidth = " + getMeasuredWidth());
        if (mMeasuredWidth <= 0 && getMeasuredWidth() > 0) {
            mMeasuredWidth = getMeasuredWidth();
            measureEllipsizeText(mMeasuredWidth);
        }
    }

 

 

B、如何在TextView顯示文本前,獲取文本展示的行數

這裏主要是利用了TextView文本排版處理,TextView中提供了三種文本處理工具輔助類:

 a、BoringLayout 主要適合單行文本顯示的情況 ;

 b、DynamicLayout 主要特性是在支持多行的情況下最關鍵的特性是監聽文本的改動,性能略低StaticLayout,靜態文本顯示 不建議使用這個;

c、StaticLayout 顧名思義特點適合多行靜態文本顯示的情況

由於app當中很大部分文本都是靜態顯示的效果,所以本文主要使用的就是StaticLayout來查理TextView文本的排查以及拆行,以到達沒有進行TextView.setText()設置顯示文本時,通過TextView的TextPaint以及要顯示文本等設置參數,獲取到文本排版情況,其中,通過StaticLayout.getLineCount()方法獲取文本顯示行數,如下代碼所示,具體完整代碼請參見項目代碼:

 /**
     * 使用StaticLayout處理文本分行和佈局
     *
     * @param lineWidth 文本(佈局)寬度
     */
    private void handleMeasureEllipsizeText(int lineWidth) {
        TextPaint textPaint = mTvContent.getPaint();
        StaticLayout staticLayout = new StaticLayout(mOriginContentStr, textPaint, lineWidth, Layout.Alignment.ALIGN_NORMAL, mLineSpacingMultiplier, mLineSpacingExtra, false);
        int lineCount = staticLayout.getLineCount();
        if (lineCount <= mMaxLines) {
            // 不足最大行數,直接設置文本
            //少於最小展示行數,不再展示更多相關佈局
            mEllipsizeStr = mOriginContentStr;
            mLayoutExpandMore.setVisibility(View.GONE);
            mTvContent.setMaxLines(Integer.MAX_VALUE);
            mTvContent.setText(mOriginContentStr);
        } else {
            // 超出最大行數
            mRootView.setOnClickListener(this);
            mLayoutExpandMore.setVisibility(View.VISIBLE);
            // Step1:第mMinLineNum行的處理
            handleEllipsizeString(staticLayout, lineWidth);
            // Step2:最後一行的處理
            handleLastLine(staticLayout, lineWidth);
            .......
        }
    }

C、如何在TextView顯示文本前,根據要顯示的文本獲取要最後一行文字的長度和字符下標

這裏主要是利用了StaticLayout以及TextPaint相關接口方法來獲取某一行文本以及文本長度或下標,主要思路如下:

1)通過StaticLayout的getLineStart(int)以及getLineEnd(int)方法,可以分別獲取到某一行的在整個文本中的起始和結束下標;

2)在得到起始index和結束index後,再通過String的取子串方法substring(int,int)即可獲取到某一行要顯示的文本

3)最後再利用TextView的TextPaint的measureText()方法,即可獲取到該行文本的長度。

如下處理展開時,最後一行文本處理代碼所示,具體完整代碼請參見項目代碼:

/**
 * 處理最後一行文本
 *
 * @param staticLayout
 * @param lineWidth
 */
private void handleLastLine(StaticLayout staticLayout, int lineWidth) {
    if (staticLayout == null) {
        return;
    }
    int lineCount = staticLayout.getLineCount();
    if (lineCount < 1) {
        return;
    }
    int startPos = staticLayout.getLineStart(lineCount - 1);
    int endPos = staticLayout.getLineEnd(lineCount - 1);
    Log.d(TAG, "最後一行 startPos = " + startPos);
    Log.d(TAG, "最後一行 endPos = " + endPos);
    // 修正,防止取子串越界
    if (startPos < 0) {
        startPos = 0;
    }
    if (endPos > mOriginContentStr.length()) {
        endPos = mOriginContentStr.length();
    }
    if (startPos > endPos) {
        startPos = endPos;
    }
    String lastLineContent = mOriginContentStr.substring(startPos, endPos);
    Log.d(TAG, "最後一行 內容 = " + lastLineContent);
    float textLength = 0f;
    TextPaint textPaint = mTvContent.getPaint();
    if (lastLineContent != null) {
        textLength = textPaint.measureText(lastLineContent);
    }
    Log.d(TAG, "最後一行 文本長度 = " + textLength);
    float reservedWidth = getExpandLayoutReservedWidth();
    if (textLength + reservedWidth > lineWidth) {
        //文字寬度+展開佈局的寬度 > 一行最大展示寬度
        //換行展示“收起”按鈕及文字
        mOriginContentStr += "\n";
    }
}

 

D、如何保證最後一行文字與展開/收縮提示佈局不重疊

在前面的問題中已描述得出,我們可以獲取到某一行顯示文本的文本以及文本寬度,有了這個理論支持後,我們需要考慮如下文本分別在展開和收起兩種狀態下,最後一行文字與展開/收縮提示佈局不重疊:

1、在文本收縮時,需要根據每行文本顯示的最大測量寬度lineWidth縮略時最後一行顯示的文本寬度textLength、展開/收縮提示佈局寬度expandTipLayoutWidth以及兩者之間設置的間距mSpaceMargin來綜合考慮:

需要預留的固定寬度爲reservedWidth:"..." + 展開佈局與文本間距 +圖標長度 + 展開文本長度,即"..."文本的測量寬度+ mSpaceMargin + expandTipLayoutWidth;

則控件收縮展示時,最後一行文本顯示的最大長度爲每行文本顯示的最大測量寬度lineWidth - 需要預留的固定寬度爲reservedWidth,

在收縮顯示情況下,此時有兩種情況:

1)如果最後一行文本顯示的最大長度比縮略時最後一行顯示的文本寬度textLength相等或還要大,則證明最後一行文本可以完整顯示

2)如果最後一行文本顯示的最大長度比縮略時最後一行顯示的文本寬度textLength還要小,則證明最後一行文本不可以完整顯示,需要在進行等比例截取

如下代碼所示,具體完整代碼請參見項目代碼:

/**
     * 處理縮略的字符串
     *
     * @param staticLayout
     * @param lineWidth
     */
    private void handleEllipsizeString(StaticLayout staticLayout, int lineWidth) {
        if (staticLayout == null) {
            return;
        }
        TextPaint textPaint = mTvContent.getPaint();
        // 獲取到第mMinLineNum行的起始和結束位置
        int startPos = staticLayout.getLineStart(mMaxLines - 1);
        int endPos = staticLayout.getLineEnd(mMaxLines - 1);
        Log.d(TAG, "startPos = " + startPos);
        Log.d(TAG, "endPos = " + endPos);
        // 修正,防止取子串越界
        if (startPos < 0) {
            startPos = 0;
        }
        if (endPos > mOriginContentStr.length()) {
            endPos = mOriginContentStr.length();
        }
        if (startPos > endPos) {
            startPos = endPos;
        }
        String lineContent = mOriginContentStr.substring(startPos, endPos);
        float textLength = 0f;
        if (lineContent != null) {
            textLength = textPaint.measureText(lineContent);
        }
        Log.d(TAG, "第" + mMaxLines + "行 = " + lineContent);
        Log.d(TAG, "第" + mMaxLines + "行 文本長度 = " + textLength);

        String strEllipsizeMark = "...";
        // 展開控件需要預留的長度,預留寬度:"..." + 展開佈局與文本間距 +圖標長度 + 展開文本長度
        float reservedWidth = mSpaceMargin + textPaint.measureText(strEllipsizeMark) + getExpandLayoutReservedWidth();
        Log.d(TAG, "需要預留的長度 = " + reservedWidth);
        int correctEndPos = endPos;
        if (reservedWidth + textLength > lineWidth) {
            // 空間不夠,需要按比例截取最後一行的字符串,以確保展示的最後一行文本不會與可展開佈局重疊
            float exceedSpace = reservedWidth + textLength - lineWidth;
            if (textLength != 0) {
                correctEndPos = endPos - (int) ((exceedSpace / textLength) * 1.0f * (endPos - startPos));
            }
        }
        Log.d(TAG, "correctEndPos = " + correctEndPos);
        String ellipsizeStr = mOriginContentStr.substring(0, correctEndPos);
        mEllipsizeStr = removeEndLineBreak(ellipsizeStr) + strEllipsizeMark;
    }

2、在文本展開時,需要也根據每行文本顯示的最大測量寬度lineWidth展開時最後一行顯示的文本寬度textLength、展開/收縮提示佈局寬度expandTipLayoutWidth來綜合考慮,有如下兩種情況:

1)文字寬度+展開/收縮提示佈局的寬度 <= 一行最大展示寬度,可以正常顯示,無需特殊處理

2)文字寬度+展開/收縮提示佈局的寬度 > 一行最大展示寬度,則需要換行展示可展開/收縮提示按鈕及文字

如下代碼所示,具體完整代碼請參見項目代碼:

/**
     * 處理最後一行文本
     *
     * @param staticLayout
     * @param lineWidth
     */
    private void handleLastLine(StaticLayout staticLayout, int lineWidth) {
        if (staticLayout == null) {
            return;
        }
        int lineCount = staticLayout.getLineCount();
        if (lineCount < 1) {
            return;
        }
        int startPos = staticLayout.getLineStart(lineCount - 1);
        int endPos = staticLayout.getLineEnd(lineCount - 1);
        Log.d(TAG, "最後一行 startPos = " + startPos);
        Log.d(TAG, "最後一行 endPos = " + endPos);
        // 修正,防止取子串越界
        if (startPos < 0) {
            startPos = 0;
        }
        if (endPos > mOriginContentStr.length()) {
            endPos = mOriginContentStr.length();
        }
        if (startPos > endPos) {
            startPos = endPos;
        }
        String lastLineContent = mOriginContentStr.substring(startPos, endPos);
        Log.d(TAG, "最後一行 內容 = " + lastLineContent);
        float textLength = 0f;
        TextPaint textPaint = mTvContent.getPaint();
        if (lastLineContent != null) {
            textLength = textPaint.measureText(lastLineContent);
        }
        Log.d(TAG, "最後一行 文本長度 = " + textLength);
        float reservedWidth = getExpandLayoutReservedWidth();
        if (textLength + reservedWidth > lineWidth) {
            //文字寬度+展開佈局的寬度 > 一行最大展示寬度
            //換行展示“收起”按鈕及文字
            mOriginContentStr += "\n";
        }
    }

 

四、使用

ExpandLayout在普通佈局以及ListView或RecyclerView中使用方法一致,使用也很簡單:

1、在佈局xml中聲明ExpandLayout,如下所示:

<com.baymax.widget.ExpandLayout
            android:id="@+id/my_expand_default"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="20dp"
            android:layout_marginEnd="20dp"
            android:layout_marginTop="20dp"
            app:maxLines="2"
            app:expandIconResId="@drawable/splitter_more"
            app:collapseIconResId="@drawable/splitter_less"
            app:expandMoreText="更多"
            app:collapseLessText="收起"
            app:contentTextSize="20sp"
            app:expandTextSize="16sp"
            app:expandTextColor="@color/colorPrimaryDark"
            app:spaceMargin="30dp"
            app:expandIconWidth="15dp"/>

2、獲取ExpandLayout實例,並設置需要顯示的文本:

下面介紹文本文本設置,其他自定義屬性的配置和設置請參考實例代碼

文本設置使用有如下兩種方式:

1)普通設置,直接通過調用ExpandLayout的setContent(String)方法,直接將要顯示的文本設置即可:

mExpandLayout = findViewById(R.id.my_expand_layout);
String contentStr = "我是正常的全中文文字,可以點擊我展開查看更多或收起,我是圖標+文字的默認模式";
mExpandLayout.setContent(contentStr);

      2)帶展開和收縮狀態的回調監聽的文本設置

有一些業務可能會需要監聽文本展開或收縮時做一些其它操作,如果需要監聽展開和收縮狀態,直接調用ExpandLayout的setContent(String,OnExpandStateChangeListener),並重寫OnExpandStateChangeListener相應的狀態回調方法即可:

mExpandLayout = findViewById(R.id.my_expand_layout);
String contentStr = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz";
mExpandLayout.setContent(contentStr, new ExpandLayout.OnExpandStateChangeListener() {
      @Override
      public void onExpand() {
      Toast.makeText(mActivity, "onExpand", Toast.LENGTH_SHORT).show();
       }

      @Override
      public void onCollapse() {
      Toast.makeText(mActivity, "onCollapse", Toast.LENGTH_SHORT).show();
       }
});

五、相關鏈接

所有代碼已提交到本人GitHub項目,有興趣的朋友可以參考本人GitHub項目代碼和參考使用例程,希望能給大家帶有更多的參考價值,鏈接請見https://github.com/oukanggui/WidgetLibrary/blob/master/widget/src/main/java/com/baymax/widget/ExpandLayout.java

歡迎大家一起完善該控件,有問題或建議歡迎在issue提出。歡迎大家Star!

發佈了11 篇原創文章 · 獲贊 19 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章