Android自定义控件之全文收起TextView(继承TextView法)

前言

因为公司项目需要全文收起的功能,一共有2种UI,所以需要写2个全文收起的控件,关于第一个控件已经在第一篇文章讲述嵌套法实现全文收起TextView,本篇文章主要讲述直接继承至TextView的实现方法

效果图

全文收起TextView效果图

实现原理

通过另外一个方法设置文本,同时在GlobalLayoutListener中计算每行出需要显示的总行数,判断是否需要全文收起功能,如果需要,则计算出每行需要显示多少文本,在设定的最大行计算时,把...+全文加进去计算,得到实际上应该显示的文本,同时把全文设置为可点击的文本,在点击事件中根据状态设置当前TextView显示的文本,如果当前状态是收起状态,点击后就设置显示所有文字+收起,全文状态则设置显示文本为最开始计算出来的文本

代码

package wang.raye.library.widge;

import android.annotation.TargetApi;  
import android.content.Context;  
import android.content.res.TypedArray;  
import android.os.Build;  
import android.text.SpannableStringBuilder;  
import android.text.Spanned;  
import android.text.TextPaint;  
import android.text.TextUtils;  
import android.text.method.LinkMovementMethod;  
import android.text.style.ClickableSpan;  
import android.util.AttributeSet;  
import android.view.View;  
import android.view.ViewTreeObserver;  
import android.widget.TextView;

import wang.raye.library.R;


/**
 * 有全文和收起的TextView ExpandableTextView
 * Created by Raye on 2016/6/24.
 */
public class CollapsedTextView extends TextView {  
    private static final String TAG = CollapsedTextView.class.getName();
    /** 收起状态下的最大行数*/
    private int maxLine = 2;
    /** 截取后,文本末尾的字符串*/
    private static final String ELLIPSE = "...";
    /** 默认全文的Text*/
    private static final String EXPANDEDTEXT = "全文";
    /** 默认收起的text*/
    private static final String COLLAPSEDTEXT = "收起";
    /** 全文的text*/
    private String expandedText = EXPANDEDTEXT;
    /** 收起的text*/
    private String collapsedText = COLLAPSEDTEXT;
    /** 所有行数*/
    private int allLines;
    /** 是否是收起状态,默认收起*/
    private boolean collapsed = true;
    /** 真实的text*/
    private String text;
    /** 收起时实际显示的text*/
    private CharSequence collapsedCs;
    /** 全文和收起的点击事件处理*/
    private ReadMoreClickableSpan viewMoreSpan = new ReadMoreClickableSpan();

    public CollapsedTextView(Context context) {
        super(context);
        init(context,null);
    }

    public CollapsedTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context,attrs);
    }

    public CollapsedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context,attrs);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public CollapsedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context,attrs);
    }

    @Override
    public TextPaint getPaint() {
        return super.getPaint();
    }

    private void init(Context context,AttributeSet attrs){
        if(attrs != null){
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CollapsedTextView);
            allLines = ta.getInt(R.styleable.CollapsedTextView_trimLines,0);
            expandedText = ta.getString(R.styleable.CollapsedTextView_expandedText);
            if(TextUtils.isEmpty(expandedText)){
                expandedText = EXPANDEDTEXT;
            }
            collapsedText = ta.getString(R.styleable.CollapsedTextView_collapsedText);
            if(TextUtils.isEmpty(collapsedText)){
                collapsedText = COLLAPSEDTEXT;
            }
        }

    }
    public void setShowText(final String text){
        this.text = text;
        if(allLines > 0) {
            getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    ViewTreeObserver obs = getViewTreeObserver();
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                        obs.removeOnGlobalLayoutListener(this);
                    } else {
                        obs.removeGlobalOnLayoutListener(this);
                    }
                    TextPaint tp = getPaint();
                    float width = tp.measureText(text);
                /* 计算行数 */
                    //获取显示宽度
                    int showWidth = getWidth() - getPaddingRight() - getPaddingLeft();
                    int lines = (int) (width / showWidth);
                    if (width % showWidth != 0) {
                        lines++;
                    }
                    allLines = (int) (tp.measureText(text + collapsedText) / showWidth);
                    if (lines > maxLine) {
                        int expect = text.length() / lines;
                        int end = 0;
                        int lastLineEnd = 0;
                        //...+expandedText的宽度,需要在最后一行加入计算
                        int expandedTextWidth = (int) tp.measureText(ELLIPSE + expandedText);
                        //计算每行显示文本数
                        for (int i = 1; i <= maxLine; i++) {
                            int tempWidth = 0;
                            if (i == maxLine) {

                                tempWidth = expandedTextWidth;
                            }
                            end += expect;
                            if (end > text.length()) {
                                end = text.length();
                            }
                            if (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) {
                                //预期的第一行超过了实际显示的宽度
                                end--;
                                while (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) {
                                    end--;
                                }
                            } else {
                                end++;
                                while (tp.measureText(text, lastLineEnd, end) < showWidth - tempWidth) {
                                    end++;
                                }
                                end--;
                            }
                            lastLineEnd = end;
                        }
                        SpannableStringBuilder s = new SpannableStringBuilder(text, 0, end)
                                .append(ELLIPSE)
                                .append(expandedText);
                        collapsedCs = addClickableSpan(s, expandedText);
                        setText(collapsedCs);

                        setMovementMethod(LinkMovementMethod.getInstance());
                    } else {
                        setText(text);
                    }
                }
            });
            setText("");
        }else{
            setText(text);
        }
    }


    private CharSequence addClickableSpan(SpannableStringBuilder s, CharSequence trimText) {
        s.setSpan(viewMoreSpan, s.length() - trimText.length(), s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        return s;
    }

    private class ReadMoreClickableSpan extends ClickableSpan {
        @Override
        public void onClick(final View widget) {
            if(collapsed){
                SpannableStringBuilder s = new SpannableStringBuilder(text)
                        .append(collapsedText);
                setText(addClickableSpan(s,collapsedText));
            }else{
                setText(collapsedCs);
            }
            collapsed = !collapsed;
        }
    }
}

