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

Posted Kevin_小飞象

tags:

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

前言

我们经常会碰到这样一个需求:文本内容过多,可以展开和收起。

效果图



注意

  • 显示 “…展开” 时,是截取的一定行数之后,在最后一行的末尾直接显示;
  • “收起” 显示在全部文本的下一行,并且是右对齐;
  • 展开和收起的动画效果。

代码实现

1. ExpandTextView.java

public class ExpandTextView extends AppCompatTextView 
    public static final String ELLIPSIS_STRING = new String(new char[]'\\u2026');
    private static final int DEFAULT_MAX_LINE = 3;
    private static final String DEFAULT_OPEN_SUFFIX = " 展开";
    private static final String DEFAULT_CLOSE_SUFFIX = " 收起";
    volatile boolean animating = false;
    boolean isClosed = false;
    private int mMaxLines = DEFAULT_MAX_LINE;
    /** TextView可展示宽度,包含paddingLeft和paddingRight */
    private int initWidth = 0;
    /** 原始文本 */
    private CharSequence originalText;

    private SpannableStringBuilder mOpenSpannableStr, mCloseSpannableStr;

    private boolean hasAnimation = false;
    private Animation mOpenAnim, mCloseAnim;
    private int mOpenHeight, mCLoseHeight;
    private boolean mExpandable;
    private boolean mCloseInNewLine;
    @Nullable
    private SpannableString mOpenSuffixSpan, mCloseSuffixSpan;
    private String mOpenSuffixStr = DEFAULT_OPEN_SUFFIX;
    private String mCloseSuffixStr = DEFAULT_CLOSE_SUFFIX;
    private int mOpenSuffixColor, mCloseSuffixColor;

    private View.OnClickListener mOnClickListener;

    private CharSequenceToSpannableHandler mCharSequenceToSpannableHandler;

    public ExpandTextView(Context context) 
        this(context,null);
    

    public ExpandTextView(Context context, AttributeSet attrs) 
        this(context, attrs,0);
    

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

    /** 初始化 */
    private void initialize() 
        mOpenSuffixColor = mCloseSuffixColor = Color.parseColor("#F23030");
        setMovementMethod(OverLinkMovementMethod.getInstance());
        setIncludeFontPadding(false);
        updateOpenSuffixSpan();
        updateCloseSuffixSpan();
    

    @Override
    public boolean hasOverlappingRendering() 
        return false;
    

    public void setOriginalText(CharSequence originalText) 
        this.originalText = originalText;
        mExpandable = false;
        mCloseSpannableStr = new SpannableStringBuilder();
        final int maxLines = mMaxLines;
        SpannableStringBuilder tempText = charSequenceToSpannable(originalText);
        mOpenSpannableStr = charSequenceToSpannable(originalText);

        if (maxLines != -1) 
            Layout layout = createStaticLayout(tempText);
            mExpandable = layout.getLineCount() > maxLines;
            if (mExpandable) 
                //拼接展开内容
                if (mCloseInNewLine) 
                    mOpenSpannableStr.append("\\n");
                
                if (mCloseSuffixSpan != null) 
                    mOpenSpannableStr.append(mCloseSuffixSpan);
                
                //计算原文截取位置
                int endPos = layout.getLineEnd(maxLines - 1);
                if (originalText.length() <= endPos) 
                    mCloseSpannableStr = charSequenceToSpannable(originalText);
                 else 
                    mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, endPos));
                
                SpannableStringBuilder tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
                if (mOpenSuffixSpan != null) 
                    tempText2.append(mOpenSuffixSpan);
                
                //循环判断,收起内容添加展开后缀后的内容
                Layout tempLayout = createStaticLayout(tempText2);
                while (tempLayout.getLineCount() > maxLines) 
                    int lastSpace = mCloseSpannableStr.length() - 1;
                    if (lastSpace == -1) 
                        break;
                    
                    if (originalText.length() <= lastSpace) 
                        mCloseSpannableStr = charSequenceToSpannable(originalText);
                     else 
                        mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));
                    
                    tempText2 = charSequenceToSpannable(mCloseSpannableStr).append(ELLIPSIS_STRING);
                    if (mOpenSuffixSpan != null) 
                        tempText2.append(mOpenSuffixSpan);
                    
                    tempLayout = createStaticLayout(tempText2);

                
                int lastSpace = mCloseSpannableStr.length() - mOpenSuffixSpan.length();
                if(lastSpace >= 0 && originalText.length() > lastSpace)
                    CharSequence redundantChar = originalText.subSequence(lastSpace, lastSpace + mOpenSuffixSpan.length());
                    int offset = hasEnCharCount(redundantChar) - hasEnCharCount(mOpenSuffixSpan) + 1;
                    lastSpace = offset <= 0 ? lastSpace : lastSpace - offset;
                    mCloseSpannableStr = charSequenceToSpannable(originalText.subSequence(0, lastSpace));
                
                //计算收起的文本高度
                mCLoseHeight = tempLayout.getHeight() + getPaddingTop() + getPaddingBottom();

                mCloseSpannableStr.append(ELLIPSIS_STRING);
                if (mOpenSuffixSpan != null) 
                    mCloseSpannableStr.append(mOpenSuffixSpan);
                
            
        
        isClosed = mExpandable;
        if (mExpandable) 
            setText(mCloseSpannableStr);
            //设置监听
            super.setOnClickListener(new OnClickListener() 
                @Override
                public void onClick(View v) 
//                    switchOpenClose();
//                    if (mOnClickListener != null) 
//                        mOnClickListener.onClick(v);
//                    
                
            );
         else 
            setText(mOpenSpannableStr);
        
    

    private int hasEnCharCount(CharSequence str)
        int count = 0;
        if(!TextUtils.isEmpty(str))
            for (int i = 0; i < str.length(); i++) 
                char c = str.charAt(i);
                if(c >= ' ' && c <= '~')
                    count++;
                
            
        
        return count;
    

    private void switchOpenClose() 
        if (mExpandable) 
            isClosed = !isClosed;
            if (isClosed) 
                close();
             else 
                open();
            
        
    

    /**
     * 设置是否有动画
     *
     * @param hasAnimation
     */
    public void setHasAnimation(boolean hasAnimation) 
        this.hasAnimation = hasAnimation;
    

    /** 展开 */
    private void open() 
        if (hasAnimation) 
            Layout layout = createStaticLayout(mOpenSpannableStr);
            mOpenHeight = layout.getHeight() + getPaddingTop() + getPaddingBottom();
            executeOpenAnim();
         else 
            ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
            setText(mOpenSpannableStr);
            if (mOpenCloseCallback != null)
                mOpenCloseCallback.onOpen();
            
        
    

    /** 收起 */
    private void close() 
        if (hasAnimation) 
            executeCloseAnim();
         else 
            ExpandTextView.super.setMaxLines(mMaxLines);
            setText(mCloseSpannableStr);
            if (mOpenCloseCallback != null)
                mOpenCloseCallback.onClose();
            
        
    

    /** 执行展开动画 */
    private void executeOpenAnim() 
        //创建展开动画
        if (mOpenAnim == null) 
            mOpenAnim = new ExpandCollapseAnimation(this, mCLoseHeight, mOpenHeight);
            mOpenAnim.setFillAfter(true);
            mOpenAnim.setAnimationListener(new Animation.AnimationListener() 
                @Override
                public void onAnimationStart(Animation animation) 
                    ExpandTextView.super.setMaxLines(Integer.MAX_VALUE);
                    setText(mOpenSpannableStr);
                

                @Override
                public void onAnimationEnd(Animation animation) 
                    //  动画结束后textview设置展开的状态
                    getLayoutParams().height = mOpenHeight;
                    requestLayout();
                    animating = false;
                

                @Override
                public void onAnimationRepeat(Animation animation) 

                
            );
        

        if (animating) 
            return;
        
        animating = true;
        clearAnimation();
        //  执行动画
        startAnimation(mOpenAnim);
    

    /** 执行收起动画 */
    private void executeCloseAnim() 
        //创建收起动画
        if (mCloseAnim == null) 
            mCloseAnim = new ExpandCollapseAnimation(this, mOpenHeight, mCLoseHeight);
            mCloseAnim.setFillAfter(true);
            mCloseAnim.setAnimationListener(new Animation.AnimationListener() 
                @Override
                public void onAnimationStart(Animation animation) 

                

                @Override
                public void onAnimationEnd(Animation animation) 
                    animating = false;
                    ExpandTextView.super.setMaxLines(mMaxLines);
                    setText(mCloseSpannableStr);
                    getLayoutParams().height = mCLoseHeight;
                    requestLayout();
                

                @Override
                public void onAnimationRepeat(Animation animation) 

                
            );
        

        if (animating) 
            return;
        
        animating = true;
        clearAnimation();
        //  执行动画
        startAnimation(mCloseAnim);
    

    /**
     * @param spannable
     *
     * @return
     */
    private Layout createStaticLayout(SpannableStringBuilder spannable) 
        int contentWidth = initWidth - getPaddingLeft() - getPaddingRight();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
            StaticLayout.Builder builder = StaticLayout.Builder.obtain(spannable, 0, spannable.length(), getPaint(), contentWidth);
            builder.setAlignment(Layout.Alignment.ALIGN_NORMAL);
            builder.setIncludePad(getIncludeFontPadding());
            builder.setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier());
            return builder.build();
        else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) 
            return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
                    getLineSpacingMultiplier(), getLineSpacingExtra(), getIncludeFontPadding());
        else
            return new StaticLayout(spannable, getPaint(), contentWidth, Layout.Alignment.ALIGN_NORMAL,
                    getFloatField("mSpacingMult",1f), getFloatField("mSpacingAdd",0f), getIncludeFontPadding());
        
    

    private float getFloatField(String fieldName,float defaultValue)
        float value = defaultValue;
        if(TextUtils.isEmpty(fieldName))
            return value;
        
        try 
            // 获取该类的所有属性值域
            Field[] fields = this.getClass().getDeclaredFields();
            for (Field field:fields) 
                if(TextUtils.equals(fieldName,field.getName()))
                    value = field.getFloat(this);
                    break;
                
            
         catch (IllegalAccessException e) 
            e.printStackTrace();
        
        return value;
    


    /**
     * @param charSequence
     *
     * @return
     */
    private SpannableStringBuilder charSequenceToSpannable(@NonNull CharSequence charSequence) 
        SpannableStringBuilder spannableStringBuilder = null;
        if (mCharSequenceToSpannableHandler != null) 
            spannableStringBuilder = mCharSequenceToSpannableHandler.charSequenceToSpannable(charSequence);
        
        if (spannableStringBuilder == null) 
            spannableStringBuilder = new SpannableStringBuilder(charSequence);
        
        return spannableStringBuilder;
    

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

    @Override
    public void setMaxLines(int maxLines) 
        Android自定义View

Android 自定义View

android自定义的dialog怎么设置view

android自定义的dialog怎么设置view

Android 自定义view之悬浮动画

android文字横向滚动的自定义view