1. 程式人生 > >自定義可展開收起TextView,展開收起按鈕緊跟文字內容

自定義可展開收起TextView,展開收起按鈕緊跟文字內容

自定義展開收起TextView的文章有很多,不過大多都是在文字後面一行新增一個按鈕,自定義按鈕監聽事件達到展開收起的目標,但是專案需求展開收起的文案要緊跟著文字內容。先上效果圖:


如果收起文案需要換行才能顯示完整,則直接將收起文案展示在下一行


直接上程式碼,詳情見註釋

ExpandTextView.class

import android.content.Context;
import android.os.Build;
import android.text.Layout;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.method.LinkMovementMethod;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;


/**
 * 自定義控制元件,長文字展開收起TextView
 */

public class ExpandTextView extends TextView {

    private String originText;// 原始內容文字
    private int initWidth = 0;// TextView可展示寬度
    private int mMaxLines = 3;// TextView最大行數
    private SpannableString SPAN_CLOSE = null;// 收起的文案(顏色處理)
    private SpannableString SPAN_EXPAND = null;// 展開的文案(顏色處理)
    private String TEXT_EXPAND = "  檢視更多>";
    private String TEXT_CLOSE = "  <收起";

    public ExpandTextView(Context context) {
        super(context);
        initCloseEnd();
    }

    public ExpandTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initCloseEnd();
    }

    public ExpandTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initCloseEnd();
    }

    /**
     * 設定TextView可顯示的最大行數
     * @param maxLines 最大行數
     */
    @Override
    public void setMaxLines(int maxLines) {
        this.mMaxLines = maxLines;
        super.setMaxLines(maxLines);
    }

    /**
     * 初始化TextView的可展示寬度
     * @param width
     */
    public void initWidth(int width){
        initWidth = width;
    }

    /**
     * 收起的文案(顏色處理)初始化
     */
    private void initCloseEnd(){
        String content = TEXT_EXPAND;
        SPAN_CLOSE = new SpannableString(content);
        ButtonSpan span = new ButtonSpan(getContext(), new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
                setExpandText(originText);
            }
        }, R.color.color_fe9901);
        SPAN_CLOSE.setSpan(span, 0, content.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    }

    /**
     * 展開的文案(顏色處理)初始化
     */
    private void initExpandEnd(){
        String content = TEXT_CLOSE;
        SPAN_EXPAND = new SpannableString(content);
        ButtonSpan span = new ButtonSpan(getContext(), new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                ExpandTextView.super.setMaxLines(mMaxLines);
                setCloseText(originText);
            }
        }, R.color.color_fe9901);
        SPAN_EXPAND.setSpan(span, 0, content.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    }

    public void setCloseText(CharSequence text) {

        if(SPAN_CLOSE == null){
            initCloseEnd();
        }
        boolean appendShowAll = false;// true 不需要展開收起功能, false 需要展開收起功能
        originText = text.toString();

        // SDK >= 16 可以直接從xml屬性獲取最大行數
        int maxLines = 0;
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
            maxLines = getMaxLines();
        } else{
            maxLines = mMaxLines;
        }
        String workingText = new StringBuilder(originText).toString();
        if (maxLines != -1) {
            Layout layout = createWorkingLayout(workingText);
            if (layout.getLineCount() > maxLines) {
                //獲取一行顯示字元個數,然後擷取字串數
                workingText = originText.substring(0, layout.getLineEnd(maxLines - 1)).trim();// 收起狀態原始文字擷取展示的部分
                String showText = originText.substring(0, layout.getLineEnd(maxLines - 1)).trim() + "..." + SPAN_CLOSE;
                Layout layout2 = createWorkingLayout(showText);
                // 對workingText進行-1擷取,直到展示行數==最大行數,並且新增 SPAN_CLOSE 後剛好佔滿最後一行
                while (layout2.getLineCount() > maxLines) {
                    int lastSpace = workingText.length()-1;
                    if (lastSpace == -1) {
                        break;
                    }
                    workingText = workingText.substring(0, lastSpace);
                    layout2 = createWorkingLayout(workingText + "..." + SPAN_CLOSE);
                }
                appendShowAll = true;
                workingText = workingText + "...";
            }
        }

        setText(workingText);
        if (appendShowAll) {
            // 必須使用append,不能在上面使用+連線,否則spannable會無效
            append(SPAN_CLOSE);
            setMovementMethod(LinkMovementMethod.getInstance());
        }
    }

        public void setExpandText(String text) {
            if(SPAN_EXPAND == null){
                initExpandEnd();
            }
            Layout layout1 = createWorkingLayout(text);
            Layout layout2 = createWorkingLayout(text + TEXT_CLOSE);
            // 展示全部原始內容時 如果 TEXT_CLOSE 需要換行才能顯示完整,則直接將TEXT_CLOSE展示在下一行
            if(layout2.getLineCount() > layout1.getLineCount()){
                setText(originText + "\n");
            }else{
                setText(originText);
            }
            append(SPAN_EXPAND);
            setMovementMethod(LinkMovementMethod.getInstance());
        }

    //返回textview的顯示區域的layout,該textview的layout並不會顯示出來,只是用其寬度來比較要顯示的文字是否過長
    private Layout createWorkingLayout(String workingText) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            return new StaticLayout(workingText, getPaint(), initWidth - getPaddingLeft() - getPaddingRight(),
                    Layout.Alignment.ALIGN_NORMAL, getLineSpacingMultiplier(), getLineSpacingExtra(), false);
        } else{
            return new StaticLayout(workingText, getPaint(), initWidth - getPaddingLeft() - getPaddingRight(),
                    Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
        }
    }
}
ButtonSpan.class
import android.content.Context;
import android.text.TextPaint;
import android.text.style.ClickableSpan;
import android.view.View;