详细说明

核心代码
TextPaint tp = getPaint();  
                    float width = tp.measureText(text);
                /* 计算行数 */
                    //获取显示宽度
                    int showWidth = getWidth() - getPaddingRight() - getPaddingLeft();
                    int lines = (int) (width / showWidth);
                    if (width % showWidth != 0) {
                        lines++;
                    }
                    allLines = (int) (tp.measureText(text + collapsedText) / showWidth);
                    if (lines > maxLine) {
                        int expect = text.length() / lines;
                        int end = 0;
                        int lastLineEnd = 0;
                        //...+expandedText的宽度,需要在最后一行加入计算
                        int expandedTextWidth = (int) tp.measureText(ELLIPSE + expandedText);
                        //计算每行显示文本数
                        for (int i = 1; i <= maxLine; i++) {
                            int tempWidth = 0;
                            if (i == maxLine) {

                                tempWidth = expandedTextWidth;
                            }
                            end += expect;
                            if (end > text.length()) {
                                end = text.length();
                            }
                            if (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) {
                                //预期的第一行超过了实际显示的宽度
                                end--;
                                while (tp.measureText(text, lastLineEnd, end) > showWidth - tempWidth) {
                                    end--;
                                }
                            } else {
                                end++;
                                while (tp.measureText(text, lastLineEnd, end) < showWidth - tempWidth) {
                                    end++;
                                }
                                end--;
                            }
                            lastLineEnd = end;
                        }
                        SpannableStringBuilder s = new SpannableStringBuilder(text, 0, end)
                                .append(ELLIPSE)
                                .append(expandedText);
                        collapsedCs = addClickableSpan(s, expandedText);
                        setText(collapsedCs);

                        setMovementMethod(LinkMovementMethod.getInstance());
                    } else {
                        setText(text);
                    }

通过TextPaint计算出文本的总宽度,粗略计算出一共需要多少行来显示,判断是否需要收起和全文功能,如果需要,则计算出每行实际展示的文本的宽度(因为通过Layout获取到的只有完全绘制成功后,才能正确获取到),同时在计算的最后一行(也就是超过多少行需要收起的最后一行),需要把"...全文"的宽度加入计算,这样才能计算出正确值,把计算出来的字符数截取出来,加入"...全文",同时针对"全文"本身,添加点击的ClickableSpan,使"全文"具有点击事件,最后设置控件展示的文本为截取的文本+"...全文",如果行数没有超过最大行数,则设置正常显示就ok了,同时保存计算出来的文本,避免再次计算。

点击事件
private CharSequence addClickableSpan(SpannableStringBuilder s, CharSequence trimText) {  
        s.setSpan(viewMoreSpan, s.length() - trimText.length(), s.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        return s;
    }

    private class ReadMoreClickableSpan extends ClickableSpan {
        @Override
        public void onClick(final View widget) {
            if(collapsed){
                SpannableStringBuilder s = new SpannableStringBuilder(text)
                        .append(collapsedText);
                setText(addClickableSpan(s,collapsedText));
            }else{
                setText(collapsedCs);
            }
            collapsed = !collapsed;
        }
    }

通过setSpan设置"全文"的点击事件,同时通过继承ClickableSpan 来实现点击事件,事件中根据当前的状态,判断需要设置什么文本,如果是收起状态,则设置文本显示内容为实际内容+"收起",同时给收起添加点击事件,如果是全文状态,则设置显示的文本为之前计算出来的文本。

自定义属性
private void init(Context context,AttributeSet attrs){  
        if(attrs != null){
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CollapsedTextView);
            allLines = ta.getInt(R.styleable.CollapsedTextView_trimLines,0);
            expandedText = ta.getString(R.styleable.CollapsedTextView_expandedText);
            if(TextUtils.isEmpty(expandedText)){
                expandedText = EXPANDEDTEXT;
            }
            collapsedText = ta.getString(R.styleable.CollapsedTextView_collapsedText);
            if(TextUtils.isEmpty(collapsedText)){
                collapsedText = COLLAPSEDTEXT;
            }
        }

    }

这里就很简单了,就是自义定最大多少行,"全文"的文本和"收起"的文本,相信不用多少

最后说两句

最近因为太忙,所以文章也写的有点水,而且总是感觉累,是身体加心累,每天躺床上就不想起床,也不喜欢敲代码,效率自热底下。同时也建议各位同行注意身体,身体才是革命的本钱,同时也要注意放松,不然心一旦累了,就很难调整过来了(对于我来说是这样),敲会代码就起身走动走动,前几天因为一直坐着敲代码,脖子痛的要命,所以适当的休息是必要的,好了,就说这么多吧,你们懂的。同时附上本控件源码和demo github链接,另外同时也欢迎大家吐槽交流(QQ群:123965382)

标签:Android, 自定义控件
下一篇:   设计模式之单例模式
上一篇:   Android自定义控件之全文收起TextView(控件嵌套法)

Copyright © 2017 Raye Blog

如果我的文章对你有帮助,或许可以打赏一下呀!

支付宝
微信