Android 可折叠自定义ExpandTextView

Posted Keepingrun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 可折叠自定义ExpandTextView相关的知识,希望对你有一定的参考价值。

参考自他人https://blog.csdn.net/Luckly452468460/article/details/103613471
通过自己实现CreateAppenderListener,可以自己定义 尾部追加的文字或者图片,或者文字加图片。
有注释,就不解释细节了。
使用:
折叠状态:

展开状态:

<!--这里的宽度不能设置wrap_content,可以具体值或者match_parent-->
<com.example.myapplication.ExpandTextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:maxLines="4" />
expandTextView.init(false, text, null);
expandTextView.setOnClickListener(new View.OnClickListener() 
            @Override
            public void onClick(View v) 
                expandTextView.toggle();
            
);

实现:

package com.example.myapplication;

import android.content.Context;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
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.text.style.ImageSpan;
import android.util.AttributeSet;

import androidx.annotation.Nullable;


/**
 * 自定义控件,文本展开收起TextView
 */
public class ExpandTextView extends androidx.appcompat.widget.AppCompatTextView 
    /**
     * 原始内容文本
     */
    private String originText;
    /**
     * TextView可展示宽度
     */
    private int mWidth = Integer.MAX_VALUE;
    /**
     * TextView限制显示的最大行数
     */
    private int mMaxLines = 0;
    /**
     * 收起状态时的拼接文案
     */
    private SpannableString SPAN_TO_EXPAND = null;
    /**
     * 展开状态时的拼接文案
     */
    private SpannableString SPAN_TO_CLOSE = null;
    /**
     * 文本格式(true全角   false半角)
     */
    private boolean ToDBC = true;
    /**
     * 状态值 true:展开中  false:折叠状态   (该状态值只能在当前类内部修改)
     */
    private boolean mIsExpanding = false;
    private int mOriginTextLines;
    /**
     * 追加的图片的宽度
     */
    private int mToExpandImageWidth = 0;
    private int mToCloseImageWidth = 0;

    public ExpandTextView(Context context) 
        super(context);

    

    public ExpandTextView(Context context, AttributeSet attrs) 
        super(context, attrs);

    

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

    

    /**
     * 使用前必须调用该方法
     */
    public void init(boolean toExpand, String text, @Nullable CreateAppenderListener createAppenderListener) 
        mMaxLines = getMaxLines();
        originText = ToDBC ? ToDBC(text) : toDBC(text);
        if (createAppenderListener != null) 
            // 1.优先使用listener的生成
            SPAN_TO_CLOSE = createAppenderListener.getDefaultToCloseSpannableString();
            SPAN_TO_EXPAND = createAppenderListener.getDefaultToExpandSpannableString();
         else 
            // 2.使用默认的SpannableString
            SPAN_TO_CLOSE = getDefaultToCloseSpannableString();
            SPAN_TO_EXPAND = getDefaultToExpandSpannableString();
        

        mOriginTextLines = createWorkingLayout(originText).getLineCount();
        // 设置初始显示的文本
        toggle(toExpand);
    

    public void toggle(boolean toExpand) 
        // 由于获取不到textview的宽度,所以这里用post方法
        post(new Runnable() 
            @Override
            public void run() 
                mWidth = getWidth();
                if (toExpand) 
                    setExpandText();
                 else 
                    setCloseText();
                
            
        );
    

    public void toggle() 
        if (mIsExpanding) 
            toggle(false);
         else 
            toggle(true);
        

    

    /**
     * 设置TextView可显示的最大行数
     *
     * @param maxLines 最大行数
     */
    @Override
    public void setMaxLines(int maxLines) 
        if (mMaxLines == 0) 
            // 这里对mMaxLines记录一次就可以
            this.mMaxLines = maxLines;
        
        super.setMaxLines(maxLines);
    

    public void setToDBC(boolean toDBC) 
        ToDBC = toDBC;
    

    public int getToExpandImageWidth() 
        return mToExpandImageWidth;
    

    public int getToCloseImageWidth() 
        return mToCloseImageWidth;
    

    public void setToExpandImageWidth(int mToExpandImageWidth) 
        this.mToExpandImageWidth = mToExpandImageWidth;
    

    public void setToCloseImageWidth(int mToCloseImageWidth) 
        this.mToCloseImageWidth = mToCloseImageWidth;
    

    public SpannableString getDefaultToExpandSpannableString() 
        SpannableString spannableString = new SpannableString("... ");
        // 测量文字的高度,用于设置图片大小
        Paint paint = getPaint();
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        setToExpandImageWidth((int) (fontMetrics.descent - fontMetrics.ascent));
        // 对图片的宽高设置
        Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
        drawable.setBounds(0, 0 , getToExpandImageWidth(), getToExpandImageWidth());
        ImageSpan span = new ImageSpan(drawable);
        spannableString.setSpan(span, spannableString.length() - 1, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        // 对imageSpan设置点击事件,也可以这里不设置,在Activity对expandTextView整个事件设置点击事件
//        spannableString.setSpan(new ClickableSpan() 
//            @Override
//            public void onClick(@NonNull View widget) 
//                toggle();
//            
//        , spannableString.length() - 1, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        return spannableString;
    


    public SpannableString getDefaultToCloseSpannableString() 
        // 因为末尾要插入图片,这里空格是占位符
        SpannableString spannableString = new SpannableString(" ");
        // 测量文字的高度,用于设置图片大小
        Paint paint = getPaint();
        Paint.FontMetrics fontMetrics = paint.getFontMetrics();
        setToCloseImageWidth((int) (fontMetrics.descent - fontMetrics.ascent));
        // 对图片的宽高设置
        Drawable drawable = getResources().getDrawable(R.mipmap.ic_launcher);
        drawable.setBounds(0, 0 , getToCloseImageWidth(), getToCloseImageWidth());
        ImageSpan span = new ImageSpan(drawable);
        spannableString.setSpan(span, spannableString.length() - 1, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
//        spannableString.setSpan(new ClickableSpan() 
//            @Override
//            public void onClick(@NonNull View widget) 
//                toggle();
//            
//        , spannableString.length() - 1, spannableString.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        return spannableString;
    

    /**
     * 设置文本收起
     */
    public void setCloseText() 
        mIsExpanding = false;
        setMaxLines(mMaxLines);

        boolean needAppend = false;
        String workingText = originText;

        if (mMaxLines != 0) 
            Layout originLayout = createWorkingLayout(originText);
            mOriginTextLines = originLayout.getLineCount();
            // 原始文本的行数 大于 最大能显示行数
            if (mOriginTextLines > mMaxLines) 
                // 获取mMaxLines行的文本
                workingText = originText.substring(0, originLayout.getLineEnd(mMaxLines - 1)).trim();
                // 计算mMaxLines行的文本的宽度
                float allWidth = getPaint().measureText(workingText);
                // 当前显示需要的宽度
                float realWidth = getPaint().measureText(workingText + SPAN_TO_EXPAND) + mToExpandImageWidth;

                while (realWidth > allWidth) 
                    int lastSpace = workingText.length() - 1;
                    if (lastSpace == -1) 
                        break;
                    
                    workingText = workingText.substring(0, lastSpace);
                    realWidth = getPaint().measureText(workingText + SPAN_TO_EXPAND) + mToExpandImageWidth;
                

                needAppend = true;
            
        
        setText(workingText);
        if (needAppend) 
            // 必须使用append,不能在上面使用+连接,否则spannable会无效
            append(SPAN_TO_EXPAND);
        
        setMovementMethod(LinkMovementMethod.getInstance());
    

    /**
     * 设置文本展开
     */
    public void setExpandText() 
        if (mOriginTextLines <= mMaxLines) 
            return;
        
        mIsExpanding = true;
        setMaxLines(Integer.MAX_VALUE);

        Layout originLayout = createWorkingLayout(originText);
        Layout compareLayout = createWorkingLayout(originText + SPAN_TO_CLOSE);
        if (compareLayout.getLineCount() > originLayout.getLineCount()) 
            setText(originText + "\\n");
         else 
            setText(originText);
        
        append(SPAN_TO_CLOSE);
        setMovementMethod(LinkMovementMethod.getInstance());
    

    /**
     * 返回textview的显示区域的layout
     */
    private Layout createWorkingLayout(String workingText) 
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) 
            return StaticLayout.Builder.obtain(workingText, 0, workingText.length(), getPaint(), mWidth).build();
         else 
            return new StaticLayout(workingText, getPaint(), mWidth - getPaddingLeft() - getPaddingRight(),
                    Layout.Alignment.ALIGN_NORMAL, getLineSpacingMultiplier(), getLineSpacingExtra(), false);
        
    

    /**
     * 屏蔽长按事件,防止崩溃
     *
     * @param longClickable
     */
    @Override
    public void setLongClickable(boolean longClickable) 
        super.setLongClickable(false);
    

    /**
     * 转全角
     *
     * @param input
     * @return
     */
    private static String toDBC(String input) 
        char[] c = input.toCharArray();
        for (int i = 0; i < c.length; i++) 
            if (c[i] == '\\n') 

             else if (c[i] == ' ') 
                c[i] = '\\u3000';
             else if (c[i] < '\\177') 
                c[i] = (char) (c[i] + 65248);
            
        
        return new String(c);
    

    /**
     * 转半角
     *
     * @param input
     * @return
     */
    public static String ToDBC(String input) 
        char[] c = input.toCharArray();
        for (int i = 0; i < c.length; i++) 
            if (c[i] == '\\u3000') 
                c[i] = ' ';
             else if (c[i] > '\\uFF00' && c[i] < '\\uFF5F') 
                c[i] = (char) (c[i] - 65248);

            
        
        return new String(c);
    

    /**
     * 用于生成 文本末尾要追加的SpannableString
     */
    public interface CreateAppenderListener 
        SpannableString getDefaultToCloseSpannableString();
        SpannableString getDefaultToExpandSpannableString();
    

以上是关于Android 可折叠自定义ExpandTextView的主要内容,如果未能解决你的问题,请参考以下文章

如何在android中实现自定义可折叠工具栏?

可折叠部分和每个部分的多个自定义 UITableViewCell

自定义可折叠视图

自定义手风琴/可折叠 UITableViewCell 的自动布局

Android--View自定义-折叠

Android 自定义 View-->TextView 的展开 & 收起(文本折叠)