public class ButtonSpan extends ClickableSpan {

    View.OnClickListener onClickListener;
    private Context context;
    private int colorId;

    public ButtonSpan(Context context, View.OnClickListener onClickListener) {
        this(context, onClickListener, R.color.color_693f3e);
    }

    public ButtonSpan(Context context, View.OnClickListener onClickListener, int colorId){
        this.onClickListener = onClickListener;
        this.context = context;
        this.colorId = colorId;
    }

    @Override
    public void updateDrawState(TextPaint ds) {
        ds.setColor(context.getResources().getColor(colorId));
        ds.setUnderlineText(false);
    }

    @Override
    public void onClick(View widget) {
        if (onClickListener != null) {
            onClickListener.onClick(widget);
        }
    }
}
使用例子
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    private ExpandTextView mContentExpandTextView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mContentExpandTextView = (ExpandTextView) findViewById(R.id.txt_content);
        // 設定TextView可展示的寬度 ( 父控制元件寬度 - 左右margin - 左右padding)
        int whidth = ScreenUtils.getScreenWidth(this) - ScreenUtils.dip2px(this, 16 * 2);
        mContentExpandTextView.initWidth(whidth);
        // 設定最大行數(如果SDK >= 16 也可以直接在xml裡設定)
        mContentExpandTextView.setMaxLines(3);
        String content = "茫茫的長白大山,浩瀚的原始森林,大山腳下,原始森林環抱中散落著幾十戶人家的" +
                "一個小山村,茅草房,對面炕,煙筒立在屋後邊。在村東頭有一個獨立的房子,那就是青年點," +
                "窗前有一道小溪流過。學子在這裡吃飯,由這裡出發每天隨社員去地裡幹活。乾的活要麼上山伐" +
                "樹,擡樹,要麼砍柳樹毛子開荒種地。在山裡,可聽那吆呵聲:“順山倒了!”放樹謹防回頭棒!" +
                "樹上的枯枝打到別的樹上再蹦回來,這回頭棒打人最厲害。";
        mContentExpandTextView.setCloseText(content);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.lxw.expandtextview.MainActivity">

    <com.lxw.expandtextview.ExpandTextView
        android:id="@+id/txt_content"
        android:textSize="20sp"
        android:textColor="@color/color_693f3e"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</RelativeLayout>
需要特別注意的是設定ExpandTextView可展示寬度,如果這個寬度值計算錯誤,則ExpandTextView會出現顯示異常的情況。

參考資料: