Android:自定义控件:可折叠展开的TextView

最近,写Android代码遇到的需求,需要文本可折叠,通过参考借鉴网上的资料和思路,自己写了一个。写篇博客记录下来,免得以后要用的时候,又要百度。

需求:在折叠状态下,文本如果大于最大显示行数,则在文本框能显示的最后一行加上“... 展开”,并与可显示的文本一起铺满整个文本框;在展开状态下,文本尾加上“ 折叠”。“展开”和“折叠”字体均为红色,且均可点击,点击后文本框变化到对应状态。

上代码(用ClickableSpan实现):

public class ExpandTextView extends AppCompatTextView {

    private String originText = "";                //原始内容文本
    private int initWidth = 0;                //TextView可展示宽度
    private int mMaxLines = 2;                //TextView最大显示行数
    private SpannableString SPAN_CLOSE = null;       //收起的文本(颜色处理和点击监听)
    private SpannableString SPAN_EXPAND = null;      //展开的文本(颜色处理和点击监听)
    private String TEXT_EXPAND = " 展开";
    private String TEXT_CLOSE = " 收起";
    private int SPAN_COLOR = R.color.red;
    private boolean isClose = true;

    private boolean isWidthModify = true;           //用于控件宽高改变时通知重绘的判断,避免循环绘制

    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);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        initWidth = getMeasuredWidth();
        if(isWidthModify) {
            notifyRefreshView();
            isWidthModify = false;
        }
    }

    /**
     * 初始化"展开"的Span文本,即收起状态的文本尾
     */
    private void initCloseEnd(){
        String content = TEXT_EXPAND;
        SPAN_CLOSE = new SpannableString(content);
        SPAN_CLOSE.setSpan(new NoRefCopySpan() {
            @Override
            public void updateDrawState(TextPaint ds) {
                ds.setColor(getResources().getColor(SPAN_COLOR));
                ds.setUnderlineText(false);
            }

            @Override
            public void onClick(@NonNull View widget) {
                ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
                setExpandText(originText);
            }

        }, 0, content.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    }

    /**
     * 初始化"收起"的Span文本,即展开状态的文本尾
     */
    private void initExpandEnd(){
        String content = TEXT_CLOSE;
        SPAN_EXPAND = new SpannableString(content);
        SPAN_EXPAND.setSpan(new NoRefCopySpan() {
            @Override
            public void updateDrawState(TextPaint ds) {
                ds.setColor(getResources().getColor(SPAN_COLOR));
                ds.setUnderlineText(false);
            }

            @Override
            public void onClick(@NonNull View widget) {
                ExpandTextView.super.setMaxLines(mMaxLines);
                setCloseText(originText);
            }

        }, 0, content.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
    }

    /**
     * 设置收起状态时的文本
     */
    private void setCloseText(CharSequence text) {
        if(SPAN_CLOSE == null){
            initCloseEnd();
        }
        boolean appendShowAll = false;//true不需要展开收起功能(文件行数小于或等于最大行),false需要展开收起功能
        originText = text.toString();

        //SDK >= 16 可以直接从xml属性获取最大行数
        int maxLines;
        if (Build.VERSION.SDK_INT >= 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());
        }
    }

    /**
     * 设置展开状态时的文本
     */
    private 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);
        }
    }

    /**
     * sdk>=16,可以通过xml进行配置,但16以下必须用该方法配置
     */
    @Override
    public void setMaxLines(int maxLines) {
        this.mMaxLines = maxLines;
        super.setMaxLines(maxLines);
    }

    /**
     * @param originText 待显示的原文本;
     * @param isClose 设置TextView的初始状态是否为收起;
     * 设置文本时,需要调用该方法来代替setText。若直接使用setText方法设置文本,
     * 该控件只会作为普通TextView显示。
     */
    public void setText(String originText, Boolean isClose) {
        this.originText = originText;
        this.isClose = isClose;
        isWidthModify = true;
        notifyRefreshView();
    }

    /**
     * 修改设置后通知重绘,setText(String originText, Boolean isClose)方法不需要调用
     */
    public void notifyRefreshView() {
        if(isClose) {
            ExpandTextView.super.setMaxLines(mMaxLines);
            setCloseText(originText);
        } else {
            ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
            setExpandText(originText);
        }
    }

    /**
     * @param colorId 展开和收起时,文本尾的Span文本的颜色;
     * 设置展开和收起时文本尾的Span文本的颜色(若不以该方法设置,则默认为R.color.red)。
     */
    public void setLabelTextColor(int colorId) {
        SPAN_COLOR = colorId;
        initExpandEnd();
        initCloseEnd();
    }

    /**
     * @param str 收起时,文本尾代表"展开"的Span文本内容;
     * 设置文本尾的"展开"Span文本的内容(若不以该方法设置,则默认为"展开")。
     */
    public void setExpandLabelText(String str) {
        TEXT_EXPAND = str;
        initExpandEnd();
    }

    /**
     * @param str 展开时,文本尾代表"收起"的Span文本内容;
     * 设置文本尾的"收起"Span文本的内容(若不以该方法设置,则默认为"收起")。
     */
    public void setCLOSELabelText(String str) {
        TEXT_CLOSE = str;
        initCloseEnd();
    }

    /**
     * 实现NoCopySpan的原因是,为了防止Span在onSaveInstanceState时被复制保存,导致内存泄漏
     */
    public static class NoRefCopySpan extends ClickableSpan implements NoCopySpan {

        @Override
        public void onClick(@NonNull View widget) {
            //等待子类继承
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            super.updateDrawState(ds);
        }

    }
}

你可能感兴趣的:(Android,Java)